@nativescript/windows 0.1.0-alpha.8 → 0.1.0-alpha.81

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 (29) 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__/Directory.Build.props +15 -0
  10. package/framework/__PROJECT_NAME__/Directory.Build.targets +153 -0
  11. package/framework/__PROJECT_NAME__/Package.appxmanifest +2 -5
  12. package/framework/__PROJECT_NAME__/RuntimeHost.cs +97 -10
  13. package/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj +90 -17
  14. package/framework/dotnet-bridge/BinaryProtocol.cs +145 -0
  15. package/framework/dotnet-bridge/Bridge.BinaryDispatch.cs +216 -0
  16. package/framework/dotnet-bridge/Bridge.Dispatch.cs +468 -0
  17. package/framework/dotnet-bridge/Bridge.JsDelegate.cs +96 -0
  18. package/framework/dotnet-bridge/Bridge.TaskHelper.cs +207 -0
  19. package/framework/dotnet-bridge/Bridge.cs +402 -263
  20. package/framework/dotnet-bridge/DispatchTypes.cs +317 -0
  21. package/framework/dotnet-bridge/DotNetBridge.csproj +7 -1
  22. package/framework/libs/arm64/nativescript.dll +0 -0
  23. package/framework/libs/devtools/arm64/nativescript.dll +0 -0
  24. package/framework/libs/devtools/x64/nativescript.dll +0 -0
  25. package/framework/libs/x64/nativescript.dll +0 -0
  26. package/framework/tools/dotnet-tool-arm64.exe +0 -0
  27. package/framework/tools/dotnet-tool-x64.exe +0 -0
  28. package/framework/tools/dotnet-tool.exe +0 -0
  29. package/package.json +19 -19
@@ -1,353 +1,492 @@
1
1
  using System;
2
- using System.Collections;
2
+ using System.Diagnostics;
3
3
  using System.Collections.Concurrent;
4
4
  using System.Collections.Generic;
5
+ using System.IO;
5
6
  using System.Linq;
6
7
  using System.Reflection;
7
- using System.Runtime.InteropServices;
8
- using System.Text.Json;
9
- using System.Text.Json.Nodes;
10
8
  using System.Threading;
11
9
  using System.Threading.Tasks;
10
+ using System.Runtime.InteropServices;
11
+ using System.Runtime.Loader;
12
+ using System.Text.Json;
13
+
14
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeTests")]
15
+ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("DotNetBridgeBenchmarks")]
12
16
 
13
17
  namespace NativeScriptBridge;
