@nativescript/windows 0.1.0-beta.2 → 0.1.0-beta.4

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.
@@ -4,6 +4,12 @@ using Microsoft.UI.Dispatching;
4
4
  using Microsoft.UI.Xaml;
5
5
  using WinRT;
6
6
 
7
+ // Windows-only WinUI desktop app. The custom entry point (DISABLE_XAML_GENERATED_MAIN)
8
+ // has no platform attribute, so CA1416 treats every Windows API call site as "reachable
9
+ // on all platforms." Declaring the assembly's supported platform once tells the analyzer
10
+ // the whole assembly only runs on Windows >= 17763, silencing those false positives.
11
+ [assembly: System.Runtime.Versioning.SupportedOSPlatform("windows10.0.17763.0")]
12
+
7
13
  namespace __PROJECT_NAME__
8
14
  {
9
15
  public static class Program
@@ -108,6 +108,15 @@
108
108
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
109
109
  </Content>
110
110
 
111
+ <!-- Debug fallback: engine variants (quickjs/hermes/v8/jsc) ship no devtools build (no
112
+ inspector), so a Debug build must still pick up their plain runtime DLL instead of
113
+ silently omitting nativescript.dll from the output. -->
114
+ <Content Include="$(NSWindowsRoot)\libs\$(NSLibPlatform)\nativescript.dll"
115
+ Condition="'$(Configuration)' == 'Debug' and !Exists('$(NSWindowsRoot)\libs\devtools\$(NSLibPlatform)\nativescript.dll') and Exists('$(NSWindowsRoot)\libs\$(NSLibPlatform)\nativescript.dll')">
116
+ <Link>nativescript.dll</Link>
117
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
118
+ </Content>
119
+
111
120
  <!-- Release: production runtime DLL (platform fallback via NSLibPlatform) -->
112
121
  <Content Include="$(NSWindowsRoot)\libs\$(NSLibPlatform)\nativescript.dll"
113
122
  Condition="'$(Configuration)' != 'Debug' and Exists('$(NSWindowsRoot)\libs\$(NSLibPlatform)\nativescript.dll')">
@@ -115,6 +124,16 @@
115
124
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
116
125
  </Content>
117
126
 
127
+ <!-- Sidecar engine runtime DLLs some engine variants ship alongside nativescript.dll (e.g.
128
+ hermes.dll/hermes-icu.dll, JavaScriptCore.dll/icu*.dll) — same files regardless of
129
+ Debug/Release. nativescript.dll itself is excluded since it's handled above per config. -->
130
+ <Content Include="$(NSWindowsRoot)\libs\$(NSLibPlatform)\*.dll"
131
+ Exclude="$(NSWindowsRoot)\libs\$(NSLibPlatform)\nativescript.dll"
132
+ Condition="Exists('$(NSWindowsRoot)\libs\$(NSLibPlatform)')">
133
+ <Link>%(Filename)%(Extension)</Link>
134
+ <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
135
+ </Content>
136
+
118
137
  </ItemGroup>
119
138
 
120
139
  <!--
@@ -106,8 +106,17 @@ public static partial class Bridge
106
106
  {
107
107
  var assembly = r.ReadString16();
108
108
  var typeName = r.ReadString16();
109
+
110
+ var interfaceCount = r.ReadI32();
111
+ var interfaceNames = interfaceCount > 0 ? new string[interfaceCount] : [];
112
+ for (int i = 0; i < interfaceCount; i++) interfaceNames[i] = r.ReadString16();
113
+
114
+ var memberCount = r.ReadI32();
115
+ var memberNames = memberCount > 0 ? new string[memberCount] : [];
116
+ for (int i = 0; i < memberCount; i++) memberNames[i] = r.ReadString16();
117
+
109
118
  var callbackId = r.ReadI32();
110
- return CreateJsSubclass(NullIfEmpty(assembly), typeName, callbackId);
119
+ return CreateJsSubclass(NullIfEmpty(assembly), typeName, callbackId, interfaceNames, memberNames);
111
120
  }
112
121
 
113
122
  if (op == 0x0B) // get CLR-only property by raw IInspectable ptr (CLR reflection fallback)
@@ -14,8 +14,12 @@ namespace NativeScriptBridge;
14
14
 
15
15
  public static partial class Bridge
16
16
  {
17
- // Cache for dynamically generated proxy types (dev-only fallback).
18
- private static readonly ConcurrentDictionary<Type, Type> s_dynamicProxyCache = new();
17
+ // Cache for dynamically generated proxy types (dev-only fallback). Keyed by base type PLUS
18
+ // the exact set of interfaces/members a given JS `overrides` object asked for, so different
19
+ // JS subclasses of the same base type (different override sets) get distinct emitted types.
20
+ private readonly record struct ProxyTypeKey(Type BaseType, string InterfaceKey, string MemberKey);
21
+ private static readonly ConcurrentDictionary<ProxyTypeKey, Type> s_dynamicProxyCache = new();
22
+ private static int s_dynamicProxyTypeCounter = 0;
19
23
  private static readonly AssemblyBuilder? s_dynamicAssembly = CreateDynamicAssembly();
20
24
  private static readonly ModuleBuilder? s_dynamicModule = CreateDynamicModule(s_dynamicAssembly);
21
25
 
@@ -168,22 +172,35 @@ public static partial class Bridge
168
172
  return null;
169
173
  }
170
174
 
171
- private static Type GetOrCreateDynamicProxyType(Type baseType)
175
+ private static Type GetOrCreateDynamicProxyType(Type baseType, string[] interfaceNames, string[] memberNames)
172
176
  {
173
- return s_dynamicProxyCache.GetOrAdd(baseType, bt => CreateDynamicProxyType(bt));
177
+ var interfaceKey = string.Join(",", interfaceNames.OrderBy(n => n, StringComparer.Ordinal));
178
+ var memberKey = string.Join(",", memberNames.OrderBy(n => n, StringComparer.Ordinal));
179
+ var key = new ProxyTypeKey(baseType, interfaceKey, memberKey);
180
+ return s_dynamicProxyCache.GetOrAdd(key, _ => CreateDynamicProxyType(baseType, interfaceNames, memberNames));
174
181
  }
175
182
 
176
- private static Type CreateDynamicProxyType(Type baseType)
183
+ private static Type CreateDynamicProxyType(Type baseType, string[] interfaceNames, string[] memberNames)
177
184
  {
178
-
185
+
179
186
  if (s_dynamicModule == null)
180
187
  throw new InvalidOperationException("Dynamic proxy generation not available in this environment");
181
188
 
182
189
  if (baseType.IsSealed)
183
190
  throw new InvalidOperationException($"Cannot derive from sealed type {baseType.FullName}");
184
191
 
192
+ var interfaceTypes = interfaceNames
193
+ .Select(n => ResolveType(null, n) ?? throw new TypeLoadException($"Interface type not found: {n}"))
194
+ .ToArray();
195
+
196
+ // Members JS actually overrides. Only base virtuals in this set get an IL override
197
+ // emitted — anything else is left alone so the real base implementation stays live in
198
+ // the vtable (no "call base" IL needed, the base method was simply never touched).
199
+ var memberSet = new HashSet<string>(memberNames, StringComparer.Ordinal);
200
+
185
201
  var safeName = (baseType.FullName ?? Guid.NewGuid().ToString()).Replace('.', '_');
186
- var typeName = $"NSWinRTDynamicProxies.{safeName}_JsProxy";
202
+ var uniqueSuffix = Interlocked.Increment(ref s_dynamicProxyTypeCounter);
203
+ var typeName = $"NSWinRTDynamicProxies.{safeName}_JsProxy_{uniqueSuffix}";
187
204
 
188
205
  var tb = s_dynamicModule.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class, baseType);
189
206
 
@@ -265,69 +282,137 @@ public static partial class Bridge
265
282
  }
