@nativescript/windows 0.1.0-alpha.7 → 0.1.0-alpha.71

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.
Files changed (26) hide show
  1. package/build.ps1 +44 -2
  2. package/framework/__PROJECT_NAME__/App.xaml.cs +67 -22
  3. package/framework/__PROJECT_NAME__/Assets/SplashScreen.png +0 -0
  4. package/framework/__PROJECT_NAME__/Assets/Square150x150Logo.png +0 -0
  5. package/framework/__PROJECT_NAME__/Assets/Square44x44Logo.png +0 -0
  6. package/framework/__PROJECT_NAME__/Assets/StoreLogo.png +0 -0
  7. package/framework/__PROJECT_NAME__/Assets/Wide310x150Logo.png +0 -0
  8. package/framework/__PROJECT_NAME__/CrashDiagnostics.cs +262 -66
  9. package/framework/__PROJECT_NAME__/Package.appxmanifest +2 -5
  10. package/framework/__PROJECT_NAME__/RuntimeHost.cs +132 -22
  11. package/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj +49 -12
  12. package/framework/dotnet-bridge/BinaryProtocol.cs +145 -0
  13. package/framework/dotnet-bridge/Bridge.BinaryDispatch.cs +203 -0
  14. package/framework/dotnet-bridge/Bridge.Dispatch.cs +433 -0
  15. package/framework/dotnet-bridge/Bridge.JsDelegate.cs +96 -0
  16. package/framework/dotnet-bridge/Bridge.TaskHelper.cs +207 -0
  17. package/framework/dotnet-bridge/Bridge.cs +287 -286
  18. package/framework/dotnet-bridge/DispatchTypes.cs +304 -0
  19. package/framework/libs/arm64/nativescript.dll +0 -0
  20. package/framework/libs/devtools/arm64/nativescript.dll +0 -0
  21. package/framework/libs/devtools/x64/nativescript.dll +0 -0
  22. package/framework/libs/x64/nativescript.dll +0 -0
  23. package/framework/tools/dotnet-tool-arm64.exe +0 -0
  24. package/framework/tools/dotnet-tool-x64.exe +0 -0
  25. package/framework/tools/dotnet-tool.exe +0 -0
  26. package/package.json +19 -19
@@ -1,353 +1,354 @@
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
- using System.Runtime.InteropServices;
8
- using System.Text.Json;
9
- using System.Text.Json.Nodes;
10
7
  using System.Threading;
11
8
  using System.Threading.Tasks;
9
+ using System.Runtime.InteropServices;
10
+ using System.Runtime.Loader;
11
+ using System.Text.Json;
12
+
13
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeTests")]
14
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeBenchmarks")]
12
15
 
13
16
  namespace NativeScriptBridge;
14
17
 
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
18
+ public static partial class Bridge
40
19
  {
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();
20
+ internal static readonly ConcurrentDictionary<int, object?> s_handles = new();
21
+ internal static int s_nextHandle;
22
+
23
+ // Function pointer registered by the Rust runtime so managed delegates can
24
+ // call back into V8 without a JSON round-trip.
25
+ internal static unsafe delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void>
26
+ s_jsInvoker;
27
+
28
+ private static readonly ConcurrentDictionary<string, Type?> s_typeCache
29
+ = new(StringComparer.Ordinal);
30
+ private static readonly ConcurrentDictionary<MethodKey, DispatchEntry> s_methodCache = new();
31
+ private static readonly ConcurrentDictionary<PropKey, PropertyInfo?> s_propCache = new();
32
+ private static readonly ConcurrentDictionary<CtorKey, CtorEntry> s_ctorCache = new();
33
+ // Cache of attempted assembly loads (simple name -> Assembly or null if not found).
34
+ private static readonly ConcurrentDictionary<string, Assembly?> s_assemblyLoadCache
35
+ = new(StringComparer.OrdinalIgnoreCase);
36
+ // Directories to search for assemblies (initialized once in static ctor).
37
+ private static readonly string[] s_assemblySearchDirs;
38
+
39
+ private static readonly JsonSerializerOptions s_coerceOpts = new()
40
+ {
41
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
42
+ };
47
43
 
48
- // ── exported entry points ────────────────────────────────────────────────
44
+ internal static void ClearCaches()
45
+ {
46
+ s_typeCache.Clear();
47
+ s_methodCache.Clear();
48
+ s_propCache.Clear();
49
+ s_ctorCache.Clear();
50
+ s_handles.Clear();
51
+ s_nextHandle = 0;
52
+ }
49
53
 
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)
54
+ // Returns a JSON object mapping top-level namespace roots (e.g. "NativeScript")
55
+ // to the assembly simple-name that most likely contains that namespace's types.
56
+ // Reuses s_assemblySearchDirs so plugin and NuGet assemblies are included.
57
+ public static string GetNamespaceAssemblyMapJson()
57
58
  {
58
- byte[] responseBytes;
59
+ var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
59
60
  try
60
61
  {
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);
62
+ foreach (var dir in s_assemblySearchDirs.Where(Directory.Exists))
63
+ {
64
+ try
65
+ {
66
+ foreach (var file in Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly))
67
+ {
68
+ try
69
+ {
70
+ var simple = Path.GetFileNameWithoutExtension(file);
71
+ if (string.IsNullOrEmpty(simple)) continue;
72
+
73
+ Assembly asm = AppDomain.CurrentDomain.GetAssemblies()
74
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, simple, StringComparison.OrdinalIgnoreCase));
75
+ if (asm is null)
76
+ {
77
+ try { asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file); }
78
+ catch { continue; }
79
+ }
80
+
81
+ Type[] types;
82
+ try { types = asm.GetExportedTypes(); }
83
+ catch { types = Array.Empty<Type>(); }
84
+
85
+ foreach (var t in types)
86
+ {
87
+ if (string.IsNullOrEmpty(t.Namespace)) continue;
88
+ var root = t.Namespace.Split('.')[0];
89
+ if (!map.ContainsKey(root)) map[root] = asm.GetName().Name;
90
+ }
91
+ }
92
+ catch { }
93
+ }
94
+ }
95
+ catch { }
96
+ }
70
97
  }
