@nativescript/windows 0.1.0-alpha.17 → 0.1.0-alpha.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,100 @@
1
+ using System;
2
+ using System.Buffers;
3
+ using System.Linq;
4
+ using System.Linq.Expressions;
5
+ using System.Reflection;
6
+ using System.Runtime.InteropServices;
7
+ using System.Threading;
8
+
9
+ namespace NativeScriptBridge;
10
+
11
+ public static partial class Bridge
12
+ {
13
+ // ── JS delegate creation ──────────────────────────────────────────────────
14
+ //
15
+ // opcode 0x09: given a delegate type name (or "" for System.Action) and a
16
+ // JS callback id, compile a .NET delegate that serialises its parameters as
17
+ // binary and calls back into V8 via the s_jsInvoker function pointer.
18
+
19
+ private static DispatchResult CreateJsDelegate(string typeName, int callbackId)
20
+ {
21
+ var delegateType = string.IsNullOrEmpty(typeName)
22
+ ? typeof(Action)
23
+ : ResolveType(null, typeName)
24
+ ?? throw new TypeLoadException($"Delegate type not found: {typeName}");
25
+
26
+ var invokeMethod = delegateType.GetMethod("Invoke")
27
+ ?? throw new MissingMethodException($"No Invoke method on {delegateType}");
28
+
29
+ var parameters = invokeMethod.GetParameters();
30
+ var returnType = invokeMethod.ReturnType;
31
+
32
+ var paramExprs = parameters
33
+ .Select((p, i) => Expression.Parameter(p.ParameterType, p.Name ?? $"p{i}"))
34
+ .ToArray();
35
+
36
+ // object?[] args = new object?[] { (object?)p0, (object?)p1, … }
37
+ var objArgs = paramExprs.Select(p => (Expression)Expression.Convert(p, typeof(object)));
38
+ var argsArray = Expression.NewArrayInit(typeof(object), objArgs);
39
+
40
+ var callMethod = typeof(Bridge).GetMethod(
41
+ nameof(CallJsCallback),
42
+ BindingFlags.Static | BindingFlags.NonPublic)!;
43
+
44
+ var callExpr = Expression.Call(callMethod, Expression.Constant(callbackId), argsArray);
45
+
46
+ Expression body = returnType == typeof(void)
47
+ ? callExpr
48
+ : (Expression)Expression.Block(returnType, callExpr, Expression.Default(returnType));
49
+
50
+ var del = Expression.Lambda(delegateType, body, paramExprs).Compile();
51
+ return Box(del);
52
+ }
53
+
54
+ // ── Callback invocation helper ────────────────────────────────────────────
55
+
56
+ internal static unsafe void CallJsCallback(int id, object?[] args)
57
+ {
58
+ if (s_jsInvoker == null) return;
59
+
60
+ var buf = new ArrayBufferWriter<byte>(64);
61
+ var w = new BinWriter(buf);
62
+ w.WriteByte((byte)Math.Min(args.Length, 255));
63
+ foreach (var arg in args)
64
+ WriteCallbackArg(ref w, arg);
65
+
66
+ var bytes = buf.WrittenSpan;
67
+ byte* respPtr = null;
68
+ int respLen = 0;
69
+ fixed (byte* p = bytes)
70
+ s_jsInvoker(id, p, bytes.Length, &respPtr, &respLen);
71
+
72
+ // Response is only set for non-void delegates; free it when present.
73
+ if (respPtr != null && respLen > 0)
74
+ Marshal.FreeHGlobal((IntPtr)respPtr);
75
+ }
76
+
77
+ // Serialises a single delegate argument in response-binary tag format so
78
+ // the Rust side can reuse its existing bin_read_value parser.
79
+ private static void WriteCallbackArg(ref BinWriter w, object? arg)
80
+ {
81
+ if (arg is null) { w.WriteByte(0x00); return; }
82
+ if (arg is bool b) { w.WriteByte(b ? (byte)0x02 : (byte)0x01); return; }
83
+ if (arg is int i) { w.WriteByte(0x03); w.WriteI32(i); return; }
84
+ if (arg is uint u) { w.WriteByte(0x03); w.WriteI32((int)u); return; }
85
+ if (arg is long l) { w.WriteByte(0x04); w.WriteF64((double)l); return; }
86
+ if (arg is float f) { w.WriteByte(0x04); w.WriteF64((double)f); return; }
87
+ if (arg is double d){ w.WriteByte(0x04); w.WriteF64(d); return; }
88
+ if (arg is string s){ w.WriteByte(0x05); w.WriteString32(s); return; }
89
+
90
+ // Object: box in the handle map and send as a handle reference.
91
+ // The JS side receives {__handle, __type} which can be turned into a
92
+ // proxy via NSWinRT.dotnet.fromHandle(...).
93
+ var handleId = Interlocked.Increment(ref s_nextHandle);
94
+ s_handles[handleId] = arg;
95
+ var typeName = arg.GetType().FullName ?? arg.GetType().Name;
96
+ w.WriteByte(0x06);
97
+ w.WriteI32(handleId);
98
+ w.WriteString16(typeName); // u16 length prefix (matches bin_read_value tag 0x06)
99
+ }
100
+ }
@@ -1,353 +1,77 @@
1
1
  using System;