266
283
  }
267
284
 
268
- // Override all non-final virtual instance methods (skip ref/out and generic methods)
269
- var methods = baseType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
270
- .Where(m => m.IsVirtual && !m.IsFinal && !m.IsGenericMethod && !m.IsConstructor)
285
+ // Shared by both loops below, keyed by "Name(Param1FullName,Param2FullName,...)" so a
286
+ // member that's both an overridden base virtual and a requested interface member (e.g.
287
+ // the base class already implements that interface) reuses one emitted body instead of
288
+ // tripping a "duplicate member" TypeBuilder error.
289
+ var definedSignatures = new Dictionary<string, MethodBuilder>(StringComparer.Ordinal);
290
+ static string SignatureKey(string name, Type[] paramTypes) =>
291
+ name + "(" + string.Join(",", paramTypes.Select(p => p.FullName)) + ")";
292
+
293
+ // Override only the base virtuals JS actually asked for (memberSet). Anything not in
294
+ // that set is simply never touched, so the base implementation stays live in the vtable —
295
+ // no "call base" IL needed. (Skip ref/out and generic methods — unsupported.)
296
+ var virtualMethods = baseType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
297
+ .Where(m => m.IsVirtual && !m.IsFinal && !m.IsGenericMethod && !m.IsConstructor && memberSet.Contains(m.Name))
271
298
  .ToArray();