71
-
72
- WriteResponse(responseBytes, responsePtr, responseLenPtr);
73
- return 0;
98
+ catch { }
99
+ try { return JsonSerializer.Serialize(map); } catch { return "{}"; }
74
100
  }
75
101
 
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)
102
+ static Bridge()
80
103
  {
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)
104
+ try
90
105
  {
91
- s_handles.TryRemove(req.Handle.Value, out _);
92
- return new InvokeResult(null, null);
93
- }
106
+ var baseDir = AppContext.BaseDirectory ?? AppDomain.CurrentDomain.BaseDirectory;
107
+ var dirs = new List<string> { baseDir };
94
108
 
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
109
+ // libs/ subtree relative to the bridge's own directory.
110
+ var libs = Path.Combine(baseDir, "libs");
111
+ if (Directory.Exists(libs))
105
112
  {
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
+ dirs.Add(libs);
114
+ try { dirs.AddRange(Directory.GetDirectories(libs, "*", SearchOption.AllDirectories)); } catch { }
115
+ }
113
116
 
114
- object? target = null;
115
- Type? type = null;
117
+ // The bridge DLL lives at dotnet-bridge/publish/ inside the app output root.
118
+ // Plugin assemblies (added via plugin.props) land at plugins/**/*.dll and NuGet
119
+ // assemblies land at the app output root itself — both outside the bridge subtree.
120
+ // Use the host process directory to reach them.
121
+ try
122
+ {
123
+ var processDir = Path.GetDirectoryName(
124
+ System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName);
125
+ if (!string.IsNullOrEmpty(processDir))
126
+ {
127
+ dirs.Add(processDir);
128
+
129
+ // plugins/ subtree: CLI-managed plugin DLLs live here.
130
+ // Check both {processDir}/plugins and {processDir}/../plugins because
131
+ // the NS CLI places plugins/ as a sibling of bin/, not inside it.
132
+ var pluginsDir = Path.Combine(processDir, "plugins");
133
+ if (Directory.Exists(pluginsDir))
134
+ try { dirs.AddRange(Directory.GetDirectories(pluginsDir, "*", SearchOption.AllDirectories)); } catch { }
135
+
136
+ var parentDir = Path.GetDirectoryName(processDir);
137
+ if (!string.IsNullOrEmpty(parentDir))
138
+ {
139
+ var parentPlugins = Path.Combine(parentDir, "plugins");
140
+ if (Directory.Exists(parentPlugins))
141
+ {
142
+ dirs.Add(parentPlugins);
143
+ try { dirs.AddRange(Directory.GetDirectories(parentPlugins, "*", SearchOption.AllDirectories)); } catch { }
144
+ }
145
+ }
146
+
147
+ // libs/ relative to the app root (alternative convention).
148
+ var processLibs = Path.Combine(processDir, "libs");
149
+ if (Directory.Exists(processLibs))
150
+ try { dirs.AddRange(Directory.GetDirectories(processLibs, "*", SearchOption.AllDirectories)); } catch { }
151
+ }
152
+ }
153
+ catch { }
116
154
 
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}");
155
+ s_assemblySearchDirs = dirs.Where(d => !string.IsNullOrEmpty(d)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
156
+ AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
128
157
  }
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")
158
+ catch
137
159
  {
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));
160
+ s_assemblySearchDirs = Array.Empty<string>();
149
161
  }