14
18
 
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
19
+ public static partial class Bridge
40
20
  {
41
- private static readonly ConcurrentDictionary<int, object?> s_handles = new();
42
- private static int s_nextHandle;
21
+ internal static readonly ConcurrentDictionary<int, object?> s_handles = new();
22
+ internal static int s_nextHandle;
23
+
24
+ // Function pointer registered by the Rust runtime so managed delegates can
25
+ // call back into V8 without a JSON round-trip.
26
+ internal static unsafe delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void>
27
+ s_jsInvoker;
28
+
29
+ private static readonly ConcurrentDictionary<string, Type?> s_typeCache
30
+ = new(StringComparer.Ordinal);
31
+ private static readonly ConcurrentDictionary<MethodKey, DispatchEntry> s_methodCache = new();
32
+ private static readonly ConcurrentDictionary<PropKey, PropertyInfo?> s_propCache = new();
33
+ private static readonly ConcurrentDictionary<CtorKey, CtorEntry> s_ctorCache = new();
34
+ // Cache of attempted assembly loads (simple name -> Assembly or null if not found).
35
+ private static readonly ConcurrentDictionary<string, Assembly?> s_assemblyLoadCache
36
+ = new(StringComparer.OrdinalIgnoreCase);
37
+ // Directories to search for assemblies (initialized once in static ctor).
38
+ private static readonly string[] s_assemblySearchDirs;
39
+
40
+ private static readonly JsonSerializerOptions s_coerceOpts = new()
41
+ {
42
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
43
+ };
43
44
 
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();
45
+ internal static void ClearCaches()
46
+ {
47
+ s_typeCache.Clear();
48
+ s_methodCache.Clear();
49
+ s_propCache.Clear();
50
+ s_ctorCache.Clear();
51
+ s_handles.Clear();
52
+ s_nativePtrs.Clear();
53
+ s_nextHandle = 0;
54
+ }
47
55
 
48
- // ── exported entry points ────────────────────────────────────────────────
56
+ // Optional mapping from exported handle id -> native IUnknown pointer (Int64)
57
+ // Populated when a managed object can yield a native COM pointer via
58
+ // Marshal.GetIUnknownForObject. Cleared and released on __release.
59
+ internal static readonly ConcurrentDictionary<int, long> s_nativePtrs = new();
49
60
 
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)
61
+ // Returns a JSON object mapping top-level namespace roots (e.g. "NativeScript")
62
+ // to the assembly simple-name that most likely contains that namespace's types.
63
+ // Reuses s_assemblySearchDirs so plugin and NuGet assemblies are included.
64
+ public static string GetNamespaceAssemblyMapJson()
57
65
  {
58
- byte[] responseBytes;
66
+ var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
59
67
  try
60
68
  {
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);
69
+ foreach (var dir in s_assemblySearchDirs.Where(Directory.Exists))
70
+ {
71
+ try
72
+ {
73
+ foreach (var file in Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly))
74
+ {
75
+ try
76
+ {
77
+ var simple = Path.GetFileNameWithoutExtension(file);
78
+ if (string.IsNullOrEmpty(simple)) continue;
79
+
80
+ Assembly asm = AppDomain.CurrentDomain.GetAssemblies()
81
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, simple, StringComparison.OrdinalIgnoreCase));
82
+ if (asm is null)
83
+ {
84
+ try { asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file); }
85
+ catch { continue; }
86
+ }
87
+
88
+ Type[] types;
89
+ try { types = asm.GetExportedTypes(); }
90
+ catch { types = Array.Empty<Type>(); }
91
+
92
+ foreach (var t in types)
93
+ {
94
+ if (string.IsNullOrEmpty(t.Namespace)) continue;
95
+ var root = t.Namespace.Split('.')[0];
96
+ if (!map.ContainsKey(root)) map[root] = asm.GetName().Name;
97
+ }
98
+ }
99
+ catch { }
100
+ }
101
+ }
102
+ catch { }
103
+ }
70
104
  }
71
-
72
- WriteResponse(responseBytes, responsePtr, responseLenPtr);
73
- return 0;
105
+ catch { }
106
+ try { return JsonSerializer.Serialize(map); } catch { return "{}"; }
74
107
  }
75
108
 
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)
109
+ static Bridge()
80
110
  {
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)
111
+ try
90
112
  {
91
- s_handles.TryRemove(req.Handle.Value, out _);
92
- return new InvokeResult(null, null);
93
- }
113
+ var baseDir = AppContext.BaseDirectory ?? AppDomain.CurrentDomain.BaseDirectory;
114
+ var dirs = new List<string> { baseDir };
94
115
 
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
116
+ // libs/ subtree relative to the bridge's own directory.
117
+ var libs = Path.Combine(baseDir, "libs");
118
+ if (Directory.Exists(libs))
105
119
  {
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
- }
120
+ dirs.Add(libs);
121
+ try { dirs.AddRange(Directory.GetDirectories(libs, "*", SearchOption.AllDirectories)); } catch { }
122
+ }
113
123
 