272
299
 
273
- foreach (var mi in methods)
300
+ foreach (var mi in virtualMethods)
274
301
  {
275
302
  var parameters = mi.GetParameters();
276
303
  if (parameters.Any(p => p.ParameterType.IsByRef)) continue;
277
304
 
278
305
  var paramTypes = parameters.Select(p => p.ParameterType).ToArray();
279
- var mb = tb.DefineMethod(mi.Name,
280
- MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
281
- mi.CallingConvention, mi.ReturnType, paramTypes);
282
-
283
- var ilg = mb.GetILGenerator();
284
- var argsLocal = ilg.DeclareLocal(typeof(object[]));
285
- ilg.Emit(OpCodes.Ldc_I4, paramTypes.Length);
286
- ilg.Emit(OpCodes.Newarr, typeof(object));
287
- ilg.Emit(OpCodes.Stloc, argsLocal);
288
-
289
- for (int i = 0; i < paramTypes.Length; i++)
306
+ var sigKey = SignatureKey(mi.Name, paramTypes);
307
+ if (!definedSignatures.TryGetValue(sigKey, out var mb))
290
308
  {
291
- ilg.Emit(OpCodes.Ldloc, argsLocal);
292
- ilg.Emit(OpCodes.Ldc_I4, i);
293
- ilg.Emit(OpCodes.Ldarg, i + 1);
294
- if (paramTypes[i].IsValueType) ilg.Emit(OpCodes.Box, paramTypes[i]);
295
- ilg.Emit(OpCodes.Stelem_Ref);
309
+ mb = EmitProxyMethodBody(tb, mi.Name,
310
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig,
311
+ mi.CallingConvention, mi.ReturnType, paramTypes, mi.Name);
312
+ definedSignatures[sigKey] = mb;
296
313
  }
297
314
 
298
- if (mi.ReturnType == typeof(void))
315
+ tb.DefineMethodOverride(mb, mi);
316
+ }
317
+
318
+ // Interfaces: every requested interface (plus any interfaces *those* extend, since
319
+ // AddInterfaceImplementation only registers the interface named, not its ancestors) must
320
+ // have every member implemented — the CLR requires it, unlike base-class virtuals where
321
+ // skipping is fine. Members JS doesn't override fall back to default/no-op via the same
322
+ // generic dispatcher (ProxyRuntime.InvokeMethodTyped/InvokeVoid already do this).
323
+ if (interfaceTypes.Length > 0)
324
+ {
325
+ var allInterfaceTypes = new List<Type>();
326
+ void CollectInterfaces(Type t)
299
327
  {
300
- var invokeVoid = typeof(Bridge).GetNestedType("ProxyRuntime", BindingFlags.Public | BindingFlags.Static)!.GetMethod("InvokeVoid", BindingFlags.Public | BindingFlags.Static)!;
301
- ilg.Emit(OpCodes.Ldarg_0);
302
- ilg.Emit(OpCodes.Ldstr, mi.Name);
303
- ilg.Emit(OpCodes.Ldloc, argsLocal);
304
- ilg.Emit(OpCodes.Call, invokeVoid);
305
- ilg.Emit(OpCodes.Ret);
328
+ if (allInterfaceTypes.Contains(t)) return;
329
+ allInterfaceTypes.Add(t);
330
+ foreach (var parent in t.GetInterfaces()) CollectInterfaces(parent);
306
331
  }
307
- else
332
+ foreach (var ifaceType in interfaceTypes) CollectInterfaces(ifaceType);
333
+
334
+ foreach (var ifaceType in allInterfaceTypes) tb.AddInterfaceImplementation(ifaceType);
335
+
336
+ foreach (var ifaceType in allInterfaceTypes)
308
337
  {
309
- var proxyRuntimeType = typeof(Bridge).GetNestedType("ProxyRuntime", BindingFlags.Public | BindingFlags.Static)!;
310
- var gm = proxyRuntimeType.GetMethod("InvokeMethodTyped", BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(mi.ReturnType);
311
- ilg.Emit(OpCodes.Ldarg_0);
312
- ilg.Emit(OpCodes.Ldstr, mi.Name);
313
- ilg.Emit(OpCodes.Ldloc, argsLocal);
314
- ilg.Emit(OpCodes.Call, gm);
315
- ilg.Emit(OpCodes.Ret);
316
- }
338
+ foreach (var mi in ifaceType.GetMethods())
339
+ {
340
+ var parameters = mi.GetParameters();
341
+ if (parameters.Any(p => p.ParameterType.IsByRef)) continue;
317
342
 
318
- tb.DefineMethodOverride(mb, mi);
343
+ var paramTypes = parameters.Select(p => p.ParameterType).ToArray();
344
+ var sigKey = SignatureKey(mi.Name, paramTypes);
345
+ if (!definedSignatures.TryGetValue(sigKey, out var mb))
346
+ {
347
+ mb = EmitProxyMethodBody(tb, mi.Name,
348
+ MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final
349
+ | MethodAttributes.HideBySig | MethodAttributes.NewSlot,
350
+ mi.CallingConvention, mi.ReturnType, paramTypes, mi.Name);
351
+ definedSignatures[sigKey] = mb;
352
+ }
353
+
354
+ tb.DefineMethodOverride(mb, mi);
355
+ }
356
+ }
319
357
  }
