@nativescript/windows 0.1.0-alpha.5 → 0.1.0-alpha.51

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.
@@ -1,353 +1,211 @@
1
1
  using System;
2
- using System.Collections;
3
2
  using System.Collections.Concurrent;
4
3
  using System.Collections.Generic;
4
+ using System.IO;
5
5
  using System.Linq;
6
6
  using System.Reflection;
7
7
  using System.Runtime.InteropServices;
8
+ using System.Runtime.Loader;
8
9
  using System.Text.Json;
9
- using System.Text.Json.Nodes;
10
- using System.Threading;
11
- using System.Threading.Tasks;
10
+
11
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeTests")]
12
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeBenchmarks")]
12
13
 
13
14
  namespace NativeScriptBridge;
14
15
 
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
16
+ public static partial class Bridge
40
17
  {
41
- private static readonly ConcurrentDictionary<int, object?> s_handles = new();
42
- private static int s_nextHandle;
43
-
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();
47
-
48
- // ── exported entry points ────────────────────────────────────────────────
49
-
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
- [UnmanagedCallersOnly(EntryPoint = "Invoke",
53
- CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
54
- public static unsafe int Invoke(
55
- byte* requestPtr, int requestLen,
56
- byte** responsePtr, int* responseLenPtr)
18
+ internal static readonly ConcurrentDictionary<int, object?> s_handles = new();
19
+ internal static int s_nextHandle;
20
+
21
+ // Function pointer registered by the Rust runtime so managed delegates can
22
+ // call back into V8 without a JSON round-trip.
23
+ internal static unsafe delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void>
24
+ s_jsInvoker;
25
+
26
+ private static readonly ConcurrentDictionary<string, Type?> s_typeCache
27
+ = new(StringComparer.Ordinal);
28
+ private static readonly ConcurrentDictionary<MethodKey, DispatchEntry> s_methodCache = new();
29
+ private static readonly ConcurrentDictionary<PropKey, PropertyInfo?> s_propCache = new();
30
+ private static readonly ConcurrentDictionary<CtorKey, CtorEntry> s_ctorCache = new();
31
+ // Cache of attempted assembly loads (simple name -> Assembly or null if not found).
32
+ private static readonly ConcurrentDictionary<string, Assembly?> s_assemblyLoadCache
33
+ = new(StringComparer.OrdinalIgnoreCase);
34
+ // Directories to search for assemblies (initialized once in static ctor).
35
+ private static readonly string[] s_assemblySearchDirs;
36
+
37
+ private static readonly JsonSerializerOptions s_coerceOpts = new()
57
38
  {
58
- byte[] responseBytes;
59
- try
60
- {
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);
65
- }
66
- catch (Exception ex)
67
- {
68
- var errResult = new InvokeResult(null, Unwrap(ex).Message);
69
- responseBytes = JsonSerializer.SerializeToUtf8Bytes(errResult, JsonOptions.Default);
70
- }
71
-
72
- WriteResponse(responseBytes, responsePtr, responseLenPtr);
73
- return 0;
74
- }
39
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
40
+ };
75
41
 
76
- /// Frees the response buffer allocated by Invoke.
77
- [UnmanagedCallersOnly(EntryPoint = "Free",
78
- CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
79
- public static unsafe void Free(byte* ptr)
42
+ internal static void ClearCaches()
80
43
  {
81
- if (ptr != null)
82
- Marshal.FreeHGlobal((IntPtr)ptr);
44
+ s_typeCache.Clear();
45
+ s_methodCache.Clear();
46
+ s_propCache.Clear();
47
+ s_ctorCache.Clear();
48
+ s_handles.Clear();
49
+ s_nextHandle = 0;
83
50
  }
84
51
 
85
- // ── dispatch ─────────────────────────────────────────────────────────────
86
-
87
- private static InvokeResult Dispatch(InvokeRequest req)
52
+ // Returns a JSON object mapping top-level namespace roots (e.g. "NativeScript")
53
+ // to the assembly simple-name that most likely contains that namespace's types.
54
+ // This probes the application base directory and libs/ for assemblies and
55
+ // inspects exported types. The result is safe-to-call and tolerant of
56
+ // non-.NET DLLs.
57
+ public static string GetNamespaceAssemblyMapJson()
88
58
  {
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__")
59
+ var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
60
+ try
97
61
  {
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
62
+ var baseDir = AppContext.BaseDirectory ?? AppDomain.CurrentDomain.BaseDirectory;
63
+ var dirs = new List<string> { baseDir };
64
+ var libs = Path.Combine(baseDir, "libs");
65
+ if (Directory.Exists(libs))
105
66
  {
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;
67
+ dirs.Add(libs);
68
+ try { dirs.AddRange(Directory.GetDirectories(libs, "*", SearchOption.AllDirectories)); } catch { }
69
+ }
154
70
 
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)
71
+ foreach (var dir in dirs.Where(d => !string.IsNullOrEmpty(d) && Directory.Exists(d)))
166
72
  {
167
- prop.SetValue(target, Coerce(req.Args[0], prop.PropertyType));
168
- return new InvokeResult(null, null);
73
+ try
74
+ {
75
+ foreach (var file in Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly))
76
+ {
77
+ try
78
+ {
79
+ var simple = Path.GetFileNameWithoutExtension(file);
80
+ if (string.IsNullOrEmpty(simple)) continue;
81
+
82
+ Assembly asm = AppDomain.CurrentDomain.GetAssemblies()
83
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, simple, StringComparison.OrdinalIgnoreCase));
84
+ if (asm is null)
85
+ {
86
+ try { asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file); }
87
+ catch { continue; }
88
+ }
89
+
90
+ Type[] types;
91
+ try { types = asm.GetExportedTypes(); }
92
+ catch { types = Array.Empty<Type>(); }
93
+
94
+ foreach (var t in types)
95
+ {
96
+ if (string.IsNullOrEmpty(t.Namespace)) continue;
97
+ var root = t.Namespace.Split('.')[0];
98
+ if (!map.ContainsKey(root)) map[root] = asm.GetName().Name;
99
+ }
100
+ }
101
+ catch { }
102
+ }
103
+ }
104
+ catch { }
169
105
  }