114
- object? target = null;
115
- Type? type = null;
124
+ // The bridge DLL lives at dotnet-bridge/publish/ inside the app output root.
125
+ // Plugin assemblies (added via plugin.props) land at plugins/**/*.dll and NuGet
126
+ // assemblies land at the app output root itself — both outside the bridge subtree.
127
+ // Use the host process directory to reach them.
128
+ try
129
+ {
130
+ var processDir = Path.GetDirectoryName(
131
+ System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName);
132
+ if (!string.IsNullOrEmpty(processDir))
133
+ {
134
+ dirs.Add(processDir);
135
+
136
+ // plugins/ subtree: CLI-managed plugin DLLs live here.
137
+ // Check both {processDir}/plugins and {processDir}/../plugins because
138
+ // the NS CLI places plugins/ as a sibling of bin/, not inside it.
139
+ var pluginsDir = Path.Combine(processDir, "plugins");
140
+ if (Directory.Exists(pluginsDir))
141
+ try { dirs.AddRange(Directory.GetDirectories(pluginsDir, "*", SearchOption.AllDirectories)); } catch { }
142
+
143
+ var parentDir = Path.GetDirectoryName(processDir);
144
+ if (!string.IsNullOrEmpty(parentDir))
145
+ {
146
+ var parentPlugins = Path.Combine(parentDir, "plugins");
147
+ if (Directory.Exists(parentPlugins))
148
+ {
149
+ dirs.Add(parentPlugins);
150
+ try { dirs.AddRange(Directory.GetDirectories(parentPlugins, "*", SearchOption.AllDirectories)); } catch { }
151
+ }
152
+ }
153
+
154
+ // libs/ relative to the app root (alternative convention).
155
+ var processLibs = Path.Combine(processDir, "libs");
156
+ if (Directory.Exists(processLibs))
157
+ try { dirs.AddRange(Directory.GetDirectories(processLibs, "*", SearchOption.AllDirectories)); } catch { }
158
+ }
159
+ }
160
+ catch { }
116
161
 
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();
162
+ s_assemblySearchDirs = dirs.Where(d => !string.IsNullOrEmpty(d)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
163
+ AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve;
122
164
  }
123
- else
165
+ catch
124
166
  {
125
- type = ResolveType(req.Assembly, req.TypeName);
126
- if (type is null)
127
- return new InvokeResult(null, $"Type not found: {req.TypeName}");
167
+ s_assemblySearchDirs = Array.Empty<string>();
128
168
  }
169
+ }
129
170
 
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");
171
+ public static object? RunOnUIThread(int callbackId)
172
+ {
173
+ var action = new Action(() =>
174
+ {
175
+ unsafe
176
+ {
177
+ if (s_jsInvoker == null) return;
178
+ byte* respPtr = null;
179
+ int respLen = 0;
180
+ s_jsInvoker(callbackId, null, 0, &respPtr, &respLen);
181
+ if (respPtr != null && respLen > 0) Marshal.FreeHGlobal((IntPtr)respPtr);
182
+ }
183
+ });
134
184
 
135
- // ── Constructor ──────────────────────────────────────────────────────
136
- if (method == ".ctor")
185
+ try
137
186
  {
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));
187
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
188
+ {
189
+ var coreAppType = asm.GetType("Windows.ApplicationModel.Core.CoreApplication");
190
+ if (coreAppType == null) continue;
191
+ var mainViewProp = coreAppType.GetProperty("MainView", BindingFlags.Public | BindingFlags.Static);
192
+ var mainView = mainViewProp?.GetValue(null);
193
+ if (mainView == null) continue;
194
+ var dispatcherProp = mainView.GetType().GetProperty("Dispatcher", BindingFlags.Public | BindingFlags.Instance);
195
+ var dispatcher = dispatcherProp?.GetValue(mainView);
196
+ if (dispatcher == null) continue;
197
+
198
+ var hasAccessProp = dispatcher.GetType().GetProperty("HasThreadAccess", BindingFlags.Public | BindingFlags.Instance);
199
+ if (hasAccessProp?.GetValue(dispatcher) is true)
200
+ {
201
+ action();
202
+ return null;
203
+ }
204
+
205
+ foreach (var m in dispatcher.GetType().GetMethods().Where(m => m.Name == "RunAsync"))
206
+ {
207
+ var parameters = m.GetParameters();
208
+ if (parameters.Length != 2) continue;
209
+ var enumType = parameters[0].ParameterType;
210
+ object priority = enumType.IsEnum ? Enum.ToObject(enumType, 0) : Activator.CreateInstance(enumType)!;
211
+ var handlerType = parameters[1].ParameterType;
212
+ var mre = new ManualResetEventSlim(false);
213
+ var wrapped = new Action(() => { try { action(); } finally { mre.Set(); } });
214
+ try
215
+ {
216
+ var d = Delegate.CreateDelegate(handlerType, wrapped.Target, wrapped.Method);
217
+ m.Invoke(dispatcher, new object[] { priority, d });
218
+ mre.Wait();
219
+ return null;
220
+ }
221
+ catch { }
222
+ }
223
+ break;
224
+ }
149
225
  }