320
358
 
321
359
  return tb.CreateTypeInfo()!.AsType();
322
360
  }
323
361
 
324
- private static DispatchResult CreateJsSubclass(string? assemblyName, string typeName, int callbackId)
362
+ // Shared IL body for both base-virtual overrides and interface-member implementations: box
363
+ // args into an object[], forward to ProxyRuntime.InvokeVoid/InvokeMethodTyped<T> by name.
364
+ private static MethodBuilder EmitProxyMethodBody(
365
+ TypeBuilder tb, string emittedName, MethodAttributes attrs, CallingConventions callingConvention,
366
+ Type returnType, Type[] paramTypes, string dispatchName)
367
+ {
368
+ var mb = tb.DefineMethod(emittedName, attrs, callingConvention, returnType, paramTypes);
369
+ var ilg = mb.GetILGenerator();
370
+ var argsLocal = ilg.DeclareLocal(typeof(object[]));
371
+ ilg.Emit(OpCodes.Ldc_I4, paramTypes.Length);
372
+ ilg.Emit(OpCodes.Newarr, typeof(object));
373
+ ilg.Emit(OpCodes.Stloc, argsLocal);
374
+
375
+ for (int i = 0; i < paramTypes.Length; i++)
376
+ {
377
+ ilg.Emit(OpCodes.Ldloc, argsLocal);
378
+ ilg.Emit(OpCodes.Ldc_I4, i);
379
+ ilg.Emit(OpCodes.Ldarg, i + 1);
380
+ if (paramTypes[i].IsValueType) ilg.Emit(OpCodes.Box, paramTypes[i]);
381
+ ilg.Emit(OpCodes.Stelem_Ref);
382
+ }
383
+
384
+ if (returnType == typeof(void))
385
+ {
386
+ var invokeVoid = typeof(Bridge).GetNestedType("ProxyRuntime", BindingFlags.Public | BindingFlags.Static)!.GetMethod("InvokeVoid", BindingFlags.Public | BindingFlags.Static)!;
387
+ ilg.Emit(OpCodes.Ldarg_0);
388
+ ilg.Emit(OpCodes.Ldstr, dispatchName);
389
+ ilg.Emit(OpCodes.Ldloc, argsLocal);
390
+ ilg.Emit(OpCodes.Call, invokeVoid);
391
+ ilg.Emit(OpCodes.Ret);
392
+ }
393
+ else
394
+ {
395
+ var proxyRuntimeType = typeof(Bridge).GetNestedType("ProxyRuntime", BindingFlags.Public | BindingFlags.Static)!;
396
+ var gm = proxyRuntimeType.GetMethod("InvokeMethodTyped", BindingFlags.Public | BindingFlags.Static)!.MakeGenericMethod(returnType);
397
+ ilg.Emit(OpCodes.Ldarg_0);
398
+ ilg.Emit(OpCodes.Ldstr, dispatchName);
399
+ ilg.Emit(OpCodes.Ldloc, argsLocal);
400
+ ilg.Emit(OpCodes.Call, gm);
401
+ ilg.Emit(OpCodes.Ret);
402
+ }
403
+
404
+ return mb;
405
+ }
406
+
407
+ private static DispatchResult CreateJsSubclass(
408
+ string? assemblyName, string typeName, int callbackId, string[] interfaceNames, string[] memberNames)
325
409
  {
326
410
  EnsureProxyDispatcherCallbacksRegistered();
327
411
 
328
412
  s_pendingProxyCallbackId.Value = callbackId;
329
413
 
330
- // Try to find a statically-generated proxy type first.
414
+ // Try to find a statically-generated proxy type first (Mechanism A: sbg-compiled into
415
+ // the app itself — already has any requested interfaces baked into real C# source).
331
416
  Type? t = FindGeneratedProxyType(typeName);
332
417
  if (t is null)
333
418
  {
@@ -342,14 +427,14 @@ public static partial class Bridge
342
427
  resolveFrom = assemblyName;
343
428
  }
344
429
 
345
- // Resolve the WinRT/base type and optionally emit a dynamic proxy (dev-only).
430
+ // Resolve the WinRT/base type and optionally emit a dynamic proxy (Mechanism B fallback).
346
431
  var baseType = ResolveType(null, resolveFrom)
347
432
  ?? ResolveType(assemblyName, typeName)
348
433
  ?? throw new TypeLoadException($"Type not found: {resolveFrom} (proxy: {typeName}, assembly: {assemblyName})");
349
-
434
+
350
435
  if (baseType.IsClass && !baseType.IsSealed)
351
436
  {
352
- try { t = GetOrCreateDynamicProxyType(baseType); }
437
+ try { t = GetOrCreateDynamicProxyType(baseType, interfaceNames, memberNames); }
353
438
  catch (Exception) { t = baseType; }
354
439
  }
355
440
  else
@@ -371,6 +456,14 @@ public static partial class Bridge
371
456
  // the pending callback id when dynamic proxies don't call it.
372
457
  try { ProxyInitializeInstance(instance, typeName); } catch { }
373
458
 
459
+ // Force COM CCW vtable creation for any implemented WinRT interfaces now, rather than
460
+ // relying on it happening implicitly the first time the instance crosses the ABI boundary.
461
+ // Best-effort/reflection-only — no-ops cleanly when CsWinRT isn't loaded (e.g. xunit host).
462
+ if (interfaceNames.Length > 0)
463
+ {
464
+ TryActivateWinRTInterfaces(instance);
465
+ }
466
+
374
467
  // Box the instance into a handle to return to the runtime.
375
468
  var result = Box(instance);
376
469
 
@@ -392,6 +485,50 @@ public static partial class Bridge
392
485
  return result;
393
486
  }