162
+ }
150
163
 
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)
164
+ public static object? RunOnUIThread(int callbackId)
165
+ {
166
+ var action = new Action(() =>
163
167
  {
164
- var prop = GetCachedProperty(type, method[4..], flags);
165
- if (prop is not null)
168
+ unsafe
166
169
  {
167
- prop.SetValue(target, Coerce(req.Args[0], prop.PropertyType));
168
- return new InvokeResult(null, null);
170
+ if (s_jsInvoker == null) return;
171
+ byte* respPtr = null;
172
+ int respLen = 0;
173
+ s_jsInvoker(callbackId, null, 0, &respPtr, &respLen);
174
+ if (respPtr != null && respLen > 0) Marshal.FreeHGlobal((IntPtr)respPtr);
169
175
  }
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
- }
176
+ });
186
177
 
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)
178
+ try
195
179
  {
196
- task.Wait();
197
- if (task.IsFaulted)
198
- throw task.Exception!.InnerException ?? task.Exception;
199
- return t.GetProperty("Result")?.GetValue(value);
180
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
181
+ {
182
+ var coreAppType = asm.GetType("Windows.ApplicationModel.Core.CoreApplication");
183
+ if (coreAppType == null) continue;
184
+ var mainViewProp = coreAppType.GetProperty("MainView", BindingFlags.Public | BindingFlags.Static);
185
+ var mainView = mainViewProp?.GetValue(null);
186
+ if (mainView == null) continue;
187
+ var dispatcherProp = mainView.GetType().GetProperty("Dispatcher", BindingFlags.Public | BindingFlags.Instance);
188
+ var dispatcher = dispatcherProp?.GetValue(mainView);
189
+ if (dispatcher == null) continue;
190
+
191
+ var hasAccessProp = dispatcher.GetType().GetProperty("HasThreadAccess", BindingFlags.Public | BindingFlags.Instance);
192
+ if (hasAccessProp?.GetValue(dispatcher) is true)
193
+ {
194
+ action();
195
+ return null;
196
+ }
197
+
198
+ foreach (var m in dispatcher.GetType().GetMethods().Where(m => m.Name == "RunAsync"))
199
+ {
200
+ var parameters = m.GetParameters();
201
+ if (parameters.Length != 2) continue;
202
+ var enumType = parameters[0].ParameterType;
203
+ object priority = enumType.IsEnum ? Enum.ToObject(enumType, 0) : Activator.CreateInstance(enumType)!;
204
+ var handlerType = parameters[1].ParameterType;
205
+ var mre = new ManualResetEventSlim(false);
206
+ var wrapped = new Action(() => { try { action(); } finally { mre.Set(); } });
207
+ try
208
+ {
209
+ var d = Delegate.CreateDelegate(handlerType, wrapped.Target, wrapped.Method);
210
+ m.Invoke(dispatcher, new object[] { priority, d });
211
+ mre.Wait();
212
+ return null;
213
+ }
214
+ catch { }
215
+ }
216
+ break;
217
+ }
200
218
  }
219
+ catch { }
201
220
 
202
- if (t.IsGenericType && t.GetGenericTypeDefinition() == typeof(ValueTask<>))
221
+ try
203
222
  {
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);
223
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
224
+ {
225
+ var dqType = asm.GetType("Microsoft.UI.Dispatching.DispatcherQueue");
226
+ if (dqType == null) continue;
227
+ var getForCurrent = dqType.GetMethod("GetForCurrentThread", BindingFlags.Public | BindingFlags.Static);
228
+ var dq = getForCurrent?.Invoke(null, null);
229
+ if (dq == null) continue;
230
+
231
+ if (dq.GetType().GetProperty("HasThreadAccess")?.GetValue(dq) is true)
232
+ {
233
+ action();
234
+ return null;
235
+ }
236
+ break;
237
+ }
209
238
  }
239
+ catch { }
210
240
 