170
106
  }
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)));
107
+ catch { }
108
+ try { return JsonSerializer.Serialize(map); } catch { return "{}"; }
185
109
  }
186
110
 
187
- // ── helpers ───────────────────────────────────────────────────────────────
188
-
189
- private static object? AwaitIfTask(object? value)
111
+ // Static ctor: initialize assembly search directories and register resolver.
112
+ static Bridge()
190
113
  {
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<>))
114
+ try
203
115
  {
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);
116
+ var baseDir = AppContext.BaseDirectory ?? AppDomain.CurrentDomain.BaseDirectory;
117
+ var dirs = new List<string> { baseDir };
118
+ var libs = Path.Combine(baseDir, "libs");
119
+ if (Directory.Exists(libs))
120
+ {
121
+ dirs.Add(libs);
122
+ try { dirs.AddRange(Directory.GetDirectories(libs, "*", SearchOption.AllDirectories)); } catch { }
123
+ }
124
+ s_assemblySearchDirs = dirs.Where(d => !string.IsNullOrEmpty(d)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
125
+ AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
209
126
  }
210
-
211
- if (value is System.Threading.Tasks.ValueTask vt)
127
+ catch
212
128
  {
213
- vt.AsTask().Wait();
214
- return null;
129
+ s_assemblySearchDirs = Array.Empty<string>();
215
130
  }
216
-
217
- return value;
218
131
  }
219
132
 
220
- private static Type? ResolveType(string? assemblyName, string? typeName)
133
+ private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
221
134
  {
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
- }
135
+ try
136
+ {
137
+ var requested = new AssemblyName(args.Name).Name;
138
+ if (string.IsNullOrEmpty(requested)) return null;
226
139
 
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;
140
+ if (s_assemblyLoadCache.TryGetValue(requested, out var cached)) return cached;
232
141
 
233
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
234
- {
235
- t = asm.GetType(typeName!);
236
- if (t is not null) return t;
237
- }
142
+ // Check already-loaded assemblies first (avoid recursive Assembly.Load).
143
+ var loadedAsm = AppDomain.CurrentDomain.GetAssemblies()
144
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, requested, StringComparison.OrdinalIgnoreCase));
145
+ if (loadedAsm is not null)
146
+ {
147
+ s_assemblyLoadCache[requested] = loadedAsm;
148
+ return loadedAsm;
149
+ }
238
150
 
239
- if (!string.IsNullOrEmpty(assemblyName))
240
- {
241
- try
151
+ // Try to locate a matching dll in known search directories.
152
+ foreach (var dir in s_assemblySearchDirs)
242
153
  {
243
- var asm = Assembly.Load(assemblyName);
244
- t = asm.GetType(typeName!);
245
- if (t is not null) return t;
154
+ try
155
+ {
156
+ var candidate = Path.Combine(dir, requested + ".dll");
157
+ if (File.Exists(candidate))
158
+ {
159
+ try
160
+ {
161
+ var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(candidate);
162
+ s_assemblyLoadCache[requested] = asm;
163
+ return asm;
164
+ }
165
+ catch { }
166
+ }
167
+ }
168
+ catch { }
246
169
  }
247
- catch { }
248
- }
249
170
 
171
+ s_assemblyLoadCache[requested] = null;
172
+ }
173
+ catch { }
250
174
  return null;
251
175
  }
252
176
 
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)
177
+ [UnmanagedCallersOnly(EntryPoint = "RegisterJsCallback",
178
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
179
+ public static unsafe int RegisterJsCallback(
180
+ delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void> callback)
275
181
  {
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);
182
+ s_jsInvoker = callback;
183
+ return 0;
283
184
  }
284
185
 
285
- private static InvokeResult Box(object? value)
186
+ [UnmanagedCallersOnly(EntryPoint = "Invoke",
187
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
188
+ public static unsafe int Invoke(
189
+ byte* requestPtr, int requestLen,
190
+ byte** responsePtr, int* responseLenPtr)
286
191
  {
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))
192
+ try
294
193
  {
295
- return new InvokeResult(
296
- JsonSerializer.SerializeToElement(value, t, JsonOptions.Default), null);
194
+ var span = new ReadOnlySpan<byte>(requestPtr, requestLen);
195
+ var req = JsonSerializer.Deserialize(span, BridgeJsonContext.Default.InvokeRequest)!;
196
+ WriteResult(Dispatch(req), responsePtr, responseLenPtr);
297
197
  }
298
-
299
- // Arrays and IEnumerable — serialise as JSON arrays.
300
- if (t.IsArray || (t != typeof(string) && value is IEnumerable enumerable))
198
+ catch (Exception ex)
301
199
  {
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 { }
200
+ WriteError(Unwrap(ex).Message, responsePtr, responseLenPtr);
311
201
  }
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);
202
+ return 0;
319
203
  }
320
204
 
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)
205
+ [UnmanagedCallersOnly(EntryPoint = "Free",
206
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
207
+ public static unsafe void Free(byte* ptr)
325
208
  {
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;
209
+ if (ptr != null) Marshal.FreeHGlobal((IntPtr)ptr);
331
210
  }
332
211
  }
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
- }