394
487
 
488
+ // Locates CsWinRT's ComWrappersSupport via reflection (same style as the WinRT.IWinRTObject
489
+ // lookup in ObtainNativePtr — no compile-time reference needed, since DotNetBridge.csproj is a
490
+ // dependency-free net9.0 library and CsWinRT is only ever loaded into the process by the WinUI
491
+ // app itself) and asks it to build a real COM CCW for the instance's implemented WinRT
492
+ // interfaces now, via CsWinRT's JIT reflection-fallback vtable path (works for types it never
493
+ // saw at compile time — see plan doc for the CsWinRT version/AOT caveats). Best-effort: no-ops
494
+ // when CsWinRT isn't loaded, e.g. the xunit test host has no WinUI/CsWinRT at all.
495
+ private static void TryActivateWinRTInterfaces(object instance)
496
+ {
497
+ try
498
+ {
499
+ Type? supportType = null;
500
+ foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
501
+ {
502
+ try
503
+ {
504
+ supportType = asm.GetType("WinRT.ComWrappersSupport");
505
+ if (supportType != null) break;
506
+ }
507
+ catch { }
508
+ }
509
+ if (supportType == null) return;
510
+
511
+ foreach (var name in new[] { "CreateCCWForObject", "GetOrCreateComInterfaceForObject" })
512
+ {
513
+ foreach (var candidate in supportType.GetMethods(BindingFlags.Public | BindingFlags.Static)
514
+ .Where(m => m.Name == name))
515
+ {
516
+ try
517
+ {
518
+ var target = candidate.IsGenericMethodDefinition
519
+ ? candidate.MakeGenericMethod(instance.GetType())
520
+ : candidate;
521
+ var ps = target.GetParameters();
522
+ if (ps.Length == 1) { target.Invoke(null, [instance]); return; }
523
+ if (ps.Length == 2 && ps[1].ParameterType == typeof(Guid)) { target.Invoke(null, [instance, Guid.Empty]); return; }
524
+ }
525
+ catch { /* try the next overload/name */ }
526
+ }
527
+ }
528
+ }
529
+ catch { /* best-effort only */ }
530
+ }
531
+
395
532
  private static void ProxyInitializeInstance(object instance, string typeName)
396
533
  {
397
534
  var cb = s_pendingProxyCallbackId.Value;
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nativescript/windows",
3
- "version": "0.1.0-beta.2",
3
+ "version": "0.1.0-beta.4",
4
4
  "description": "NativeScript Windows runtime with a WinUI 3 app template",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,5 +17,9 @@
17
17
  "framework/**/*",
18
18
  "!framework/**/bin/**",
19
19
  "!framework/**/obj/**"
20
- ]
20
+ ],
21
+ "nativescript": {
22
+ "runtime": "windows",
23
+ "engine": "v8"
24
+ }
21
25
  }