211
- if (value is System.Threading.Tasks.ValueTask vt)
241
+ try
212
242
  {
213
- vt.AsTask().Wait();
214
- return null;
243
+ var wpfDispatcherType = AppDomain.CurrentDomain.GetAssemblies()
244
+ .Select(a => a.GetType("System.Windows.Threading.Dispatcher"))
245
+ .FirstOrDefault(t => t != null);
246
+ if (wpfDispatcherType != null)
247
+ {
248
+ var currentDispatcherProp = wpfDispatcherType.GetProperty("CurrentDispatcher", BindingFlags.Public | BindingFlags.Static);
249
+ var dispatcher = currentDispatcherProp?.GetValue(null);
250
+ if (dispatcher != null)
251
+ {
252
+ var checkAccess = dispatcher.GetType().GetMethod("CheckAccess");
253
+ if (checkAccess?.Invoke(dispatcher, null) is true)
254
+ {
255
+ action();
256
+ return null;
257
+ }
258
+ var beginInvoke = dispatcher.GetType().GetMethod("BeginInvoke", new[] { typeof(Action) })
259
+ ?? dispatcher.GetType().GetMethods().FirstOrDefault(m => m.Name == "BeginInvoke" && m.GetParameters().Length == 1);
260
+ if (beginInvoke != null)
261
+ {
262
+ var mre = new ManualResetEventSlim(false);
263
+ beginInvoke.Invoke(dispatcher, new object[] { new Action(() => { try { action(); } finally { mre.Set(); } }) });
264
+ mre.Wait();
265
+ return null;
266
+ }
267
+ }
268
+ }
215
269
  }
270
+ catch { }
216
271
 
217
- return value;
272
+ action();
273
+ return null;
218
274
  }
219
275
 
220
- private static Type? ResolveType(string? assemblyName, string? typeName)
276
+ private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
221
277
  {
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
- }
278
+ try
279
+ {
280
+ var requested = new AssemblyName(args.Name).Name;
281
+ if (string.IsNullOrEmpty(requested)) return null;
226
282
 
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;
283
+ if (s_assemblyLoadCache.TryGetValue(requested, out var cached)) return cached;
232
284
 
233
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
234
- {
235
- t = asm.GetType(typeName!);
236
- if (t is not null) return t;
237
- }
285
+ // Check already-loaded assemblies first (avoid recursive Assembly.Load).
286
+ var loadedAsm = AppDomain.CurrentDomain.GetAssemblies()
287
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, requested, StringComparison.OrdinalIgnoreCase));
288
+ if (loadedAsm is not null)
289
+ {
290
+ s_assemblyLoadCache[requested] = loadedAsm;
291
+ return loadedAsm;
292
+ }
238
293
 
239
- if (!string.IsNullOrEmpty(assemblyName))
240
- {
241
- try
294
+ // Try to locate a matching dll in known search directories.
295
+ foreach (var dir in s_assemblySearchDirs)
242
296
  {
243
- var asm = Assembly.Load(assemblyName);
244
- t = asm.GetType(typeName!);
245
- if (t is not null) return t;
297
+ try
298
+ {
299
+ var candidate = Path.Combine(dir, requested + ".dll");
300
+ if (File.Exists(candidate))
301
+ {
302
+ try
303
+ {
304
+ var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(candidate);
305
+ s_assemblyLoadCache[requested] = asm;
306
+ return asm;
307
+ }
308
+ catch { }
309
+ }
310
+ }
311
+ catch { }
246
312
  }
247
- catch { }
248
- }
249
313
 
314
+ s_assemblyLoadCache[requested] = null;
315
+ }
316
+ catch { }
250
317
  return null;
251
318
  }
252
319
 
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)
320
+ [UnmanagedCallersOnly(EntryPoint = "RegisterJsCallback",
321
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
322
+ public static unsafe int RegisterJsCallback(
323
+ delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void> callback)
275
324
  {
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);
325
+ s_jsInvoker = callback;
326
+ return 0;
283
327
  }
284
328
 
285
- private static InvokeResult Box(object? value)
329
+ [UnmanagedCallersOnly(EntryPoint = "Invoke",
330
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
331
+ public static unsafe int Invoke(
332
+ byte* requestPtr, int requestLen,
333
+ byte** responsePtr, int* responseLenPtr)
286
334
  {
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))
335
+ try
294
336
  {
295
- return new InvokeResult(
296
- JsonSerializer.SerializeToElement(value, t, JsonOptions.Default), null);
337
+ var span = new ReadOnlySpan<byte>(requestPtr, requestLen);
338
+ var req = JsonSerializer.Deserialize(span, BridgeJsonContext.Default.InvokeRequest)!;
339
+ WriteResult(Dispatch(req), responsePtr, responseLenPtr);
297
340
  }
298
-
299
- // Arrays and IEnumerable — serialise as JSON arrays.
300
- if (t.IsArray || (t != typeof(string) && value is IEnumerable enumerable))
341
+ catch (Exception ex)
301
342
  {
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 { }
343
+ WriteError(Unwrap(ex).Message, responsePtr, responseLenPtr);
311
344
  }
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);
345
+ return 0;
319
346
  }
320
347
 
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)
348
+ [UnmanagedCallersOnly(EntryPoint = "Free",
349
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
350
+ public static unsafe void Free(byte* ptr)
325
351
  {
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;
352
+ if (ptr != null) Marshal.FreeHGlobal((IntPtr)ptr);
331
353
  }
332
354
  }
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
- }