226
+ catch { }
150
227
 
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))
228
+ try
157
229
  {
158
- var prop = GetCachedProperty(type, method[4..], flags);
159
- if (prop is not null)
160
- return Box(prop.GetValue(target));
230
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
231
+ {
232
+ var dqType = asm.GetType("Microsoft.UI.Dispatching.DispatcherQueue");
233
+ if (dqType == null) continue;
234
+ var getForCurrent = dqType.GetMethod("GetForCurrentThread", BindingFlags.Public | BindingFlags.Static);
235
+ var dq = getForCurrent?.Invoke(null, null);
236
+ if (dq == null) continue;
237
+
238
+ if (dq.GetType().GetProperty("HasThreadAccess")?.GetValue(dq) is true)
239
+ {
240
+ action();
241
+ return null;
242
+ }
243
+ break;
244
+ }
161
245
  }
162
- if (method.StartsWith("set_", StringComparison.Ordinal) && req.Args?.Count == 1)
246
+ catch { }
247
+
248
+ try
163
249
  {
164
- var prop = GetCachedProperty(type, method[4..], flags);
165
- if (prop is not null)
250
+ var wpfDispatcherType = AppDomain.CurrentDomain.GetAssemblies()
251
+ .Select(a => a.GetType("System.Windows.Threading.Dispatcher"))
252
+ .FirstOrDefault(t => t != null);
253
+ if (wpfDispatcherType != null)
166
254
  {
167
- prop.SetValue(target, Coerce(req.Args[0], prop.PropertyType));
168
- return new InvokeResult(null, null);
255
+ var currentDispatcherProp = wpfDispatcherType.GetProperty("CurrentDispatcher", BindingFlags.Public | BindingFlags.Static);
256
+ var dispatcher = currentDispatcherProp?.GetValue(null);
257
+ if (dispatcher != null)
258
+ {
259
+ var checkAccess = dispatcher.GetType().GetMethod("CheckAccess");
260
+ if (checkAccess?.Invoke(dispatcher, null) is true)
261
+ {
262
+ action();
263
+ return null;
264
+ }
265
+ var beginInvoke = dispatcher.GetType().GetMethod("BeginInvoke", new[] { typeof(Action) })
266
+ ?? dispatcher.GetType().GetMethods().FirstOrDefault(m => m.Name == "BeginInvoke" && m.GetParameters().Length == 1);
267
+ if (beginInvoke != null)
268
+ {
269
+ var mre = new ManualResetEventSlim(false);
270
+ beginInvoke.Invoke(dispatcher, new object[] { new Action(() => { try { action(); } finally { mre.Set(); } }) });
271
+ mre.Wait();
272
+ return null;
273
+ }
274
+ }
169
275
  }
170
276
  }
277
+ catch { }
171
278
 
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)));
279
+ action();
280
+ return null;
185
281
  }
186
282
 