2
- using System.Collections;
3
2
  using System.Collections.Concurrent;
4
- using System.Collections.Generic;
5
- using System.Linq;
6
3
  using System.Reflection;
7
4
  using System.Runtime.InteropServices;
8
5
  using System.Text.Json;
9
- using System.Text.Json.Nodes;
10
- using System.Threading;
11
- using System.Threading.Tasks;
6
+
7
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeTests")]
8
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeBenchmarks")]
12
9
 
13
10
  namespace NativeScriptBridge;
14
11
 
15
- /// <summary>
16
- /// Reflection-based .NET dispatch bridge — UTF-8 ABI edition.
17
- ///
18
- /// Invoke() receives a UTF-8 byte slice (no string allocation on the Rust side),
19
- /// dispatches via reflection, and serialises the response directly to a UTF-8
20
- /// byte array via JsonSerializer.SerializeToUtf8Bytes(). This removes the
21
- /// UTF-16 encode/decode round-trip of the original char* ABI.
22
- ///
23
- /// Request JSON schema
24
- /// -------------------
25
- /// Static call: { "assembly": "System", "typeName": "System.Diagnostics.Stopwatch", "method": "StartNew", "args": [] }
26
- /// Constructor: { "assembly": "System", "typeName": "System.Text.StringBuilder", "method": ".ctor", "args": [128] }
27
- /// Instance call: { "handle": 3, "method": "Stop", "args": [] }
28
- /// Property get: { "assembly": "...", "typeName": "...", "method": "get_Now", "args": [] }
29
- /// Property set: { "handle": 3, "method": "set_IsEnabled", "args": [true] }
30
- /// Release: { "handle": 3, "method": "__release", "args": [] }
31
- ///
32
- /// Response JSON schema
33
- /// --------------------
34
- /// Primitive / string: { "result": 42.5 }
35
- /// Managed object: { "result": { "__handle": 7, "__type": "System.Diagnostics.Stopwatch" } }
36
- /// Array / enumerable: { "result": [1, 2, 3] }
37
- /// Error: { "error": "Method not found" }
38
- /// </summary>
39
- public static class Bridge
12
+ public static partial class Bridge
40
13
  {
41
- private static readonly ConcurrentDictionary<int, object?> s_handles = new();
42
- private static int s_nextHandle;
14
+ internal static readonly ConcurrentDictionary<int, object?> s_handles = new();
15
+ internal static int s_nextHandle;
16
+
17
+ // Function pointer registered by the Rust runtime so managed delegates can
18
+ // call back into V8 without a JSON round-trip.
19
+ internal static unsafe delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void>
20
+ s_jsInvoker;
21
+
22
+ private static readonly ConcurrentDictionary<string, Type?> s_typeCache
23
+ = new(StringComparer.Ordinal);
24
+ private static readonly ConcurrentDictionary<MethodKey, DispatchEntry> s_methodCache = new();
25
+ private static readonly ConcurrentDictionary<PropKey, PropertyInfo?> s_propCache = new();
26
+ private static readonly ConcurrentDictionary<CtorKey, CtorEntry> s_ctorCache = new();
27
+
28
+ private static readonly JsonSerializerOptions s_coerceOpts = new()
29
+ {
30
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
31
+ };
43
32
 
44
- private static readonly ConcurrentDictionary<string, Type?> s_typeCache = new();
45
- private static readonly ConcurrentDictionary<string, MethodInfo?> s_methodCache = new();
46
- private static readonly ConcurrentDictionary<string, PropertyInfo?> s_propertyCache = new();
33
+ internal static void ClearCaches()
34
+ {
35
+ s_typeCache.Clear();
36
+ s_methodCache.Clear();
37
+ s_propCache.Clear();
38
+ s_ctorCache.Clear();
39
+ s_handles.Clear();
40
+ s_nextHandle = 0;
41
+ }
47
42
 
48
- // ── exported entry points ────────────────────────────────────────────────
43
+ [UnmanagedCallersOnly(EntryPoint = "RegisterJsCallback",
44
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
45
+ public static unsafe int RegisterJsCallback(
46
+ delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void> callback)
47
+ {
48
+ s_jsInvoker = callback;
49
+ return 0;
50
+ }
49
51
 
50
- /// Called by the Rust runtime. Request arrives as a raw UTF-8 byte slice;
51
- /// response is written as a UTF-8 byte buffer allocated with Marshal.AllocHGlobal.
52
52
  [UnmanagedCallersOnly(EntryPoint = "Invoke",
53
53
  CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
54
54
  public static unsafe int Invoke(
55
55
  byte* requestPtr, int requestLen,
56
56
  byte** responsePtr, int* responseLenPtr)
57
57
  {
58
- byte[] responseBytes;
59
58
  try
60
59
  {
61
- var requestSpan = new ReadOnlySpan<byte>(requestPtr, requestLen);
62
- var req = JsonSerializer.Deserialize<InvokeRequest>(requestSpan, JsonOptions.Default)!;
63
- var result = Dispatch(req);
64
- responseBytes = JsonSerializer.SerializeToUtf8Bytes(result, JsonOptions.Default);
60
+ var span = new ReadOnlySpan<byte>(requestPtr, requestLen);
61
+ var req = JsonSerializer.Deserialize(span, BridgeJsonContext.Default.InvokeRequest)!;
62
+ WriteResult(Dispatch(req), responsePtr, responseLenPtr);
65
63
  }
66
64
  catch (Exception ex)
67
65
  {
68
- var errResult = new InvokeResult(null, Unwrap(ex).Message);
69
- responseBytes = JsonSerializer.SerializeToUtf8Bytes(errResult, JsonOptions.Default);
66
+ WriteError(Unwrap(ex).Message, responsePtr, responseLenPtr);
70
67
  }
71
-
72
- WriteResponse(responseBytes, responsePtr, responseLenPtr);
73
68
  return 0;
74
69
  }
75
70
 
76
- /// Frees the response buffer allocated by Invoke.
77
71
  [UnmanagedCallersOnly(EntryPoint = "Free",
78
72
  CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
79
73
  public static unsafe void Free(byte* ptr)
80
74
  {
81
- if (ptr != null)
82
- Marshal.FreeHGlobal((IntPtr)ptr);
83
- }
84
-
85
- // ── dispatch ─────────────────────────────────────────────────────────────
86
-
87
- private static InvokeResult Dispatch(InvokeRequest req)
88
- {
89
- if (req.Method == "__release" && req.Handle.HasValue)
90
- {
91
- s_handles.TryRemove(req.Handle.Value, out _);
92
- return new InvokeResult(null, null);
93
- }
94
-
95
- // Return reflection metadata so JS can distinguish methods from properties.
96
- if (req.Method == "__members__")
97
- {
98
- var t = req.Handle.HasValue
99
- ? (s_handles.TryGetValue(req.Handle.Value, out var h) ? h?.GetType() : null)
100
- : ResolveType(req.Assembly, req.TypeName);
101
- if (t is null) return new InvokeResult(null, $"Type not found: {req.TypeName}");
102
- var inst = BindingFlags.Public | BindingFlags.Instance;
103
- var stat = BindingFlags.Public | BindingFlags.Static;
104
- var members = new
105
- {
106
- methods = t.GetMethods(inst).Where(m => !m.IsSpecialName).Select(m => m.Name).Distinct().ToArray(),
107
- properties = t.GetProperties(inst).Select(p => p.Name).Distinct().ToArray(),
108
- staticMethods = t.GetMethods(stat).Where(m => !m.IsSpecialName).Select(m => m.Name).Distinct().ToArray(),
109
- staticProperties = t.GetProperties(stat).Select(p => p.Name).Distinct().ToArray(),
110
- };
111
- return new InvokeResult(JsonSerializer.SerializeToElement(members, JsonOptions.Default), null);
112
- }
113
-
114
- object? target = null;
115
- Type? type = null;
116
-
117
- if (req.Handle.HasValue)
118
- {
119
- if (!s_handles.TryGetValue(req.Handle.Value, out target))
120
- return new InvokeResult(null, $"Invalid handle {req.Handle.Value}");
121
- type = target?.GetType();
122
- }
123
- else
124
- {
125
- type = ResolveType(req.Assembly, req.TypeName);
126
- if (type is null)
127
- return new InvokeResult(null, $"Type not found: {req.TypeName}");
128
- }
129
-
130
- if (type is null)
131
- return new InvokeResult(null, "Cannot determine target type");
132
-
133
- var method = req.Method ?? throw new ArgumentException("Method is required");
134
-
135
- // ── Constructor ──────────────────────────────────────────────────────
136
- if (method == ".ctor")
137
- {
138
- var argElements = req.Args ?? [];
139
- var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
140
- var ctor = ctors.FirstOrDefault(c => c.GetParameters().Length == argElements.Count)
141
- ?? ctors.FirstOrDefault();
142
- if (ctor is null)
143
- return new InvokeResult(null, $"No public constructor found on {type.FullName}");
144
- var pars = ctor.GetParameters();
145
- var ctorArgs = new object?[pars.Length];
146
- for (int i = 0; i < pars.Length && i < argElements.Count; i++)
147
- ctorArgs[i] = Coerce(argElements[i], pars[i].ParameterType);
148
- return Box(ctor.Invoke(ctorArgs));
149
- }
150
-
151
- var isStatic = target is null;
152
- var flags = (isStatic ? BindingFlags.Static : BindingFlags.Instance)
153
- | BindingFlags.Public;
154
-
155
- // ── Property getter / setter ─────────────────────────────────────────
156
- if (method.StartsWith("get_", StringComparison.Ordinal))
157
- {
158
- var prop = GetCachedProperty(type, method[4..], flags);
159
- if (prop is not null)
160
- return Box(prop.GetValue(target));
161
- }
162
- if (method.StartsWith("set_", StringComparison.Ordinal) && req.Args?.Count == 1)
163
- {
164
- var prop = GetCachedProperty(type, method[4..], flags);
165
- if (prop is not null)
166
- {
167
- prop.SetValue(target, Coerce(req.Args[0], prop.PropertyType));
168
- return new InvokeResult(null, null);
169
- }
170
- }
171
-
172
- // ── Regular method ───────────────────────────────────────────────────
173
- var argElems = req.Args ?? [];
174
- var mi = FindMethod(type, method, argElems.Count, flags);
175
- if (mi is null)
176
- return new InvokeResult(null,
177
- $"Method '{method}' ({argElems.Count} args) not found on {type.FullName}");
178
-
179
- var parameters = mi.GetParameters();
180
- var callArgs = new object?[parameters.Length];
181
- for (int i = 0; i < parameters.Length && i < argElems.Count; i++)
182
- callArgs[i] = Coerce(argElems[i], parameters[i].ParameterType);
183
-
184
- return Box(AwaitIfTask(mi.Invoke(target, callArgs)));
185
- }
186
-
187
- // ── helpers ───────────────────────────────────────────────────────────────
188
-
189
- private static object? AwaitIfTask(object? value)
190
- {
191
- if (value is null) return null;
192
- var t = value.GetType();
193
-
194
- if (value is Task task)
195
- {
196
- task.Wait();
197
- if (task.IsFaulted)
198
- throw task.Exception!.InnerException ?? task.Exception;
199
- return t.GetProperty("Result")?.GetValue(value);
200
- }
201
-
202
- if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueTask<>))
203
- {
204
- var innerTask = (Task)t.GetMethod("AsTask")!.Invoke(value, null)!;
205
- innerTask.Wait();
206
- if (innerTask.IsFaulted)
207
- throw innerTask.Exception!.InnerException ?? innerTask.Exception;
208
- return innerTask.GetType().GetProperty("Result")?.GetValue(innerTask);
209
- }
210
-
211
- if (value is System.Threading.Tasks.ValueTask vt)
212
- {
213
- vt.AsTask().Wait();
214
- return null;
215
- }
216
-
217
- return value;
218
- }
219
-
220
- private static Type? ResolveType(string? assemblyName, string? typeName)
221
- {
222
- if (string.IsNullOrEmpty(typeName)) return null;
223
- var key = string.IsNullOrEmpty(assemblyName) ? typeName : $"{assemblyName}|{typeName}";
224
- return s_typeCache.GetOrAdd(key, _ => ResolveTypeCore(assemblyName, typeName));
225
- }
226
-
227
- private static Type? ResolveTypeCore(string? assemblyName, string? typeName)
228
- {
229
- var fqn = string.IsNullOrEmpty(assemblyName) ? typeName! : $"{typeName}, {assemblyName}";
230
- var t = Type.GetType(fqn);
231
- if (t is not null) return t;
232
-
233
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
234
- {
235
- t = asm.GetType(typeName!);
236
- if (t is not null) return t;
237
- }
238
-
239
- if (!string.IsNullOrEmpty(assemblyName))
240
- {
241
- try
242
- {
243
- var asm = Assembly.Load(assemblyName);
244
- t = asm.GetType(typeName!);
245
- if (t is not null) return t;
246
- }
247
- catch { }
248
- }
249
-
250
- return null;
251
- }
252
-
253
- private static MethodInfo? FindMethod(Type type, string name, int argCount, BindingFlags flags)
254
- {
255
- var key = $"{type.FullName}|{name}|{argCount}|{(int)flags}";
256
- return s_methodCache.GetOrAdd(key, _ => FindMethodCore(type, name, argCount, flags));
257
- }
258
-
259
- private static MethodInfo? FindMethodCore(Type type, string name, int argCount, BindingFlags flags)
260
- {
261
- var match = Array.Find(
262
- type.GetMethods(flags),
263
- m => m.Name == name && !m.IsGenericMethod && m.GetParameters().Length == argCount);
264
- if (match is not null) return match;
265
- return type.GetMethod(name, flags);
266
- }
267
-
268
- private static PropertyInfo? GetCachedProperty(Type type, string name, BindingFlags flags)
269
- {
270
- var key = $"{type.FullName}|{name}|{(int)flags}";
271
- return s_propertyCache.GetOrAdd(key, _ => type.GetProperty(name, flags));
272
- }
273
-
274
- private static object? Coerce(JsonElement el, Type targetType)
275
- {
276
- if (el.ValueKind == JsonValueKind.Null) return null;
277
- if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("__handle", out var h))
278
- {
279
- s_handles.TryGetValue(h.GetInt32(), out var obj);
280
- return obj;
281
- }
282
- return JsonSerializer.Deserialize(el.GetRawText(), targetType, JsonOptions.Default);
283
- }
284
-
285
- private static InvokeResult Box(object? value)
286
- {
287
- if (value is null) return new InvokeResult(null, null);
288
-
289
- var t = value.GetType();
290
-
291
- if (t.IsPrimitive || t == typeof(string) || t == typeof(decimal)
292
- || t == typeof(DateTime) || t == typeof(DateTimeOffset)
293
- || t == typeof(TimeSpan) || t == typeof(Guid))
294
- {
295
- return new InvokeResult(
296
- JsonSerializer.SerializeToElement(value, t, JsonOptions.Default), null);
297
- }
298
-
299
- // Arrays and IEnumerable — serialise as JSON arrays.
300
- if (t.IsArray || (t != typeof(string) && value is IEnumerable enumerable))
301
- {
302
- try
303
- {
304
- var list = new List<object?>();
305
- foreach (var item in (IEnumerable)value) list.Add(item);
306
- return new InvokeResult(
307
- JsonSerializer.SerializeToElement(list, typeof(List<object?>), JsonOptions.Default),
308
- null);
309
- }
310
- catch { }
311
- }
312
-
313
- var id = Interlocked.Increment(ref s_nextHandle);
314
- s_handles[id] = value;
315
- return new InvokeResult(
316
- JsonSerializer.SerializeToElement(
317
- new { __handle = id, __type = t.FullName }, JsonOptions.Default),
318
- null);
75
+ if (ptr != null) Marshal.FreeHGlobal((IntPtr)ptr);
319
76
  }
320
-
321
- private static Exception Unwrap(Exception ex) =>
322
- ex is TargetInvocationException { InnerException: { } inner } ? Unwrap(inner) : ex;
323
-
324
- private static unsafe void WriteResponse(byte[] json, byte** responsePtr, int* responseLenPtr)
325
- {
326
- var ptr = (byte*)Marshal.AllocHGlobal(json.Length + 1);
327
- Marshal.Copy(json, 0, (IntPtr)ptr, json.Length);
328
- ptr[json.Length] = 0;
329
- *responsePtr = ptr;
330
- *responseLenPtr = json.Length;
331
- }
332
- }
333
-
334
- // ── JSON types ────────────────────────────────────────────────────────────────
335
-
336
- internal sealed record InvokeRequest(
337
- string? Assembly,
338
- string? TypeName,
339
- string? Method,
340
- int? Handle,
341
- List<JsonElement>? Args
342
- );
343
-
344
- internal sealed record InvokeResult(JsonElement? Result, string? Error);
345
-
346
- internal static class JsonOptions
347
- {
348
- internal static readonly JsonSerializerOptions Default = new()
349
- {
350
- PropertyNameCaseInsensitive = true,
351
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
352
- };
353
77
  }