187
- // ── helpers ───────────────────────────────────────────────────────────────
188
-
189
- private static object? AwaitIfTask(object? value)
283
+ private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args)
190
284
  {
191
- if (value is null) return null;
192
- var t = value.GetType();
193
-
194
- if (value is Task task)
285
+ try
195
286
  {
196
- task.Wait();
197
- if (task.IsFaulted)
198
- throw task.Exception!.InnerException ?? task.Exception;
199
- return t.GetProperty("Result")?.GetValue(value);
200
- }
287
+ var requested = new AssemblyName(args.Name).Name;
288
+ if (string.IsNullOrEmpty(requested)) return null;
201
289
 
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
- }
290
+ if (s_assemblyLoadCache.TryGetValue(requested, out var cached)) return cached;
210
291
 
211
- if (value is System.Threading.Tasks.ValueTask vt)
212
- {
213
- vt.AsTask().Wait();
214
- return null;
215
- }
292
+ // Check already-loaded assemblies first (avoid recursive Assembly.Load).
293
+ var loadedAsm = AppDomain.CurrentDomain.GetAssemblies()
294
+ .FirstOrDefault(a => string.Equals(a.GetName().Name, requested, StringComparison.OrdinalIgnoreCase));
295
+ if (loadedAsm is not null)
296
+ {
297
+ s_assemblyLoadCache[requested] = loadedAsm;
298
+ return loadedAsm;
299
+ }
216
300
 
217
- return value;
301
+ // Try to locate a matching dll in known search directories.
302
+ foreach (var dir in s_assemblySearchDirs)
303
+ {
304
+ try
305
+ {
306
+ var candidate = Path.Combine(dir, requested + ".dll");
307
+ if (File.Exists(candidate))
308
+ {
309
+ try
310
+ {
311
+ var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(candidate);
312
+ s_assemblyLoadCache[requested] = asm;
313
+ return asm;
314
+ }
315
+ catch { }
316
+ }
317
+ }
318
+ catch { }
319
+ }
320
+
321
+ s_assemblyLoadCache[requested] = null;
322
+ }
323
+ catch { }
324
+ return null;
218
325
  }
219
326
 
220
- private static Type? ResolveType(string? assemblyName, string? typeName)
327
+ [UnmanagedCallersOnly(EntryPoint = "RegisterJsCallback",
328
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
329
+ public static unsafe int RegisterJsCallback(
330
+ delegate* unmanaged[Cdecl]<int, byte*, int, byte**, int*, void> callback)
221
331
  {
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));
332
+ s_jsInvoker = callback;
333
+ return 0;
225
334
  }
226
335
 
227
- private static Type? ResolveTypeCore(string? assemblyName, string? typeName)
336
+ // Callback id registered by the runtime/JS that should receive unhandled
337
+ // managed exceptions and unobserved task exceptions.
338
+ private static int s_unhandledExceptionCallbackId = -1;
339
+
340
+ // Called from JS/Rust to request that managed unhandled exceptions be
341
+ // forwarded to the registered JS callback id. The callback id must have
342
+ // been created on the Rust side and point to a JS function stored in the
343
+ // runtime's `DOTNET_JS_CALLBACKS` map.
344
+ public static int RegisterUnhandledExceptionCallback(int callbackId)
228
345
  {
229
- var fqn = string.IsNullOrEmpty(assemblyName) ? typeName! : $"{typeName}, {assemblyName}";
230
- var t = Type.GetType(fqn);
231
- if (t is not null) return t;
346
+ s_unhandledExceptionCallbackId = callbackId;
232
347
 
233
- foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
348
+ // Subscribe once (idempotent).
349
+ try
234
350
  {
235
- t = asm.GetType(typeName!);
236
- if (t is not null) return t;
237
- }
351
+ AppDomain.CurrentDomain.UnhandledException -= OnAppDomainUnhandledException;
352
+ AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException;
238
353
 
239
- if (!string.IsNullOrEmpty(assemblyName))
240
- {
241
- try
354
+ TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException;
355
+ TaskScheduler.UnobservedTaskException += OnUnobservedTaskException;
356
+
357
+ // Try to subscribe to Windows.CoreApplication.UnhandledErrorDetected if available
358
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
242
359
  {
243
- var asm = Assembly.Load(assemblyName);
244
- t = asm.GetType(typeName!);
245
- if (t is not null) return t;
360
+ var coreAppType = asm.GetType("Windows.ApplicationModel.Core.CoreApplication");
361
+ if (coreAppType == null) continue;
362
+ var ev = coreAppType.GetEvent("UnhandledErrorDetected");
363
+ if (ev == null) continue;
364
+ try
365
+ {
366
+ var handlerType = ev.EventHandlerType;
367
+ var method = typeof(Bridge).GetMethod(nameof(CoreApplicationUnhandledErrorHandler), BindingFlags.NonPublic | BindingFlags.Static);
368
+ if (method != null)
369
+ {
370
+ var del = Delegate.CreateDelegate(handlerType, method);
371
+ ev.AddEventHandler(null, del);
372
+ }
373
+ }
374
+ catch { }
246
375
  }
247
- catch { }
248
376
  }
377
+ catch { }
249
378
 
250
- return null;
379
+ return 0;
251
380
  }
252
381
 
253
- private static MethodInfo? FindMethod(Type type, string name, int argCount, BindingFlags flags)
382
+ // Return a canonical native pointer (IUnknown / IInspectable) for an
383
+ // exported handle id. Returns 0 when no pointer is available. If a
384
+ // pointer isn't already cached in `s_nativePtrs`, attempt to obtain one
385
+ // via `Marshal.GetIUnknownForObject` and cache it for subsequent calls.
386
+ public static long GetNativePtrForHandle(int handleId)
254
387
  {
255
- var key = $"{type.FullName}|{name}|{argCount}|{(int)flags}";
256
- return s_methodCache.GetOrAdd(key, _ => FindMethodCore(type, name, argCount, flags));
257
- }
388
+ if (handleId <= 0) return 0;
389
+ if (s_nativePtrs.TryGetValue(handleId, out var p)) {
390
+ try { Debug.WriteLine($"[Bridge] GetNativePtrForHandle({handleId}) -> 0x{p:x} (cached)"); } catch { }
391
+ return p;
392
+ }
258
393
 
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);
394
+ if (!s_handles.TryGetValue(handleId, out var obj) || obj == null)
395
+ return 0;
396
+
397
+ try
398
+ {
399
+ var ip = Marshal.GetIUnknownForObject(obj);
400
+ if (ip != IntPtr.Zero)
401
+ {
402
+ var val = ip.ToInt64();
403
+ s_nativePtrs[handleId] = val;
404
+ try { Debug.WriteLine($"[Bridge] GetNativePtrForHandle({handleId}) -> 0x{val:x} (new)"); } catch { }
405
+ return val;
406
+ }
407
+ }
408
+ catch (Exception ex)
409
+ {
410
+ try { Debug.WriteLine($"[Bridge] GetNativePtrForHandle({handleId}) threw: {ex}"); } catch { }
411
+ }
412
+
413
+ try { Debug.WriteLine($"[Bridge] GetNativePtrForHandle({handleId}) -> 0 (none)"); } catch { }
414
+ return 0;
266
415
  }
267
416
 
268
- private static PropertyInfo? GetCachedProperty(Type type, string name, BindingFlags flags)
417
+ private static void OnAppDomainUnhandledException(object? sender, UnhandledExceptionEventArgs e)
269
418
  {
270
- var key = $"{type.FullName}|{name}|{(int)flags}";
271
- return s_propertyCache.GetOrAdd(key, _ => type.GetProperty(name, flags));
419
+ try
420
+ {
421
+ var ex = e.ExceptionObject as Exception;
422
+ var msg = ex?.ToString() ?? e.ExceptionObject?.ToString() ?? "(unknown)";
423
+ SendUnhandledToJs("unhandled", msg);
424
+ }
425
+ catch { }
272
426
  }
273
427
 
274
- private static object? Coerce(JsonElement el, Type targetType)
428
+ private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e)
275
429
  {
276
- if (el.ValueKind == JsonValueKind.Null) return null;
277
- if (el.ValueKind == JsonValueKind.Object && el.TryGetProperty("__handle", out var h))
430
+ try
278
431
  {
279
- s_handles.TryGetValue(h.GetInt32(), out var obj);
280
- return obj;
432
+ var msg = e.Exception?.ToString() ?? "(unobserved task exception)";
433
+ SendUnhandledToJs("unobservedTask", msg);
281
434
  }
282
- return JsonSerializer.Deserialize(el.GetRawText(), targetType, JsonOptions.Default);
435
+ catch { }
283
436
  }
284
437
 
285
- private static InvokeResult Box(object? value)
438
+ // Fallback handler for platform-specific CoreApplication unhandled events.
439
+ private static void CoreApplicationUnhandledErrorHandler(object? sender, object? args)
286
440
  {
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))
441
+ try
294
442
  {
295
- return new InvokeResult(
296
- JsonSerializer.SerializeToElement(value, t, JsonOptions.Default), null);
443
+ var msg = args?.ToString() ?? "(core unhandled)";
444
+ SendUnhandledToJs("coreUnhandled", msg);
297
445
  }
446
+ catch { }
447
+ }
298
448
 
299
- // Arrays and IEnumerable serialise as JSON arrays.
300
- if (t.IsArray || (t != typeof(string) && value is IEnumerable enumerable))
449
+ private static unsafe void SendUnhandledToJs(string kind, string message)
450
+ {
451
+ try
301
452
  {
302
- try
453
+ if (s_jsInvoker == null || s_unhandledExceptionCallbackId <= 0) return;
454
+ var payload = JsonSerializer.Serialize(new { kind = kind, message = message });
455
+ var bytes = System.Text.Encoding.UTF8.GetBytes(payload);
456
+ fixed (byte* p = bytes)
303
457
  {
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);
458
+ byte* respPtr = null;
459
+ int respLen = 0;
460
+ s_jsInvoker(s_unhandledExceptionCallbackId, p, bytes.Length, &respPtr, &respLen);
461
+ if (respPtr != null && respLen > 0) Marshal.FreeHGlobal((IntPtr)respPtr);
309
462
  }
310
- catch { }
311
463
  }
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);
464
+ catch { }
319
465
  }
320
466
 
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)
467
+ [UnmanagedCallersOnly(EntryPoint = "Invoke",
468
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
469
+ public static unsafe int Invoke(
470
+ byte* requestPtr, int requestLen,
471
+ byte** responsePtr, int* responseLenPtr)
325
472
  {
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;
473
+ try
474
+ {
475
+ var span = new ReadOnlySpan<byte>(requestPtr, requestLen);
476
+ var req = JsonSerializer.Deserialize(span, BridgeJsonContext.Default.InvokeRequest)!;
477
+ WriteResult(Dispatch(req), responsePtr, responseLenPtr);
478
+ }
479
+ catch (Exception ex)
480
+ {
481
+ WriteError(Unwrap(ex).Message, responsePtr, responseLenPtr);
482
+ }
483
+ return 0;
331
484
  }
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
485
 
344
- internal sealed record InvokeResult(JsonElement? Result, string? Error);
345
-
346
- internal static class JsonOptions
347
- {
348
- internal static readonly JsonSerializerOptions Default = new()
486
+ [UnmanagedCallersOnly(EntryPoint = "Free",
487
+ CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
488
+ public static unsafe void Free(byte* ptr)
349
489
  {
350
- PropertyNameCaseInsensitive = true,
351
- PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
352
- };
490
+ if (ptr != null) Marshal.FreeHGlobal((IntPtr)ptr);
491
+ }
353
492
  }