From 7c01df7fad9f5debad1d4a3316e0b358544c6d67 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 23 Oct 2026 10:28:06 -0400 Subject: [PATCH 01/10] fix(jsc): BigInt values in the JSC Node-API shim The shim declared no BigInt support, so napi_create_bigint_* and napi_get_value_bigint_* were missing and any BigInt crossing the boundary (a .NET long, a WinRT __native_ptr) aborted the process. The JavaScriptCore.dll this package ships exports the BigInt C API, so the shim now implements them on it and napi_typeof reports napi_bigint. --- packages/windows-jsc/vendor/shim/jsc-api.cpp | 126 ++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/packages/windows-jsc/vendor/shim/jsc-api.cpp b/packages/windows-jsc/vendor/shim/jsc-api.cpp index 45891b0..5cb0101 100644 --- a/packages/windows-jsc/vendor/shim/jsc-api.cpp +++ b/packages/windows-jsc/vendor/shim/jsc-api.cpp @@ -1480,13 +1480,33 @@ napi_status napi_create_range_error(napi_env env, return napi_ok; } +// JavaScriptCore's BigInt C API (exported by the bundled JavaScriptCore.dll; the vendored headers +// predate it). +extern "C" { +JS_EXPORT JSValueRef JSBigIntCreateWithInt64(JSContextRef ctx, int64_t integer, JSValueRef* exception); +JS_EXPORT JSValueRef JSBigIntCreateWithUInt64(JSContextRef ctx, uint64_t integer, JSValueRef* exception); +JS_EXPORT JSValueRef JSBigIntCreateWithString(JSContextRef ctx, JSStringRef string, JSValueRef* exception); +JS_EXPORT bool JSValueIsBigInt(JSContextRef ctx, JSValueRef value); +JS_EXPORT int64_t JSValueToInt64(JSContextRef ctx, JSValueRef value, JSValueRef* exception); +JS_EXPORT uint64_t JSValueToUInt64(JSContextRef ctx, JSValueRef value, JSValueRef* exception); +typedef enum { kJSRelationConditionUndefined, kJSRelationConditionEqual, kJSRelationConditionGreaterThan, kJSRelationConditionLessThan } JSRelationCondition; +JS_EXPORT JSRelationCondition JSValueCompareInt64(JSContextRef ctx, JSValueRef left, int64_t right, JSValueRef* exception); +JS_EXPORT JSRelationCondition JSValueCompareUInt64(JSContextRef ctx, JSValueRef left, uint64_t right, JSValueRef* exception); +} + +// JSType's BigInt member, after kJSTypeSymbol in newer headers. +static constexpr int kJSTypeBigIntValue = 7; + napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result) { CHECK_ENV(env); CHECK_ARG(env, value); CHECK_ARG(env, result); - // JSC does not support BigInt JSType valueType = JSValueGetType(env->context, ToJSValue(value)); + if (static_cast(valueType) == kJSTypeBigIntValue) { + *result = napi_bigint; + return napi_ok; + } switch (valueType) { case kJSTypeUndefined: *result = napi_undefined; break; case kJSTypeNull: *result = napi_null; break; @@ -1736,6 +1756,110 @@ napi_status napi_get_value_int64(napi_env env, napi_value value, int64_t* result return napi_ok; } +napi_status napi_create_bigint_int64(napi_env env, int64_t value, napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + JSValueRef exception{}; + JSValueRef big = JSBigIntCreateWithInt64(env->context, value, &exception); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; +} + +napi_status napi_create_bigint_uint64(napi_env env, uint64_t value, napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + JSValueRef exception{}; + JSValueRef big = JSBigIntCreateWithUInt64(env->context, value, &exception); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; +} + +// Built from a hex string: the C API has no words constructor. +napi_status napi_create_bigint_words(napi_env env, int sign_bit, size_t word_count, const uint64_t* words, napi_value* result) { + CHECK_ENV(env); + CHECK_ARG(env, result); + if (word_count > 0) CHECK_ARG(env, words); + std::string text = sign_bit ? "-0x" : "0x"; + bool leading = true; + static const char digits[] = "0123456789abcdef"; + for (size_t i = word_count; i-- > 0;) { + for (int shift = 60; shift >= 0; shift -= 4) { + int nibble = static_cast((words[i] >> shift) & 0xF); + if (leading && nibble == 0) continue; + leading = false; + text.push_back(digits[nibble]); + } + } + if (leading) text = "0"; + JSStringRef string = JSStringCreateWithUTF8CString(text.c_str()); + JSValueRef exception{}; + JSValueRef big = JSBigIntCreateWithString(env->context, string, &exception); + JSStringRelease(string); + CHECK_JSC(env, exception); + *result = ToNapi(big); + return napi_ok; +} + +napi_status napi_get_value_bigint_int64(napi_env env, napi_value value, int64_t* result, bool* lossless) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + JSValueRef v = ToJSValue(value); + if (!JSValueIsBigInt(env->context, v)) return napi_set_last_error(env, napi_bigint_expected); + JSValueRef exception{}; + *result = JSValueToInt64(env->context, v, &exception); + CHECK_JSC(env, exception); + if (lossless) *lossless = JSValueCompareInt64(env->context, v, *result, &exception) == kJSRelationConditionEqual; + return napi_ok; +} + +napi_status napi_get_value_bigint_uint64(napi_env env, napi_value value, uint64_t* result, bool* lossless) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, result); + JSValueRef v = ToJSValue(value); + if (!JSValueIsBigInt(env->context, v)) return napi_set_last_error(env, napi_bigint_expected); + JSValueRef exception{}; + *result = JSValueToUInt64(env->context, v, &exception); + CHECK_JSC(env, exception); + if (lossless) *lossless = JSValueCompareUInt64(env->context, v, *result, &exception) == kJSRelationConditionEqual; + return napi_ok; +} + +// Sign and the low 64 bits; a wider value reports word_count 2 with a zero high word. +napi_status napi_get_value_bigint_words(napi_env env, napi_value value, int* sign_bit, size_t* word_count, uint64_t* words) { + CHECK_ENV(env); + CHECK_ARG(env, value); + CHECK_ARG(env, word_count); + JSValueRef v = ToJSValue(value); + if (!JSValueIsBigInt(env->context, v)) return napi_set_last_error(env, napi_bigint_expected); + JSValueRef exception{}; + bool negative = JSValueCompareInt64(env->context, v, 0, &exception) == kJSRelationConditionLessThan; + uint64_t magnitude; + bool fits; + if (negative) { + int64_t s = JSValueToInt64(env->context, v, &exception); + fits = JSValueCompareInt64(env->context, v, s, &exception) == kJSRelationConditionEqual; + magnitude = 0 - static_cast(s); + } else { + magnitude = JSValueToUInt64(env->context, v, &exception); + fits = JSValueCompareUInt64(env->context, v, magnitude, &exception) == kJSRelationConditionEqual; + } + CHECK_JSC(env, exception); + size_t needed = magnitude == 0 && fits ? 0 : (fits ? 1 : 2); + if (words == nullptr) { + *word_count = needed; + return napi_ok; + } + if (sign_bit) *sign_bit = negative ? 1 : 0; + if (*word_count > 0) words[0] = magnitude; + if (*word_count > 1) words[1] = 0; + *word_count = needed; + return napi_ok; +} + napi_status napi_get_value_bool(napi_env env, napi_value value, bool* result) { CHECK_ENV(env); CHECK_ARG(env, value); From e6c95d5a1003b14a63ece75b7f49454a417c5502 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 23 Oct 2026 10:28:06 -0400 Subject: [PATCH 02/10] feat(typings): .NET typings that match what the runtime exposes - nested types in a namespace named after their outer type (Outer.Inner) - type parameters on generic classes and methods, plus static of() - long/ulong as number | bigint, byte[] as Uint8Array (and what JS can pass) - optional and params parameters, out/ref references, and the [result, ...outs] overload for out parameters left off - BCL delegates, Task and Nullable as JS function, PromiseLike and T | null - protected members, abstract classes, virtual properties as accessors so JS subclasses can override them - parameter names read by sequence number (a return row no longer shifts them) --- typings-generator/dotnet-src/Program.cs | 214 ++++++++++++++++++++---- 1 file changed, 185 insertions(+), 29 deletions(-) diff --git a/typings-generator/dotnet-src/Program.cs b/typings-generator/dotnet-src/Program.cs index 41e9d19..b9d8b50 100644 --- a/typings-generator/dotnet-src/Program.cs +++ b/typings-generator/dotnet-src/Program.cs @@ -83,7 +83,7 @@ public static int Run(string[] args) if (td.BaseType.IsNil) continue; if (Helpers.ResolveEntityName(er, td.BaseType) != "System.Enum") continue; TsTypeDecoder.EnumNames.Add( - Helpers.StripArity(Helpers.FullName(er.GetString(td.Namespace), er.GetString(td.Name)))); + Helpers.StripArity(Helpers.FullName(Helpers.NamespaceOf(er, td) ?? "", er.GetString(td.Name)))); } } @@ -109,7 +109,8 @@ public static int Run(string[] args) var vis = typeDef.Attributes & TypeAttributes.VisibilityMask; if (vis != TypeAttributes.Public && vis != TypeAttributes.NestedPublic) continue; - var ns = reader.GetString(typeDef.Namespace); + var ns = Helpers.NamespaceOf(reader, typeDef); + if (ns == null) continue; // nested in a type that isn't public var name = Helpers.StripArity(reader.GetString(typeDef.Name)); var fullName = Helpers.FullName(ns, name); @@ -236,7 +237,7 @@ public static int Run(string[] args) if (!isIface && !typeDef.BaseType.IsNil) { string baseName = Helpers.ResolveEntityName(reader, typeDef.BaseType); - if (!string.IsNullOrEmpty(baseName) && baseName != "System.Object") + if (!string.IsNullOrEmpty(baseName) && !baseName.StartsWith("System.", StringComparison.Ordinal)) extendsClause = $" extends {Helpers.MapBuiltinType(baseName)}"; } @@ -257,7 +258,15 @@ public static int Run(string[] args) : (isIface ? (string.IsNullOrEmpty(extendsClause) ? " extends " : ", ") : " implements ") + string.Join(", ", ifaces); var sb = new StringBuilder(); - sb.AppendLine($" {kw} {simpleName}{extendsClause}{implementsClause} {{"); + // Abstract classes, not static ones (abstract and sealed). + if (!isIface && (typeDef.Attributes & (TypeAttributes.Abstract | TypeAttributes.Sealed)) == TypeAttributes.Abstract) + kw = "abstract class"; + var typeParams = typeDef.GetGenericParameters().Count; + var typeParamList = typeParams == 0 ? "" : "<" + string.Join(", ", Enumerable.Range(0, typeParams).Select(i => "T" + i)) + ">"; + sb.AppendLine($" {kw} {simpleName}{typeParamList}{extendsClause}{implementsClause} {{"); + // `Box.of(System.Int32)`: the closed generic type, constructed like the class itself. + if (typeParams > 0 && !isIface) + sb.AppendLine($" static of(...typeArguments: any[]): typeof {simpleName};"); // Member names emitted on THIS type, so inlined collection members below don't duplicate them. var emitted = new HashSet(StringComparer.Ordinal); @@ -279,7 +288,7 @@ public static int Run(string[] args) bool evStatic = (adder.Attributes & MethodAttributes.Static) != 0; string evName = reader.GetString(ev.Name); string handler = Helpers.MapBuiltinType(Helpers.ResolveTypeName(reader, dec, ev.Type)); - sb.AppendLine($" {(evStatic ? "static " : "")}{evName}: {handler} | null;"); + sb.AppendLine($" {(evStatic ? "static " : "")}{evName}: {Helpers.Ts(handler, false)} | null;"); emitted.Add(evName); } @@ -288,12 +297,13 @@ public static int Run(string[] args) { var field = reader.GetFieldDefinition(fieldHandle); if ((field.Attributes & FieldAttributes.FieldAccessMask) != FieldAttributes.Public) continue; + if ((field.Attributes & FieldAttributes.SpecialName) != 0) continue; // an enum's value__ string fName = reader.GetString(field.Name); bool fStat = (field.Attributes & FieldAttributes.Static) != 0; - bool fRo = (field.Attributes & FieldAttributes.InitOnly) != 0; + bool fRo = (field.Attributes & (FieldAttributes.InitOnly | FieldAttributes.Literal)) != 0; string fType; - try { fType = field.DecodeSignature(dec, null); } + try { fType = Helpers.Ts(field.DecodeSignature(dec, null), false); } catch { continue; } sb.AppendLine($" {(fStat ? "static " : "")}{(fRo ? "readonly " : "")}{fName}: {fType};"); @@ -308,8 +318,10 @@ public static int Run(string[] args) if (acc.Getter.IsNil) continue; var getter = reader.GetMethodDefinition(acc.Getter); - // Skip non-public properties (e.g. private helper properties) - if ((getter.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) continue; + // Public and protected properties (JS subclasses reach protected ones). + var getterAccess = getter.Attributes & MethodAttributes.MemberAccessMask; + bool prot = getterAccess is MethodAttributes.Family or MethodAttributes.FamORAssem; + if (getterAccess != MethodAttributes.Public && !prot) continue; bool stat = (getter.Attributes & MethodAttributes.Static) != 0; bool ro = acc.Setter.IsNil; @@ -318,7 +330,16 @@ public static int Run(string[] args) try { sig = prop.DecodeSignature(dec, null); } catch { continue; } - sb.AppendLine($" {(stat ? "static " : "")}{(ro ? "readonly " : "")}{pName}: {sig.ReturnType};"); + var mods = (prot ? "protected " : "") + (stat ? "static " : ""); + var pType = Helpers.Ts(sig.ReturnType, false); + // Virtual properties as accessors, so a JS subclass can override them with get/set. + if ((getter.Attributes & MethodAttributes.Virtual) != 0 && (getter.Attributes & MethodAttributes.Final) == 0) + { + sb.AppendLine($" {mods}get {pName}(): {pType};"); + if (!ro) sb.AppendLine($" {mods}set {pName}(value: {pType});"); + } + else + sb.AppendLine($" {mods}{(ro ? "readonly " : "")}{pName}: {pType};"); emitted.Add(pName); } @@ -326,7 +347,9 @@ public static int Run(string[] args) { if (accessors.Contains(methodHandle) || eventAccessors.Contains(methodHandle)) continue; var method = reader.GetMethodDefinition(methodHandle); - if ((method.Attributes & MethodAttributes.MemberAccessMask) != MethodAttributes.Public) continue; + var access = method.Attributes & MethodAttributes.MemberAccessMask; + bool isProtected = access is MethodAttributes.Family or MethodAttributes.FamORAssem; + if (access != MethodAttributes.Public && !isProtected) continue; string mName = reader.GetString(method.Name); bool isCtor = mName == ".ctor"; @@ -337,24 +360,68 @@ public static int Run(string[] args) try { sig = method.DecodeSignature(dec, null); } catch { continue; } - var paramHandles = method.GetParameters().ToArray(); - var paramStrs = new List(); + // Parameter rows by position (sequence 0, when present, is the return value). + var paramRows = new Dictionary(); + foreach (var ph in method.GetParameters()) + { + var p = reader.GetParameter(ph); + if (p.SequenceNumber > 0) paramRows[p.SequenceNumber - 1] = p; + } + var paramStrs = new List(); + var outTypes = new List(); + int firstTrailingOut = sig.ParameterTypes.Length; + for (int i = sig.ParameterTypes.Length - 1; i >= 0; i--) + { + if (!paramRows.TryGetValue(i, out var row) || (row.Attributes & ParameterAttributes.Out) == 0) break; + firstTrailingOut = i; + } + // Optional (`?`) only where every later parameter can be left off too. + var canOmit = new bool[sig.ParameterTypes.Length + 1]; + canOmit[sig.ParameterTypes.Length] = true; + for (int i = sig.ParameterTypes.Length - 1; i >= 0; i--) + { + paramRows.TryGetValue(i, out var row); + var optional = (row.Attributes & (ParameterAttributes.Optional | ParameterAttributes.HasDefault | ParameterAttributes.Out)) != 0 + || Helpers.IsParamArray(reader, row); + canOmit[i] = canOmit[i + 1] && optional; + } for (int i = 0; i < sig.ParameterTypes.Length; i++) { string pn = "p" + i; - if (i < paramHandles.Length) + paramRows.TryGetValue(i, out var row); + if (paramRows.ContainsKey(i)) { - var candidate = reader.GetString(reader.GetParameter(paramHandles[i]).Name); - if (!string.IsNullOrWhiteSpace(candidate)) pn = candidate; + var candidate = reader.GetString(row.Name); + if (!string.IsNullOrWhiteSpace(candidate)) pn = Helpers.SafeName(candidate); } - paramStrs.Add($"{pn}: {sig.ParameterTypes[i]}"); + var raw = sig.ParameterTypes[i]; + var byRef = raw.StartsWith(Helpers.RefMark, StringComparison.Ordinal); + var type = Helpers.Ts(byRef ? raw[Helpers.RefMark.Length..] : raw, true); + var isOut = (row.Attributes & ParameterAttributes.Out) != 0; + if (isOut) outTypes.Add(Helpers.Ts(raw[(byRef ? Helpers.RefMark.Length : 0)..], false)); + if (paramRows.ContainsKey(i) && Helpers.IsParamArray(reader, row) && i == sig.ParameterTypes.Length - 1) + paramStrs.Add($"...{pn}: {type}"); + else if (byRef) + paramStrs.Add($"{pn}{(canOmit[i] ? "?" : "")}: {(isOut ? "" : type + " | ")}{{ value?: {type} }}"); + else + paramStrs.Add($"{pn}{(canOmit[i] ? "?" : "")}: {type}"); } + string typeArgs = method.GetGenericParameters().Count is var n and > 0 + ? "<" + string.Join(", ", Enumerable.Range(0, n).Select(k => "M" + k)) + ">" : ""; if (isCtor) sb.AppendLine($" constructor({string.Join(", ", paramStrs)});"); else { - sb.AppendLine($" {(isStatic ? "static " : "")}{mName}({string.Join(", ", paramStrs)}): {sig.ReturnType};"); + var prefix = (isProtected ? "protected " : "") + (isStatic ? "static " : ""); + var ret = Helpers.Ts(sig.ReturnType, false); + // Out parameters left off come back with the result: [result, ...outs]. + if (firstTrailingOut < sig.ParameterTypes.Length && outTypes.Count > 0) + { + var tuple = (ret == "void" ? outTypes : new[] { ret }.Concat(outTypes.TakeLast(sig.ParameterTypes.Length - firstTrailingOut))).ToList(); + sb.AppendLine($" {prefix}{mName}{typeArgs}({string.Join(", ", paramStrs.Take(firstTrailingOut))}): [{string.Join(", ", tuple)}];"); + } + sb.AppendLine($" {prefix}{mName}{typeArgs}({string.Join(", ", paramStrs)}): {ret};"); emitted.Add(mName); } } @@ -464,6 +531,51 @@ public static string StripArity(string name) return tick >= 0 ? name.Substring(0, tick) : name; } + public const string RefMark = "$ref$"; + + // The namespace a type is declared in, with outer types for a nested one (`Ns.Outer`); null when + // an outer type isn't public. + public static string? NamespaceOf(MetadataReader reader, TypeDefinition typeDef) + { + if (!typeDef.IsNested) return reader.GetString(typeDef.Namespace); + var outer = reader.GetTypeDefinition(typeDef.GetDeclaringType()); + var vis = outer.Attributes & TypeAttributes.VisibilityMask; + if (vis != TypeAttributes.Public && vis != TypeAttributes.NestedPublic) return null; + var outerNs = NamespaceOf(reader, outer); + return outerNs == null ? null : FullName(outerNs, reader.GetString(outer.Name)); + } + + public static bool IsParamArray(MetadataReader reader, Parameter row) + { + foreach (var ah in row.GetCustomAttributes()) + { + var ctor = reader.GetCustomAttribute(ah).Constructor; + EntityHandle owner = ctor.Kind == HandleKind.MemberReference + ? reader.GetMemberReference((MemberReferenceHandle)ctor).Parent + : ctor.Kind == HandleKind.MethodDefinition ? reader.GetMethodDefinition((MethodDefinitionHandle)ctor).GetDeclaringType() : default; + if (!owner.IsNil && ResolveEntityName(reader, owner) == "System.ParamArrayAttribute") return true; + } + return false; + } + + static readonly HashSet Reserved = new(StringComparer.Ordinal) + { + "function", "var", "let", "const", "class", "default", "delete", "in", "new", "this", "typeof", + "void", "with", "yield", "enum", "export", "import", "super", "switch", "case", "catch", "finally", + "for", "if", "return", "throw", "try", "while", "do", "else", "break", "continue", "debugger", + "instanceof", "extends", "null", "true", "false", + }; + + public static string SafeName(string name) => Reserved.Contains(name) ? name + "_" : name; + + // A decoded type as TypeScript, for a parameter (what JS may pass) or a result (what JS gets). + public static string Ts(string type, bool parameter) => type + .Replace("$bytes$", parameter ? "Uint8Array | ArrayBuffer | number[]" : "Uint8Array") + .Replace("$long$[]", "(number | bigint)[]") + .Replace("$long$", "number | bigint") + .Replace("$byte$", "number") + .Replace(RefMark, ""); + public static string FullName(string ns, string name) { name = StripArity(name); @@ -533,43 +645,87 @@ class TsTypeDecoder : ISignatureTypeProvider PrimitiveTypeCode.Char => "string", PrimitiveTypeCode.String => "string", PrimitiveTypeCode.Object => "any", - PrimitiveTypeCode.SByte or PrimitiveTypeCode.Byte + // Markers finished by Helpers.Ts: byte[] is a Uint8Array, longs may be BigInts. + PrimitiveTypeCode.Byte => "$byte$", + PrimitiveTypeCode.Int64 or PrimitiveTypeCode.UInt64 => "$long$", + PrimitiveTypeCode.SByte or PrimitiveTypeCode.Int16 or PrimitiveTypeCode.UInt16 or PrimitiveTypeCode.Int32 or PrimitiveTypeCode.UInt32 - or PrimitiveTypeCode.Int64 or PrimitiveTypeCode.UInt64 or PrimitiveTypeCode.Single or PrimitiveTypeCode.Double or PrimitiveTypeCode.IntPtr or PrimitiveTypeCode.UIntPtr => "number", _ => "any", }; + // Non-generic BCL types as JS sees them; other System types (not declared here) are `any`. + static string MapSystem(string fn) => fn switch + { + "System.Threading.Tasks.Task" or "System.Threading.Tasks.ValueTask" => "PromiseLike", + "System.Action" => "(() => void)", + "System.EventHandler" => "((sender: any, e: any) => void)", + "System.Byte" => "$byte$", + "System.Int64" or "System.UInt64" => "$long$", + _ when fn.StartsWith("System.", StringComparison.Ordinal) && Helpers.MapBuiltinType(fn) == fn => "any", + _ => Helpers.MapBuiltinType(fn), + }; + public string GetTypeFromDefinition(MetadataReader r, TypeDefinitionHandle h, byte _) { var td = r.GetTypeDefinition(h); - var fn = Helpers.FullName(r.GetString(td.Namespace), r.GetString(td.Name)); + var raw = r.GetString(td.Name); + if (raw.Contains('`')) return Helpers.FullName(Helpers.NamespaceOf(r, td) ?? "", raw) + "`"; + var fn = Helpers.FullName(Helpers.NamespaceOf(r, td) ?? "", raw); if (EnumNames.Contains(Helpers.StripArity(fn))) return "number"; - return Helpers.MapBuiltinType(fn); + return MapSystem(fn); } public string GetTypeFromReference(MetadataReader r, TypeReferenceHandle h, byte _) { var tr = r.GetTypeReference(h); - var fn = Helpers.FullName(r.GetString(tr.Namespace), r.GetString(tr.Name)); + var raw = r.GetString(tr.Name); + // A nested type's reference is scoped by its outer type's. + var ns = tr.ResolutionScope.Kind == HandleKind.TypeReference + ? Helpers.ResolveEntityName(r, (TypeReferenceHandle)tr.ResolutionScope) : r.GetString(tr.Namespace); + if (raw.Contains('`')) return Helpers.FullName(ns, raw) + "`"; + var fn = Helpers.FullName(ns, raw); if (EnumNames.Contains(Helpers.StripArity(fn))) return "number"; - return Helpers.MapBuiltinType(fn); + return MapSystem(fn); } public string GetTypeFromSpecification(MetadataReader r, object? ctx, TypeSpecificationHandle h, byte _) => r.GetTypeSpecification(h).DecodeSignature(this, ctx); - public string GetSZArrayType(string elem) => $"{elem}[]"; - public string GetArrayType(string elem, ArrayShape _) => $"{elem}[]"; - public string GetByReferenceType(string elem) => elem; + public string GetSZArrayType(string elem) => elem == "$byte$" ? "$bytes$" : Array(elem); + public string GetArrayType(string elem, ArrayShape _) => Array(elem); + public string GetByReferenceType(string elem) => Helpers.RefMark + elem; + static string Array(string elem) => elem.Contains(' ') && !elem.StartsWith('(') ? $"({elem})[]" : $"{elem}[]"; public string GetPointerType(string _) => "any"; public string GetPinnedType(string elem) => elem; public string GetModifiedType(string _, string unmodified, bool __) => unmodified; public string GetFunctionPointerType(MethodSignature _) => "Function"; - public string GetGenericMethodParameter(object? _, int index) => $"T{index}"; + public string GetGenericMethodParameter(object? _, int index) => $"M{index}"; public string GetGenericTypeParameter(object? _, int index) => $"T{index}"; + + // BCL delegates as JS functions, tasks as promises (results are awaitable), Nullable as + // `T | null`; other BCL generics are `any`. public string GetGenericInstantiation(string generic, ImmutableArray typeArgs) - => $"{generic}<{string.Join(", ", typeArgs)}>"; + { + // A generic definition comes back from GetTypeFrom* with a trailing backtick. + generic = generic.TrimEnd('`'); + string Params(IEnumerable types) => string.Join(", ", types.Select((t, i) => $"arg{i}: {Helpers.Ts(t, false)}")); + switch (generic) + { + case "System.Action": return $"(({Params(typeArgs)}) => void)"; + case "System.Func": return $"(({Params(typeArgs.Take(typeArgs.Length - 1))}) => {Helpers.Ts(typeArgs[^1], true)})"; + case "System.EventHandler": return $"((sender: any, e: {typeArgs[0]}) => void)"; + case "System.Nullable": return $"{typeArgs[0]} | null"; + case "System.Threading.Tasks.Task": + case "System.Threading.Tasks.ValueTask": return $"PromiseLike<{typeArgs[0]}>"; + case "System.Predicate": return $"(({Params(typeArgs)}) => boolean)"; + case "System.Memory": + case "System.ReadOnlyMemory": + case "System.ArraySegment": return typeArgs[0] == "$byte$" ? "$bytes$" : "any"; + } + if (generic == "any" || generic.StartsWith("System.", StringComparison.Ordinal)) return "any"; + return $"{generic}<{string.Join(", ", typeArgs)}>"; + } } From 989b686398083ec9472e88929754f94ee7e0bb1f Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 23 Oct 2026 10:30:11 -0400 Subject: [PATCH 03/10] feat(dotnet): fields, generics, events and out parameters of .NET types from JS The .NET JS layer (NSWinRT.dotnet and JS subclasses of .NET/WinRT classes) was hand-copied into both engines; it now lives once in runtime/src/js and both include it. What C# code can now do from JS, on the classic and all napi engines: - fields, constants and enum members (DayOfWeek.Monday, tuple Item1, struct fields), read-only ones refusing assignment - nested types as Outer.Inner - one JS object per .NET object: the bridge hands out one handle per object and counts its exports, so a release racing a new export can't drop it - long/ulong beyond 2^53 as BigInt both ways; BigInt arguments were null - byte[] (and Memory, ArraySegment) as Uint8Array; typed arrays, ArrayBuffers and DataViews as arrays, Memory or Stream - optional/default parameters, named arguments (NSWinRT.dotnet.named), ref/out through NSWinRT.interop.out() and [result, ...outs] when out parameters are left off, as WinRT methods do - generic types (List.of(System.String)) and methods (inferred, or .of(T)) - extension methods (list.Where(fn).ToList()) - events: obj.Event = fn replaces the handler as a WinRT event does, and add_X/remove_X remove the delegate that was added for a function - string-keyed dictionaries by key (dict.key) - errors carrying the .NET type, stack and exception (error.nativeException); faulted Tasks reject with an Error; JS errors reach C# with their stack, from void callbacks too; unhandled .NET exceptions reach __onUncaughtError - protected members and generic virtuals in JS subclasses - toString() is the object's ToString() - NSWinRT.dotnet.loadAssembly(path); a type in an assembly loaded later now resolves (failed lookups were cached forever) Also fixes the fallback for an overload that isn't cached, which passed a pooled oversized array to MethodInfo.Invoke and always threw. Tests: dotnet-bridge-tests/InteropTests.cs (136 xunit), test-app tests/csharp-interop.js (42 specs; classic 131/131, napi V8 123/123; QuickJS, Hermes and JSC fail only pre-existing Lifetime specs). --- dotnet-bridge-tests/InteropTests.cs | 285 ++ dotnet-bridge/BinaryProtocol.cs | 20 +- dotnet-bridge/Bridge.Binary.cs | 154 + dotnet-bridge/Bridge.BinaryDispatch.cs | 155 +- dotnet-bridge/Bridge.Binder.cs | 389 +++ dotnet-bridge/Bridge.Dispatch.cs | 133 +- dotnet-bridge/Bridge.Errors.cs | 57 + dotnet-bridge/Bridge.Identity.cs | 74 + dotnet-bridge/Bridge.JsDelegate.cs | 36 +- dotnet-bridge/Bridge.Members.cs | 183 ++ dotnet-bridge/Bridge.Proxy.cs | 93 +- dotnet-bridge/Bridge.TaskHelper.cs | 9 +- dotnet-bridge/Bridge.cs | 122 +- dotnet-bridge/DispatchTypes.cs | 52 +- runtime/src/global_fns.rs | 2807 ++++------------- runtime/src/js/dotnet.js | 1255 ++++++++ runtime/src/js/dotnet_subclass.js | 855 +++++ runtime/src/napi_engine/dotnet.rs | 2128 ++----------- .../Windows/src/TestFixtures/Interop.cs | 247 ++ test-app/app/main.js | 1 + test-app/app/tests/csharp-interop.js | 354 +++ 21 files changed, 5049 insertions(+), 4360 deletions(-) create mode 100644 dotnet-bridge-tests/InteropTests.cs create mode 100644 dotnet-bridge/Bridge.Binary.cs create mode 100644 dotnet-bridge/Bridge.Binder.cs create mode 100644 dotnet-bridge/Bridge.Errors.cs create mode 100644 dotnet-bridge/Bridge.Identity.cs create mode 100644 dotnet-bridge/Bridge.Members.cs create mode 100644 runtime/src/js/dotnet.js create mode 100644 runtime/src/js/dotnet_subclass.js create mode 100644 test-app/App_Resources/Windows/src/TestFixtures/Interop.cs create mode 100644 test-app/app/tests/csharp-interop.js diff --git a/dotnet-bridge-tests/InteropTests.cs b/dotnet-bridge-tests/InteropTests.cs new file mode 100644 index 0000000..d423a8f --- /dev/null +++ b/dotnet-bridge-tests/InteropTests.cs @@ -0,0 +1,285 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NativeScriptBridge; +using Xunit; + +namespace DotNetBridgeTests; + +public static class InteropFixtures +{ + public const int Limit = 10; + public static readonly string Fixed = "fixed"; + public static int Counter; + + public static string Optional(string a, string b = "B", int c = 3) => $"{a}{b}{c}"; + public static bool TryDouble(string text, out int result) => (result = int.TryParse(text, out var v) ? v * 2 : 0) != 0; + public static void Swap(ref int a, ref int b) => (a, b) = (b, a); + public static string Point(int x = 0, int y = 0) => $"({x},{y})"; + public static T Identity(T value) => value; + public static string NameOf() => typeof(T).Name; + public static int CountOf(IEnumerable items) => items.Count(); + public static long Echo(long value) => value; + public static int Sum(byte[] data) => data.Sum(b => (int)b); + public static int SumInts(int[] data) => data.Sum(); + public static long StreamLength(Stream s) => s.Length; + public static object Shared { get; } = new object(); + + public class Nested + { + public static string Where() => "nested"; + } +} + +public sealed class FieldHolder +{ + public int Value = 7; + public readonly int Fixed = 1; +} + +public static class InteropExtensions +{ + public static int Doubled(this FieldHolder holder) => holder.Value * 2; +} + +[Collection("Bridge")] +/// Handle identity, fields, the slow-path binder (optional, out/ref, named, generic, extension +/// methods), longs, binary data and type names. +public sealed class InteropTests : IDisposable +{ + public InteropTests() => Bridge.ClearCaches(); + public void Dispose() => Bridge.ClearCaches(); + + private const string Fixtures = "DotNetBridgeTests.InteropFixtures"; + + [Fact] + public void SameObject_GetsTheSameHandle_UntilEveryExportIsGivenBack() + { + var first = Static(Fixtures, "get_Shared", null, 0).HandleId(); + var second = Static(Fixtures, "get_Shared", null, 0).HandleId(); + Assert.Equal(first, second); + + Release(first, 1); + Assert.True(Bridge.s_handles.ContainsKey(first)); + Release(first, 1); + Assert.False(Bridge.s_handles.ContainsKey(first)); + + var third = Static(Fixtures, "get_Shared", null, 0).HandleId(); + Assert.NotEqual(first, third); + } + + [Fact] + public void ReleaseWithoutACount_DropsTheHandle() + { + var h = Static(Fixtures, "get_Shared", null, 0).HandleId(); + Static(Fixtures, "get_Shared", null, 0); + Release(h, null); + Assert.False(Bridge.s_handles.ContainsKey(h)); + } + + [Fact] + public void Fields_ReadAndWrite() + { + Assert.Equal(10, Static(Fixtures, "get_Limit", null, 0).PrimitiveValue()); + Assert.Equal("fixed", Static(Fixtures, "get_Fixed", null, 0).PrimitiveValue()); + Static(Fixtures, "set_Counter", w => { w.WriteByte(0x03); w.WriteI32(4); }); + Assert.Equal(4, InteropFixtures.Counter); + } + + [Fact] + public void ReadOnlyField_RefusesAssignment() + { + var h = Construct(typeof(FieldHolder).FullName!); + Assert.Throws(() => Instance(h, "set_Fixed", w => { w.WriteByte(0x03); w.WriteI32(2); })); + Instance(h, "set_Value", w => { w.WriteByte(0x03); w.WriteI32(9); }); + Assert.Equal(9, Instance(h, "get_Value", null, 0).PrimitiveValue()); + } + + [Fact] + public void MembersList_IncludesFieldsAsProperties() + { + var result = Members(typeof(FieldHolder).FullName!); + var buf = new ArrayBufferWriter(); + result.WriteAsBin(buf); + var text = System.Text.Encoding.UTF8.GetString(buf.WrittenSpan); + Assert.Contains("Value", text); + Assert.Contains("Fixed", text); + Assert.Contains("nestedTypes", text); + } + + [Fact] + public void OptionalParameters_AreFilledIn() + { + Assert.Equal("aB3", Static(Fixtures, "Optional", w => { w.WriteByte(0x05); w.WriteString16("a"); }).PrimitiveValue()); + Assert.Equal("ax3", Static(Fixtures, "Optional", w => { w.WriteByte(0x05); w.WriteString16("a"); w.WriteByte(0x05); w.WriteString16("x"); }, 2).PrimitiveValue()); + } + + [Fact] + public void OmittedOutParameters_ComeBackWithTheResult() + { + var result = Static(Fixtures, "TryDouble", w => { w.WriteByte(0x05); w.WriteString16("21"); }); + Assert.Equal(new object?[] { true, 42 }, Items(result)); + } + + [Fact] + public void ByRefCall_ReturnsTheArgumentsAfterTheCall() + { + var result = Static(Fixtures, "&Swap", w => { w.WriteByte(0x03); w.WriteI32(1); w.WriteByte(0x03); w.WriteI32(2); }, 2); + Assert.Equal(new object?[] { null, 2, 1 }, Items(result)); + } + + [Fact] + public void NamedArguments_GoToTheirParameters() + { + var result = Static(Fixtures, "Point#y", w => { w.WriteByte(0x03); w.WriteI32(5); }); + Assert.Equal("(0,5)", result.PrimitiveValue()); + } + + [Fact] + public void GenericMethods_InferOrTakeTypeArguments() + { + Assert.Equal("same", Static(Fixtures, "Identity", w => { w.WriteByte(0x05); w.WriteString16("same"); }).PrimitiveValue()); + Assert.Equal("Guid", Static(Fixtures, "NameOf", null, 0).PrimitiveValue()); + var count = Static(Fixtures, "CountOf", w => + { + w.WriteByte(0x07); w.WriteU32(3); + for (int i = 0; i < 3; i++) { w.WriteByte(0x03); w.WriteI32(i); } + }); + Assert.Equal(3, count.PrimitiveValue()); + } + + [Fact] + public void ExtensionMethods_AreCalledOnInstances() + { + var h = Construct(typeof(FieldHolder).FullName!); + Assert.Equal(14, Instance(h, "Doubled", null, 0).PrimitiveValue()); + } + + [Fact] + public void Int64Arguments_AndBigResults() + { + var result = Static(Fixtures, "Echo", w => { w.WriteByte(0x10); w.WriteI64(9007199254740993L); }); + var buf = new ArrayBufferWriter(); + result.WriteAsBin(buf); + Assert.Equal(0x10, buf.WrittenSpan[0]); + Assert.Equal(9007199254740993L, BitConverter.ToInt64(buf.WrittenSpan[1..])); + + var small = Static(Fixtures, "Echo", w => { w.WriteByte(0x10); w.WriteI64(5); }); + buf.ResetWrittenCount(); + small.WriteAsBin(buf); + Assert.Equal(0x04, buf.WrittenSpan[0]); + } + + [Fact] + public void TypedArrays_BecomeArraysAndStreams() + { + Assert.Equal(6, Static(Fixtures, "Sum", w => Bytes(w, 0, [1, 2, 3])).PrimitiveValue()); + Assert.Equal(3, Static(Fixtures, "SumInts", w => Bytes(w, 4, BitConverter.GetBytes(1).Concat(BitConverter.GetBytes(2)).ToArray())).PrimitiveValue()); + Assert.Equal(4L, Static(Fixtures, "StreamLength", w => Bytes(w, 0, [1, 2, 3, 4])).PrimitiveValue()); + } + + [Fact] + public void ByteArrayResults_AreBinary() + { + var result = Static("System.Convert", "FromBase64String", w => { w.WriteByte(0x05); w.WriteString16("AQL/"); }); + var buf = new ArrayBufferWriter(); + result.WriteAsBin(buf); + Assert.Equal(new byte[] { 0x12, 0, 3, 0, 0, 0, 1, 2, 255 }, buf.WrittenSpan.ToArray()); + } + + [Fact] + public void NestedAndGenericTypeNames_Resolve() + { + Assert.Equal("nested", Static(Fixtures + ".Nested", "Where", null, 0).PrimitiveValue()); + var list = Construct("System.Collections.Generic.List`1[System.String]"); + Assert.IsType>(Bridge.s_handles[list]); + var dict = Construct("System.Collections.Generic.Dictionary`2[System.String,DotNetBridgeTests.FieldHolder]"); + Assert.IsType>(Bridge.s_handles[dict]); + } + + [Fact] + public void CallNames_Parse() + { + var plain = Bridge.CallName.Parse("Add"); + Assert.False(plain.WantsByRef); + Assert.Null(plain.TypeArgs); + var full = Bridge.CallName.Parse("&Convert#a,b"); + Assert.True(full.WantsByRef); + Assert.Equal("Convert", full.Name); + Assert.Equal(new[] { "System.Collections.Generic.List`1[System.Int32]", "System.String" }, full.TypeArgs); + Assert.Equal(new[] { "a", "b" }, full.Named); + } + + private static object?[] Items(DispatchResult r) => ((System.Collections.IEnumerable)r.PrimitiveValue()!).Cast().ToArray(); + + private static void Bytes(BinWriter w, byte kind, byte[] data) + { + w.WriteByte(0x12); + w.WriteByte(kind); + w.WriteU32((uint)data.Length); + w.WriteBytes(data); + } + + private static DispatchResult Static(string type, string method, Action? args, int argCount = 1) + { + var buf = new ArrayBufferWriter(64); + var w = new BinWriter(buf); + w.WriteByte(0x02); + w.WriteString16(type); + w.WriteString16(""); + w.WriteString16(method); + w.WriteByte((byte)argCount); + args?.Invoke(w); + var r = new BinReader(buf.WrittenSpan); + return Bridge.DispatchBin(ref r); + } + + private static int Construct(string type) + { + var buf = new ArrayBufferWriter(64); + var w = new BinWriter(buf); + w.WriteByte(0x03); + w.WriteString16(type); + w.WriteString16(""); + w.WriteByte(0); + var r = new BinReader(buf.WrittenSpan); + return Bridge.DispatchBin(ref r).HandleId(); + } + + private static DispatchResult Members(string type) + { + var buf = new ArrayBufferWriter(64); + var w = new BinWriter(buf); + w.WriteByte(0x06); + w.WriteString16(type); + w.WriteString16(""); + var r = new BinReader(buf.WrittenSpan); + return Bridge.DispatchBin(ref r); + } + + private static DispatchResult Instance(int handle, string method, Action? args, int argCount = 1) + { + var buf = new ArrayBufferWriter(64); + var w = new BinWriter(buf); + w.WriteByte(0x01); + w.WriteI32(handle); + w.WriteString16(method); + w.WriteByte((byte)argCount); + args?.Invoke(w); + var r = new BinReader(buf.WrittenSpan); + return Bridge.DispatchBin(ref r); + } + + private static void Release(int handle, int? count) + { + var buf = new ArrayBufferWriter(16); + var w = new BinWriter(buf); + w.WriteByte(0x04); + w.WriteI32(handle); + if (count is { } n) w.WriteI32(n); + var r = new BinReader(buf.WrittenSpan); + Bridge.DispatchBin(ref r); + } +} diff --git a/dotnet-bridge/BinaryProtocol.cs b/dotnet-bridge/BinaryProtocol.cs index 83e34be..f32e2b4 100644 --- a/dotnet-bridge/BinaryProtocol.cs +++ b/dotnet-bridge/BinaryProtocol.cs @@ -19,7 +19,7 @@ internal readonly struct WinRtRef(long ptr) // A JS function passed where a delegate is expected (tag 0x0C): the runtime's callback id, wrapped // in a delegate of the parameter's type when the call's arguments are coerced. -internal readonly struct JsFunctionRef(int id) +public readonly struct JsFunctionRef(int id) { public readonly int Id = id; } @@ -140,9 +140,21 @@ public uint ReadU32() case 0x0A: return new WinRtRef(ReadI64()); case 0x0C: return new JsFunctionRef(ReadI32()); case 0x0D: return new JsJsonValue(ReadString32()); + case 0x10: return ReadI64(); + case 0x11: return unchecked((ulong)ReadI64()); + case 0x12: return ReadTypedArray(); default: return null; } } + + public JsTypedArray ReadTypedArray() + { + var kind = ReadByte(); + var len = (int)ReadU32(); + var bytes = _buf.Slice(_pos, len).ToArray(); + _pos += len; + return new JsTypedArray(kind, bytes); + } } // Decoded names by their UTF-8 bytes. Per thread and direct-mapped: a lookup takes no lock and a @@ -224,6 +236,12 @@ public void WriteString32(ReadOnlySpan s) _buf.Advance(byteCount); } + public void WriteBytes(ReadOnlySpan bytes) + { + bytes.CopyTo(_buf.GetSpan(bytes.Length)); + _buf.Advance(bytes.Length); + } + public void WriteString16(ReadOnlySpan s) { var byteCount = Encoding.UTF8.GetByteCount(s); diff --git a/dotnet-bridge/Bridge.Binary.cs b/dotnet-bridge/Bridge.Binary.cs new file mode 100644 index 0000000..77991a3 --- /dev/null +++ b/dotnet-bridge/Bridge.Binary.cs @@ -0,0 +1,154 @@ +using System; +using System.Buffers; +using System.IO; +using System.Runtime.InteropServices; + +namespace NativeScriptBridge; + +// A JS typed array, ArrayBuffer or DataView (tag 0x12). Kinds: 0 bytes (Uint8Array, +// Uint8ClampedArray, ArrayBuffer, DataView), 1 Int8, 2 Int16, 3 Uint16, 4 Int32, 5 Uint32, +// 6 Float32, 7 Float64, 8 BigInt64, 9 BigUint64. +internal sealed class JsTypedArray(byte kind, byte[] bytes) +{ + public readonly byte Kind = kind; + public readonly byte[] Bytes = bytes; + + public Type ElementType => ElementTypeOf(Kind); + + public static Type ElementTypeOf(byte kind) => kind switch + { + 1 => typeof(sbyte), + 2 => typeof(short), + 3 => typeof(ushort), + 4 => typeof(int), + 5 => typeof(uint), + 6 => typeof(float), + 7 => typeof(double), + 8 => typeof(long), + 9 => typeof(ulong), + _ => typeof(byte), + }; + + // The typed-array kind for a .NET array element type, or -1 when JS has none. + public static int KindOf(Type element) => + element == typeof(byte) ? 0 : element == typeof(sbyte) ? 1 : element == typeof(short) ? 2 + : element == typeof(ushort) ? 3 : element == typeof(int) ? 4 : element == typeof(uint) ? 5 + : element == typeof(float) ? 6 : element == typeof(double) ? 7 : element == typeof(long) ? 8 + : element == typeof(ulong) ? 9 : -1; + + // The items as an array of their own element type. + public Array ToNativeArray() + { + var element = ElementType; + if (element == typeof(byte)) return Bytes; + var size = Marshal.SizeOf(element); + var array = Array.CreateInstance(element, Bytes.Length / size); + Buffer.BlockCopy(Bytes, 0, array, 0, array.Length * size); + return array; + } + + // The items as an array of `element`, converted when the types differ. + public Array ToArray(Type element) + { + var native = ToNativeArray(); + if (native.GetType().GetElementType() == element) return native; + var array = Array.CreateInstance(element, native.Length); + for (int i = 0; i < native.Length; i++) + array.SetValue(Convert.ChangeType(native.GetValue(i), element), i); + return array; + } +} + +public static partial class Bridge +{ + private const double MaxSafeInteger = 9007199254740991; + + // How well a typed array fits a parameter type (see MatchScore). + private static int TypedArrayScore(JsTypedArray t, Type p) + { + var element = t.ElementType; + if (p.IsArray) + { + var pe = p.GetElementType()!; + if (pe == element) return 10; + if (pe == typeof(byte)) return 4; + return JsTypedArray.KindOf(pe) >= 0 ? 5 : pe == typeof(object) ? 2 : -1; + } + if (p.IsGenericType) + { + var definition = p.GetGenericTypeDefinition(); + var arg = p.GetGenericArguments()[0]; + if (definition == typeof(Memory<>) || definition == typeof(ReadOnlyMemory<>) || definition == typeof(ArraySegment<>)) + return arg == element ? 9 : arg == typeof(byte) ? 4 : -1; + } + if (element == typeof(byte) && p.IsAssignableFrom(typeof(MemoryStream))) return p == typeof(object) ? 1 : 5; + if (p.IsAssignableFrom(element.MakeArrayType())) return p == typeof(object) ? 1 : 7; + return -1; + } + + private static object? CoerceTypedArray(JsTypedArray t, Type targetType) + { + var p = Nullable.GetUnderlyingType(targetType) ?? targetType; + if (p.IsArray) + { + var pe = p.GetElementType()!; + return pe == typeof(byte) ? t.Bytes : pe == typeof(object) ? ToObjects(t.ToNativeArray()) : t.ToArray(pe); + } + if (p.IsGenericType) + { + var definition = p.GetGenericTypeDefinition(); + if (definition == typeof(Memory<>) || definition == typeof(ReadOnlyMemory<>) || definition == typeof(ArraySegment<>)) + { + var arg = p.GetGenericArguments()[0]; + return Activator.CreateInstance(p, arg == typeof(byte) ? t.Bytes : t.ToArray(arg)); + } + } + if (t.ElementType == typeof(byte) && p.IsAssignableFrom(typeof(MemoryStream)) && p != typeof(object)) + return new MemoryStream(t.Bytes, writable: true); + return t.ToNativeArray(); + } + + private static object?[] ToObjects(Array array) + { + var objects = new object?[array.Length]; + for (int i = 0; i < objects.Length; i++) objects[i] = array.GetValue(i); + return objects; + } + + // Values JS gets as a typed array: byte[], sbyte[], Memory, ArraySegment. + internal static bool TryGetBytes(object value, out ReadOnlySpan bytes, out byte kind) + { + switch (value) + { + case byte[] b: bytes = b; kind = 0; return true; + case sbyte[] sb: bytes = MemoryMarshal.AsBytes(sb.AsSpan()); kind = 1; return true; + case Memory m: bytes = m.Span; kind = 0; return true; + case ReadOnlyMemory rm: bytes = rm.Span; kind = 0; return true; + case ArraySegment seg: bytes = seg.AsSpan(); kind = 0; return true; + } + bytes = default; + kind = 0; + return false; + } + + internal static void WriteTypedArray(ref BinWriter w, ReadOnlySpan bytes, byte kind) + { + w.WriteByte(0x12); + w.WriteByte(kind); + w.WriteU32((uint)bytes.Length); + w.WriteBytes(bytes); + } + + // A number when the value fits a double exactly, else a BigInt (tags 0x10 / 0x11). + internal static void WriteInt64(ref BinWriter w, long v) + { + if (v >= -MaxSafeInteger && v <= MaxSafeInteger) { w.WriteByte(0x04); w.WriteF64(v); } + else { w.WriteByte(0x10); w.WriteI64(v); } + } + + internal static void WriteUInt64(ref BinWriter w, ulong v) + { + if (v <= (ulong)MaxSafeInteger) { w.WriteByte(0x04); w.WriteF64(v); } + else { w.WriteByte(0x11); w.WriteI64(unchecked((long)v)); } + } +} diff --git a/dotnet-bridge/Bridge.BinaryDispatch.cs b/dotnet-bridge/Bridge.BinaryDispatch.cs index 4596468..85aabd0 100644 --- a/dotnet-bridge/Bridge.BinaryDispatch.cs +++ b/dotnet-bridge/Bridge.BinaryDispatch.cs @@ -31,7 +31,7 @@ public static unsafe int InvokeBinary( } catch (Exception ex) { - WriteBinError(Unwrap(ex).Message, responsePtr, responseLenPtr); + WriteBinError(Unwrap(ex), responsePtr, responseLenPtr); } return 0; } @@ -73,23 +73,12 @@ internal static DispatchResult DispatchBin(ref BinReader r) { var op = r.ReadByte(); - if (op == 0x04) // release - { - var handleToRemove = r.ReadI32(); - s_handles.TryRemove(handleToRemove, out _); - if (s_nativePtrs.TryRemove(handleToRemove, out var nativePtr)) - { - RemoveNativePtr(handleToRemove, nativePtr); - try - { - Marshal.Release(nativePtr); - } - catch - { - } - } - return DispatchResult.Void; - } + if (op == 0x04) // release: handle, then the number of exports given back (all when absent) + { + var handleToRemove = r.ReadI32(); + ReleaseHandle(handleToRemove, r.HasMore ? r.ReadI32() : -1); + return DispatchResult.Void; + } if (op == 0x05) // members by handle { @@ -180,6 +169,14 @@ internal static DispatchResult DispatchBin(ref BinReader r) // Constructors with the same parameter count (StringBuilder(int) / StringBuilder(string)): // the one whose parameter types best fit the arguments. var ctors = GetCtorOverloads(typeS, args.Length); + if (ctors.Ctors.Length == 0 || SelectOverload(ctors.Parameters, args) < 0) + { + // A struct's implicit parameterless constructor, or one with optional parameters. + if (args.Length == 0 && typeS.IsValueType) return Box(DefaultOf(typeS)); + var ctorCall = new CallName(".ctor", false, null, null); + if (TryBindCall(SlowCandidates(typeS, ".ctor", BindingFlags.Public | BindingFlags.Instance), null, args, ctorCall, out var bound, out var boundArgs, out var info)) + return InvokeBound(bound!, null, boundArgs!, info, ctorCall); + } if (ctors.Ctors.Length > 1) { var best = SelectOverload(ctors.Parameters, args); @@ -193,7 +190,7 @@ internal static DispatchResult DispatchBin(ref BinReader r) return Box(ctors.Ctors[best].Invoke(built)); } var entry = GetCachedCtor(typeS, args.Length); - if (entry.Ctor is null) + if (entry.Ctor is null || entry.Parameters.Length != args.Length) throw new MissingMethodException( $"No public ctor on {typeS.FullName} for {args.Length} args"); // ConstructorInfo.Invoke requires exact arg count — build a precise array. @@ -208,11 +205,17 @@ internal static DispatchResult DispatchBin(ref BinReader r) } private static DispatchResult DispatchCallBin( - object? target, Type type, string method, object?[] args, bool isStatic) + object? target, Type type, string rawMethod, object?[] args, bool isStatic) { var flags = (isStatic ? BindingFlags.Static : BindingFlags.Instance) | BindingFlags.Public; - - + var call = CallName.Parse(rawMethod); + var method = call.Name; + // `&`, `<...>` and `#...` calls (see CallName) only go through the slow path. + if (!ReferenceEquals(method, rawMethod)) + { + if (TrySlowCall(target, type, call, args, flags, out var special)) return special; + throw new MissingMethodException($"No method '{method}' on {type.FullName} takes these {args.Length} argument(s)"); + } if (method.Length > 4 && args.Length == 0 && method[0] == 'g' && method[1] == 'e' && method[2] == 't' && method[3] == '_') @@ -237,10 +240,13 @@ private static DispatchResult DispatchCallBin( // Describe(Shape)): the one whose parameter types best fit the arguments. var overloads = GetOverloads(type, method, args.Length, flags); var best = overloads.Methods.Length > 0 ? overloads.Select(args) : -1; - // No method with this many parameters takes these arguments: a params method may - // (String.Join(",", "a", "b", "c")). - if (best < 0 && TryParamsCall(target, type, method, args, flags, out var paramsResult)) - return paramsResult; + // No overload of this arity fits: try params methods (String.Join(",", "a", "b")), then + // optional parameters, generic methods and extension methods. + if (best < 0) + { + if (TryParamsCall(target, type, method, args, flags, out var paramsResult)) return paramsResult; + if (TrySlowCall(target, type, call, args, flags, out var slowResult)) return slowResult; + } if (overloads.Methods.Length > 1) { if (best >= 0) @@ -269,59 +275,30 @@ private static DispatchResult DispatchCallBin( } var entry = GetCachedMethod(type, method, args.Length, flags); - if (entry.Invoke is null && target is not null && IsJsBacked(target)) - { - // A JS subclass may call the protected members it inherits. - var protectedFlags = BindingFlags.Instance | BindingFlags.NonPublic; - var inherited = type.GetMethods(protectedFlags) - .FirstOrDefault(m => m.Name == method && (m.IsFamily || m.IsFamilyOrAssembly) && m.GetParameters().Length == args.Length); - if (inherited is not null) - { - var built = BuildArgsBinExact(args, inherited.GetParameters()); - return Box(inherited.Invoke(target, built)); - } - } if (entry.Invoke is null) - { - var candidates = type.GetMethods(flags).Where(m => m.Name == method && !m.IsSpecialName); - foreach (var m in candidates) - { - var parameters = m.GetParameters(); - var built = BuildArgsBin(args, parameters); - try - { - var res = (m.Invoke(target, built)); - return Box(res); - } - catch (TargetInvocationException tie) when (IsMarshaledForDifferentThread(tie.InnerException)) - { - if (Bridge.IsLogToConsole()) Console.Error.WriteLine($"[Bridge] Detected wrong-thread COM error; retrying {type.FullName}.{m.Name} on UI thread"); - try - { - var res = InvokeOnUIThread(() => (m.Invoke(target, built))); - return Box(res); - } - catch { /* retry failed — try next candidate */ } - } - catch (System.Runtime.InteropServices.COMException ce) when (IsMarshaledForDifferentThread(ce)) - { - if (Bridge.IsLogToConsole()) Console.Error.WriteLine($"[Bridge] Detected COMException wrong-thread; retrying {type.FullName}.{m.Name} on UI thread"); - try - { - var res = InvokeOnUIThread(() => (m.Invoke(target, built))); - return Box(res); - } - catch { /* retry failed — try next candidate */ } - } - finally { if (built.Length > 0) ReturnArgs(built); } - } throw new MissingMethodException( $"Method '{method}' ({args.Length} args) not found on {type.FullName}"); - } return InvokeBuilt(target, type, method, entry, BuildArgsBin(args, entry.Parameters)); } + // Methods the overload cache skips: other arities, generic definitions, protected members (for + // JS subclasses) and extension methods. + private static bool TrySlowCall(object? target, Type type, CallName call, object?[] args, BindingFlags flags, out DispatchResult result) + { + result = default; + IEnumerable candidates = SlowCandidates(type, call.Name, flags); + if (target is not null && IsJsBacked(target)) + candidates = candidates.Concat(SlowCandidates(type, call.Name, BindingFlags.Instance | BindingFlags.NonPublic) + .Where(m => m.IsFamily || m.IsFamilyOrAssembly)); + if (TryBindCall(candidates, target, args, call, out var method, out var built, out var info)) + { + result = InvokeBound(method!, target, built!, info, call); + return true; + } + return target is not null && TryExtensionCall(target, args, call, out result); + } + // Runs a compiled invoker on arguments from BuildArgsBin (returned to the pool afterwards), // retrying on the UI thread when a COM object rejects the calling thread. private static DispatchResult InvokeBuilt(object? target, Type type, string method, DispatchEntry entry, object?[] builtArgs) @@ -449,12 +426,33 @@ private static int MatchScore(object? arg, Type p) if (obj.GetType() == p) return 10; return p.IsInstanceOfType(obj) ? (p == typeof(object) ? 2 : 8) : -1; case JsFunctionRef: + if (p == typeof(JsFunctionRef)) return 10; if (typeof(Delegate).IsAssignableFrom(p)) return p == typeof(Delegate) || p == typeof(MulticastDelegate) ? 4 : 8; return p == typeof(object) ? 1 : -1; case WinRtRef: if (p == typeof(object)) return 2; return p.IsInterface || (p.IsClass && p != typeof(string)) ? 4 : -1; + case long l: + if (p == typeof(long)) return 10; + if (p == typeof(ulong)) return l >= 0 ? 9 : -1; + if (p == typeof(decimal)) return 8; + if (p == typeof(double)) return 7; + if (p == typeof(float)) return 6; + if (p.IsEnum) return 5; + if (IsIntegerType(p)) return FitsInteger(l, p) ? 4 : -1; + return p.IsInstanceOfType(arg) ? 2 : -1; + case ulong u: + if (p == typeof(ulong)) return 10; + if (p == typeof(long)) return u <= long.MaxValue ? 9 : -1; + if (p == typeof(decimal)) return 8; + if (p == typeof(double)) return 7; + if (p == typeof(float)) return 6; + if (p.IsEnum) return 5; + if (IsIntegerType(p)) return FitsInteger(u, p) ? 4 : -1; + return p.IsInstanceOfType(arg) ? 2 : -1; + case JsTypedArray typed: + return TypedArrayScore(typed, p); case JsJsonValue: if (p == typeof(object)) return 1; if (p == typeof(string)) return 2; @@ -629,10 +627,13 @@ TypeCode.SByte or TypeCode.Byte or TypeCode.Int16 or TypeCode.UInt16 or } if (value is JsArrayValue array) return CoerceArray(array.Items, targetType); + if (value is JsTypedArray typed) + return CoerceTypedArray(typed, targetType); if (value is string text && IsParsedFromString(Nullable.GetUnderlyingType(targetType) ?? targetType)) return ParseString(text, Nullable.GetUnderlyingType(targetType) ?? targetType); if (value is JsFunctionRef fn) { + if ((Nullable.GetUnderlyingType(targetType) ?? targetType) == typeof(JsFunctionRef)) return fn; var delegateType = typeof(Delegate).IsAssignableFrom(targetType) && targetType != typeof(Delegate) && targetType != typeof(MulticastDelegate) ? targetType @@ -741,14 +742,4 @@ private static DispatchResult ClrGetProperty(IntPtr instancePtr, string propName } private static string? NullIfEmpty(string s) => s.Length == 0 ? null : s; - - private static unsafe void WriteBinError(string msg, byte** outPtr, int* outLen) - { - var msgBytes = System.Text.Encoding.UTF8.GetByteCount(msg); - var buf = new ArrayBufferWriter(5 + msgBytes); - var w = new BinWriter(buf); - w.WriteByte(0xFF); - w.WriteString32(msg); - WriteUnmanaged(buf.WrittenSpan, outPtr, outLen); - } } diff --git a/dotnet-bridge/Bridge.Binder.cs b/dotnet-bridge/Bridge.Binder.cs new file mode 100644 index 0000000..1b772c8 --- /dev/null +++ b/dotnet-bridge/Bridge.Binder.cs @@ -0,0 +1,389 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace NativeScriptBridge; + +// Calls the same-arity overload cache can't make: optional, named and ref/out parameters, generic +// methods and extension methods. Also resolves closed generic type names. +public static partial class Bridge +{ + // A member name as JS sends it, `&Name#a,b`: `&` returns ref/out values, `<...>` holds + // type arguments, `#...` names the trailing arguments. + internal readonly record struct CallName(string Name, bool WantsByRef, string[]? TypeArgs, string[]? Named) + { + public static CallName Parse(string raw) + { + if (raw.Length == 0 || (raw[0] != '&' && raw.IndexOf('#') < 0 && raw[^1] != '>')) + return new CallName(raw, false, null, null); + var byRef = raw[0] == '&'; + var name = byRef ? raw[1..] : raw; + string[]? named = null; + var hash = name.LastIndexOf('#'); + if (hash >= 0) + { + named = name[(hash + 1)..].Split(',', StringSplitOptions.RemoveEmptyEntries); + name = name[..hash]; + } + string[]? typeArgs = null; + var lt = name.IndexOf('<'); + if (lt > 0 && name[^1] == '>') + { + typeArgs = SplitTypeList(name[(lt + 1)..^1]); + name = name[..lt]; + } + return new CallName(name, byRef, typeArgs, named); + } + } + + // Comma-separated type names, ignoring commas inside brackets. + internal static string[] SplitTypeList(string list) + { + var parts = new List(); + int depth = 0, start = 0; + for (int i = 0; i < list.Length; i++) + { + var c = list[i]; + if (c is '[' or '<') depth++; + else if (c is ']' or '>') depth--; + else if (c == ',' && depth == 0) + { + parts.Add(list[start..i].Trim()); + start = i + 1; + } + } + if (start < list.Length) parts.Add(list[start..].Trim()); + return parts.ToArray(); + } + + private static Type? ResolveTypeArg(string name) + { + name = name.Trim(); + if (name.Length > 1 && name[0] == '[' && name[^1] == ']') name = name[1..^1]; + return ResolveType(null, name); + } + + // `List`1[System.String]` (or `List[System.String]`): the closed generic type. + private static Type? ResolveGenericType(string? assemblyName, string typeName) + { + var open = typeName.IndexOf('['); + if (open <= 0 || typeName[^1] != ']') return null; + var argNames = SplitTypeList(typeName[(open + 1)..^1]); + if (argNames.Length == 0) return null; + var args = new Type[argNames.Length]; + for (int i = 0; i < args.Length; i++) + if ((args[i] = ResolveTypeArg(argNames[i])!) is null) return null; + var definitionName = typeName[..open]; + if (definitionName.IndexOf('`') < 0) definitionName += "`" + args.Length; + var definition = ResolveType(assemblyName, definitionName); + if (definition is null || !definition.IsGenericTypeDefinition) return null; + try { return definition.MakeGenericType(args); } + catch (ArgumentException e) { throw new ArgumentException($"{definition.FullName} can't take those type arguments: {e.Message}", e); } + } + + // Picks the best-fitting candidate and builds its argument array; false when none fits. + private static bool TryBindCall(IEnumerable candidates, object? target, object?[] args, CallName call, out MethodBase? method, out object?[]? built, out BindInfo info) + { + method = null; + built = null; + info = default; + int bestScore = int.MinValue; + var namedCount = call.Named?.Length ?? 0; + var positional = args.Length - namedCount; + if (positional < 0) return false; + + foreach (var candidate in candidates) + { + var m = candidate; + if (m is MethodInfo { IsGenericMethodDefinition: true } generic) + { + m = call.TypeArgs is { } names ? CloseGenericMethod(generic, names) : InferGenericMethod(generic, args, positional); + if (m is null) continue; + } + else if (call.TypeArgs is not null) + { + continue; + } + + var ps = m.GetParameters(); + if (positional > ps.Length) continue; + var slots = new int[ps.Length]; + Array.Fill(slots, -1); + for (int i = 0; i < positional; i++) slots[i] = i; + var ok = true; + for (int n = 0; n < namedCount && ok; n++) + { + var index = Array.FindIndex(ps, p => string.Equals(p.Name, call.Named![n], StringComparison.OrdinalIgnoreCase)); + if (index < 0 || slots[index] >= 0) ok = false; + else slots[index] = positional + n; + } + if (!ok) continue; + + int score = 0; + var omittedOuts = new List(); + for (int i = 0; i < ps.Length && score >= 0; i++) + { + var p = ps[i]; + if (slots[i] < 0) + { + if (p.IsOut) { omittedOuts.Add(i); continue; } + if (p.HasDefaultValue || p.IsOptional || IsParamArray(p)) continue; + score = -1; + break; + } + var arg = args[slots[i]]; + var type = p.ParameterType.IsByRef ? p.ParameterType.GetElementType()! : p.ParameterType; + // JS passes null for an out parameter it only reads back. + var s = p.IsOut && arg is null ? 5 : MatchScore(arg, type); + score = s < 0 ? -1 : score + s; + } + if (score < 0) continue; + // Prefer methods that need fewer filled-in parameters. + score = score * 4 - (ps.Length - args.Length); + if (score <= bestScore) continue; + + object?[] values; + try { values = BuildBoundArgs(ps, slots, args); } + catch { continue; } + bestScore = score; + method = m; + built = values; + info = new BindInfo(omittedOuts.ToArray(), slots); + } + return method is not null; + } + + internal readonly record struct BindInfo(int[] OmittedOuts, int[] Slots); + + private static bool IsParamArray(ParameterInfo p) => + p.ParameterType.IsArray && p.IsDefined(typeof(ParamArrayAttribute), false); + + private static object?[] BuildBoundArgs(ParameterInfo[] ps, int[] slots, object?[] args) + { + var values = new object?[ps.Length]; + for (int i = 0; i < ps.Length; i++) + { + var p = ps[i]; + var type = p.ParameterType.IsByRef ? p.ParameterType.GetElementType()! : p.ParameterType; + if (slots[i] >= 0) + { + var v = CoerceBin(args[slots[i]], type); + values[i] = v is null && type.IsValueType && Nullable.GetUnderlyingType(type) is null ? DefaultOf(type) : v; + } + else if (IsParamArray(p)) + values[i] = Array.CreateInstance(type.GetElementType()!, 0); + else if (p.HasDefaultValue && p.DefaultValue is not DBNull && p.DefaultValue is not Missing) + values[i] = p.DefaultValue is null ? DefaultOf(type) : CoerceDefault(p.DefaultValue, type); + else + values[i] = DefaultOf(type); + } + return values; + } + + private static object? DefaultOf(Type t) => t.IsValueType ? RuntimeHelpers.GetUninitializedObject(t) : null; + + // Enum defaults are stored as their underlying number. + private static object? CoerceDefault(object value, Type type) + { + var underlying = Nullable.GetUnderlyingType(type) ?? type; + if (underlying.IsEnum) return Enum.ToObject(underlying, value); + return value; + } + + // `&` calls return [result, ...arguments after the call]; calls that leave out parameters off + // return [result, ...outs] (outs only for void), like WinRT methods. + private static DispatchResult InvokeBound(MethodBase method, object? target, object?[] built, BindInfo info, CallName call) + { + object? result; + try + { + result = method is ConstructorInfo ctor ? ctor.Invoke(built) : method.Invoke(target, built); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null && IsMarshaledForDifferentThread(tie.InnerException)) + { + result = InvokeOnUIThread(() => method is ConstructorInfo c ? c.Invoke(built) : method.Invoke(target, built)); + } + var isVoid = method is MethodInfo mi && mi.ReturnType == typeof(void); + if (call.WantsByRef) + { + var byPosition = new object?[info.Slots.Length > 0 ? info.Slots.Max() + 1 : 0]; + for (int i = 0; i < info.Slots.Length; i++) + if (info.Slots[i] >= 0) byPosition[info.Slots[i]] = built[i]; + return DispatchResult.Collection(new object?[] { result }.Concat(byPosition).ToArray()); + } + if (info.OmittedOuts.Length > 0) + { + var outs = info.OmittedOuts.Select(i => built[i]); + return DispatchResult.Collection((isVoid ? outs : new[] { result }.Concat(outs)).ToArray()); + } + return Box(result); + } + + private static MethodInfo? CloseGenericMethod(MethodInfo definition, string[] typeArgNames) + { + if (definition.GetGenericArguments().Length != typeArgNames.Length) return null; + var types = new Type[typeArgNames.Length]; + for (int i = 0; i < types.Length; i++) + if ((types[i] = ResolveTypeArg(typeArgNames[i])!) is null) return null; + try { return definition.MakeGenericMethod(types); } + catch (ArgumentException) { return null; } + } + + // Infers type arguments from the arguments as C# does (ToList(list) of a List is + // ToList). Ones the arguments leave open, such as a JS callback's TResult, are object. + private static MethodInfo? InferGenericMethod(MethodInfo definition, object?[] args, int positional) + { + var genericArgs = definition.GetGenericArguments(); + var bound = new Type?[genericArgs.Length]; + var ps = definition.GetParameters(); + for (int i = 0; i < Math.Min(positional, ps.Length); i++) + { + var argType = BridgedType(args[i]); + if (argType is null) continue; + var p = ps[i].ParameterType; + Unify(p.IsByRef ? p.GetElementType()! : p, argType, genericArgs, bound); + } + var types = new Type[bound.Length]; + for (int i = 0; i < types.Length; i++) types[i] = bound[i] ?? typeof(object); + try { return definition.MakeGenericMethod(types); } + catch (ArgumentException) { return null; } + } + + // The .NET type a bridged argument stands for, when there is one to infer from. + private static Type? BridgedType(object? arg) => arg switch + { + null => null, + HandleRef h => s_handles.TryGetValue(h.Id, out var o) ? o?.GetType() : null, + JsArrayValue a => (CommonType(a.Items) ?? typeof(object)).MakeArrayType(), + JsTypedArray t => t.ElementType.MakeArrayType(), + JsFunctionRef or JsJsonValue or WinRtRef => null, + _ => arg.GetType(), + }; + + // The element type every item of a JS array shares (int, double, string, bool, a .NET type). + private static Type? CommonType(object?[] items) + { + Type? common = null; + foreach (var item in items) + { + var t = BridgedType(item); + if (t is null) continue; + if (common is null) common = t; + else if (common != t) + { + if ((common == typeof(int) && t == typeof(double)) || (common == typeof(double) && t == typeof(int))) common = typeof(double); + else return typeof(object); + } + } + return common; + } + + private static void Unify(Type parameter, Type argument, Type[] genericArgs, Type?[] bound) + { + if (parameter.IsGenericParameter) + { + var index = Array.IndexOf(genericArgs, parameter); + if (index >= 0 && bound[index] is null) bound[index] = argument; + return; + } + if (!parameter.ContainsGenericParameters) return; + if (parameter.IsArray) + { + if (argument.IsArray) Unify(parameter.GetElementType()!, argument.GetElementType()!, genericArgs, bound); + else if (CollectionElementType(argument) is { } element) Unify(parameter.GetElementType()!, element, genericArgs, bound); + return; + } + if (!parameter.IsGenericType) return; + var definition = parameter.GetGenericTypeDefinition(); + var match = FindGenericBase(argument, definition); + if (match is null && argument.IsArray && definition == typeof(IEnumerable<>)) + match = typeof(IEnumerable<>).MakeGenericType(argument.GetElementType()!); + if (match is null) return; + var pArgs = parameter.GetGenericArguments(); + var aArgs = match.GetGenericArguments(); + for (int i = 0; i < pArgs.Length; i++) Unify(pArgs[i], aArgs[i], genericArgs, bound); + } + + // `t` itself, a base class or an interface of it that is an instance of `definition`. + private static Type? FindGenericBase(Type t, Type definition) + { + for (var b = t; b is not null; b = b.BaseType) + if (b.IsGenericType && b.GetGenericTypeDefinition() == definition) return b; + if (definition.IsInterface) + foreach (var i in t.GetInterfaces()) + if (i.IsGenericType && i.GetGenericTypeDefinition() == definition) return i; + return null; + } + + // Every method (or constructor) with the call's name: any arity, generic definitions included. + private static readonly ConcurrentDictionary<(Type, string, BindingFlags), MethodBase[]> s_slowCandidates = new(); + + private static MethodBase[] SlowCandidates(Type type, string name, BindingFlags flags) => + s_slowCandidates.GetOrAdd((type, name, flags), static k => + k.Item2 == ".ctor" + ? k.Item1.GetConstructors(BindingFlags.Public | BindingFlags.Instance) + : k.Item1.GetMethods(k.Item3).Where(m => m.Name == k.Item2).ToArray()); + + // Extension methods of every loaded assembly, by name; rebuilt after an assembly loads. + private static Dictionary? s_extensionIndex; + private static int s_extensionIndexGeneration = -1; + + private static MethodInfo[] ExtensionMethods(string name) + { + var generation = Volatile.Read(ref s_assemblyGeneration); + var index = s_extensionIndex; + if (index is null || s_extensionIndexGeneration != generation) + { + var built = new Dictionary>(StringComparer.Ordinal); + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + if (asm.IsDynamic) continue; + Type[] types; + try { types = asm.GetExportedTypes(); } + catch { continue; } + foreach (var t in types) + { + if (!t.IsSealed || !t.IsAbstract || t.IsGenericType || !t.IsDefined(typeof(ExtensionAttribute), false)) continue; + foreach (var m in t.GetMethods(BindingFlags.Public | BindingFlags.Static)) + { + if (!m.IsDefined(typeof(ExtensionAttribute), false)) continue; + if (!built.TryGetValue(m.Name, out var list)) built[m.Name] = list = new List(); + list.Add(m); + } + } + } + index = built.ToDictionary(kv => kv.Key, kv => kv.Value.ToArray(), StringComparer.Ordinal); + s_extensionIndex = index; + s_extensionIndexGeneration = generation; + } + return index.TryGetValue(name, out var methods) ? methods : []; + } + + // `obj.Where(fn)` for an extension method of the object's type: Enumerable.Where(obj, fn). + private static bool TryExtensionCall(object target, object?[] args, CallName call, out DispatchResult result) + { + result = default; + var candidates = ExtensionMethods(call.Name); + if (candidates.Length == 0) return false; + var targetType = target.GetType(); + var applicable = candidates.Where(m => + { + var first = m.GetParameters()[0].ParameterType; + if (!first.ContainsGenericParameters) return first.IsInstanceOfType(target); + if (first.IsGenericParameter) return true; + return first.IsGenericType && FindGenericBase(targetType, first.GetGenericTypeDefinition()) is not null; + }).ToArray(); + if (applicable.Length == 0) return false; + var withTarget = new object?[args.Length + 1]; + withTarget[0] = target; + Array.Copy(args, 0, withTarget, 1, args.Length); + // The target is the extension method's first argument. + if (!TryBindCall(applicable, null, withTarget, call, out var method, out var built, out var info)) return false; + result = InvokeBound(method!, null, built!, info, call with { WantsByRef = false }); + return true; + } +} diff --git a/dotnet-bridge/Bridge.Dispatch.cs b/dotnet-bridge/Bridge.Dispatch.cs index c7d9fdc..14ba54f 100644 --- a/dotnet-bridge/Bridge.Dispatch.cs +++ b/dotnet-bridge/Bridge.Dispatch.cs @@ -20,24 +20,11 @@ internal static DispatchResult Dispatch(InvokeRequest req) { var method = req.Method ?? throw new ArgumentException("Method is required"); - if (method == "__release" && req.Handle.HasValue) - { - var id = req.Handle.Value; - s_handles.TryRemove(id, out _); - if (s_nativePtrs.TryRemove(id, out var nativePtr)) - { - RemoveNativePtr(id, nativePtr); - try - { - Marshal.Release(nativePtr); - } - catch - { - } - } - - return DispatchResult.Void; - } + if (method == "__release" && req.Handle.HasValue) + { + ReleaseHandle(req.Handle.Value, -1); + return DispatchResult.Void; + } object? target = null; Type type; @@ -112,7 +99,11 @@ internal static DispatchResult Dispatch(InvokeRequest req) foreach (var m in candidates) { var parameters = m.GetParameters(); - var built = BuildArgs(args, parameters); + // MethodInfo.Invoke takes exactly as many arguments as the method has parameters. + if (parameters.Length != args.Length) continue; + object?[] built; + try { built = BuildArgsExact(args, parameters); } + catch { continue; } try { var res = (m.Invoke(target, built)); @@ -126,7 +117,7 @@ internal static DispatchResult Dispatch(InvokeRequest req) var res = InvokeOnUIThread(() => (m.Invoke(target, built))); return Box(res); } - catch { /* retry failed — try next candidate */ } + catch { /* retry failed: try the next candidate */ } } catch (System.Runtime.InteropServices.COMException ce) when (IsMarshaledForDifferentThread(ce)) { @@ -136,9 +127,8 @@ internal static DispatchResult Dispatch(InvokeRequest req) var res = InvokeOnUIThread(() => (m.Invoke(target, built))); return Box(res); } - catch { /* retry failed — try next candidate */ } + catch { /* retry failed: try the next candidate */ } } - finally { if (built.Length > 0) ReturnArgs(built); } } throw new MissingMethodException( $"Method '{method}' ({args.Length} args) not found on {type.FullName}"); @@ -227,10 +217,10 @@ private static CtorEntry GetCachedCtor(Type type, int argCount) // Keyed by the accessor name as called ("get_Length") plus the prefix length, so a warm lookup // allocates no property-name substring. - private static PropertyInfo? GetCachedProp(Type type, string method, int prefixLen, BindingFlags flags) + private static DataMember? GetCachedProp(Type type, string method, int prefixLen, BindingFlags flags) => s_propCache.GetOrAdd( new PropKey(type, method, prefixLen, flags), - static k => k.Type.GetProperty(k.Name[k.PrefixLen..], k.Flags)); + static k => DataMember.Find(k.Type, k.Name[k.PrefixLen..], k.Flags)); // Pooled: rented array passed to the compiled delegate (which accesses by index, // not by Length). Caller must return via ReturnArgs immediately after invoke. @@ -278,6 +268,8 @@ private static DispatchResult Box(object? value) if (value is UIntPtr up) return DispatchResult.Primitive(unchecked((long)up.ToUInt64()), typeof(long)); + if (TryGetBytes(value, out _, out _)) return DispatchResult.Bytes(value); + var t = value.GetType(); var info = GetBoxInfo(t); @@ -302,9 +294,8 @@ private static DispatchResult Box(object? value) - var id = Interlocked.Increment(ref s_nextHandle); - s_handles[id] = value; - if (jsHolder is not null) jsHolder.HandleId = id; + var id = HandleFor(value); + if (jsHolder is not null) jsHolder.HandleId = id; // Try to obtain a canonical IInspectable/IUnknown pointer for COM/WinRT objects // so the runtime can call native vtable methods directly. @@ -317,7 +308,7 @@ private static DispatchResult Box(object? value) // (GetNativePtrForHandle): making a COM wrapper costs more than the rest of the call. try { - if (info.WinRTNativeObject is not null || jsHolder is not null) + if ((info.WinRTNativeObject is not null || jsHolder is not null) && !s_nativePtrs.ContainsKey(id)) { var p = ObtainNativePtr(value); if (p != IntPtr.Zero) @@ -401,12 +392,30 @@ public static string TypeKind(string typeName) } // What a .NET collection supports, for the JS side to make it array-like: 1 = enumerable - // (for...of, map, ...), 2 = has a count (length), 4 = indexed by position (list[0]). + // (for...of, map, ...), 2 = has a count (length), 4 = indexed by position (list[0]), + // 8 = keyed by string (dict.key / dict['key']). public static int CollectionKind(object? value) { if (value is null or string || value is not System.Collections.IEnumerable) return 0; var info = GetCollectionInfo(value.GetType()); - return 1 | (info.Count is not null ? 2 : 0) | (info.Indexer is not null ? 4 : 0); + return 1 | (info.Count is not null ? 2 : 0) | (info.Indexer is not null ? 4 : 0) | (info.KeyIndexer is not null ? 8 : 0); + } + + // [found, value] for `key` of a string-keyed dictionary. + public static object?[] DictionaryTryGet(object? value, string key) + { + var info = value is null ? null : GetCollectionInfo(value.GetType()); + if (info?.KeyIndexer is null || info.ContainsKey is null) return [false, null]; + if (info.ContainsKey.Invoke(value, [key]) is not true) return [false, null]; + return [true, info.KeyIndexer.GetValue(value, [key])]; + } + + public static void DictionarySet(object? value, string key, object? item) + { + var info = value is null ? null : GetCollectionInfo(value.GetType()); + if (info?.KeyIndexer is not { CanWrite: true } indexer) + throw new NotSupportedException($"{value?.GetType().FullName} has no settable string keys"); + indexer.SetValue(value, CoerceBin(item, indexer.PropertyType), [key]); } // A collection's items, as an array the runtime copies into a JS array. @@ -449,6 +458,9 @@ internal sealed class CollectionInfo { public PropertyInfo? Count; public PropertyInfo? Indexer; + // The `this[string]` of an IDictionary / IReadOnlyDictionary. + public PropertyInfo? KeyIndexer; + public MethodInfo? ContainsKey; } private static readonly System.Collections.Concurrent.ConcurrentDictionary s_collectionInfo = new(); @@ -464,6 +476,13 @@ private static CollectionInfo GetCollectionInfo(Type t) => s_collectionInfo.GetO info.Indexer = i.GetProperty("Item"); if (info.Count is null && (definition == typeof(ICollection<>) || definition == typeof(IReadOnlyCollection<>) || definition == typeof(System.Collections.ICollection))) info.Count = i.GetProperty("Count"); + if ((info.KeyIndexer is null || !info.KeyIndexer.CanWrite) + && (definition == typeof(IDictionary<,>) || definition == typeof(IReadOnlyDictionary<,>)) + && i.GetGenericArguments()[0] == typeof(string)) + { + info.KeyIndexer = i.GetProperty("Item"); + info.ContainsKey = i.GetMethod("ContainsKey"); + } } // A writable IList indexer beats a read-only IReadOnlyList one. if (info.Indexer is { CanWrite: false }) @@ -481,31 +500,45 @@ public static bool IsInstanceOf(object? value, string typeName) return t is not null && t.IsInstanceOfType(value); } - internal static DispatchResult BuildMembersResult(Type t) + internal static Type? ResolveType(string? assemblyName, string? typeName) { - const BindingFlags inst = BindingFlags.Public | BindingFlags.Instance; - const BindingFlags stat = BindingFlags.Public | BindingFlags.Static; + if (string.IsNullOrEmpty(typeName)) return null; + var key = (assemblyName ?? "", typeName); + if (s_typeCache.TryGetValue(key, out var cached)) return cached; + var generation = Volatile.Read(ref s_assemblyGeneration); + if (s_missingTypes.TryGetValue(key, out var missedAt) && missedAt == generation) return null; + var t = ResolveTypeCore(key.Item1.Length == 0 ? null : key.Item1, typeName) + ?? ResolveNestedType(key.Item1.Length == 0 ? null : key.Item1, typeName) + ?? ResolveGenericType(key.Item1.Length == 0 ? null : key.Item1, typeName); + if (t is not null) s_typeCache[key] = t; + else s_missingTypes[key] = generation; + return t; + } - var instProps = Array.FindAll(t.GetProperties(inst), p => p.GetIndexParameters().Length == 0); - var statProps = Array.FindAll(t.GetProperties(stat), p => p.GetIndexParameters().Length == 0); + // Type lookups that failed, with the assembly-load generation they failed in. + private static readonly System.Collections.Concurrent.ConcurrentDictionary<(string, string), int> s_missingTypes = new(); + private static int s_assemblyGeneration; - return DispatchResult.Members( - methods: t.GetMethods(inst).Where(m => !m.IsSpecialName).Select(m => m.Name).Distinct().ToArray(), - props: instProps.Where(p => p.GetGetMethod() != null).Select(p => p.Name).Distinct().ToArray(), - staticMethods: t.GetMethods(stat).Where(m => !m.IsSpecialName).Select(m => m.Name).Distinct().ToArray(), - staticProps: statProps.Where(p => p.GetGetMethod() != null).Select(p => p.Name).Distinct().ToArray(), - readonlyProps: instProps.Where(p => p.GetGetMethod() != null && p.GetSetMethod() == null).Select(p => p.Name).Distinct().ToArray(), - readonlyStaticProps: statProps.Where(p => p.GetGetMethod() != null && p.GetSetMethod() == null).Select(p => p.Name).Distinct().ToArray(), - writeonlyProps: instProps.Where(p => p.GetGetMethod() == null && p.GetSetMethod() != null).Select(p => p.Name).Distinct().ToArray(), - writeonlyStaticProps: statProps.Where(p => p.GetGetMethod() == null && p.GetSetMethod() != null).Select(p => p.Name).Distinct().ToArray() - ); - } + private static void OnAssemblyLoad(object? sender, AssemblyLoadEventArgs args) => + Interlocked.Increment(ref s_assemblyGeneration); - internal static Type? ResolveType(string? assemblyName, string? typeName) + // `Outer.Inner` as JS spells a nested type: CLR names it `Outer+Inner`. + private static Type? ResolveNestedType(string? assemblyName, string typeName) { - if (string.IsNullOrEmpty(typeName)) return null; - return s_typeCache.GetOrAdd((assemblyName ?? "", typeName), - static k => ResolveTypeCore(k.Assembly.Length == 0 ? null : k.Assembly, k.Type)); + if (typeName.IndexOf('[') >= 0) return null; + for (var dot = typeName.LastIndexOf('.'); dot > 0; dot = typeName.LastIndexOf('.', dot - 1)) + { + var outer = ResolveType(assemblyName, typeName[..dot]); + if (outer is null) continue; + Type? t = outer; + foreach (var part in typeName[(dot + 1)..].Split('.')) + { + t = t.GetNestedType(part, BindingFlags.Public | BindingFlags.NonPublic); + if (t is null) break; + } + return t; + } + return null; } private static Type? ResolveTypeInAssembly(Assembly asm, string typeName, int lastDot, string shortName) diff --git a/dotnet-bridge/Bridge.Errors.cs b/dotnet-bridge/Bridge.Errors.cs new file mode 100644 index 0000000..add6b28 --- /dev/null +++ b/dotnet-bridge/Bridge.Errors.cs @@ -0,0 +1,57 @@ +using System; +using System.Buffers; +using System.Threading.Tasks; + +namespace NativeScriptBridge; + +/// Thrown into managed code when a JavaScript callback threw; StackTrace leads with the JS stack. +public sealed class JsException(string message, string? jsStack = null) : Exception(message) +{ + public string? JsStack { get; } = jsStack; + + public override string? StackTrace => + string.IsNullOrEmpty(JsStack) ? base.StackTrace : JsStack + Environment.NewLine + base.StackTrace; +} + +public static partial class Bridge +{ + // 0xFF, message, exception type, .NET stack trace, exception handle (`error.nativeException` in + // JS). Older readers stop after the message. + private static unsafe void WriteBinError(Exception ex, byte** outPtr, int* outLen) + { + var buf = new ArrayBufferWriter(256); + var w = new BinWriter(buf); + w.WriteByte(0xFF); + w.WriteString32(ex.Message); + w.WriteString16(ex.GetType().FullName ?? ex.GetType().Name); + w.WriteString32(ex.StackTrace ?? ""); + int handle = -1; + try { handle = HandleFor(ex); } catch { } + w.WriteI32(handle); + WriteUnmanaged(buf.WrittenSpan, outPtr, outLen); + } + + // The handler JS registered for exceptions nothing in .NET caught. + private static Action? s_unhandledHandler; + + /// Forwards unhandled .NET exceptions ("unhandled") and faulted tasks nobody awaited + /// ("unobservedTask") to `handler`, with the exception. + public static void RegisterUnhandledExceptionHandler(Action handler) + { + s_unhandledHandler = handler; + AppDomain.CurrentDomain.UnhandledException -= OnUnhandled; + AppDomain.CurrentDomain.UnhandledException += OnUnhandled; + TaskScheduler.UnobservedTaskException -= OnUnobserved; + TaskScheduler.UnobservedTaskException += OnUnobserved; + } + + private static void OnUnhandled(object? sender, UnhandledExceptionEventArgs e) + { + try { s_unhandledHandler?.Invoke("unhandled", e.ExceptionObject); } catch { } + } + + private static void OnUnobserved(object? sender, UnobservedTaskExceptionEventArgs e) + { + try { s_unhandledHandler?.Invoke("unobservedTask", (object?)e.Exception.InnerException ?? e.Exception); } catch { } + } +} diff --git a/dotnet-bridge/Bridge.Identity.cs b/dotnet-bridge/Bridge.Identity.cs new file mode 100644 index 0000000..fce85d2 --- /dev/null +++ b/dotnet-bridge/Bridge.Identity.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace NativeScriptBridge; + +public static partial class Bridge +{ + // One handle per live object, so JS gets one proxy per object. Each handle sent out counts as + // an export; a collected proxy gives back the exports it saw, and the handle goes when none are + // left, so a release can't race a new export. + private static readonly Dictionary s_handleOf = new(ReferenceEqualityComparer.Instance); + private static readonly Dictionary s_exports = new(); + private static readonly object s_handleLock = new(); + + // The handle for `value`, counting an export. Value types get a new one every time. + internal static int HandleFor(object value) + { + var shared = !value.GetType().IsValueType; + lock (s_handleLock) + { + if (shared && s_handleOf.TryGetValue(value, out var existing)) + { + s_exports[existing] = s_exports.TryGetValue(existing, out var n) ? n + 1 : 1; + return existing; + } + var id = Interlocked.Increment(ref s_nextHandle); + s_handles[id] = value; + if (shared) + { + s_handleOf[value] = id; + s_exports[id] = 1; + } + return id; + } + } + + // Gives back `count` exports (all when negative); the handle goes when none remain. + internal static void ReleaseHandle(int id, int count) + { + lock (s_handleLock) + { + if (count >= 0 && s_exports.TryGetValue(id, out var n)) + { + if (n > count) + { + s_exports[id] = n - count; + return; + } + } + s_exports.Remove(id); + if (!s_handles.TryRemove(id, out var obj)) return; + if (obj is not null && s_handleOf.TryGetValue(obj, out var owner) && owner == id) + s_handleOf.Remove(obj); + } + if (s_nativePtrs.TryRemove(id, out var nativePtr)) + { + RemoveNativePtr(id, nativePtr); + try { Marshal.Release(nativePtr); } catch { } + } + } + + internal static void ClearHandles() + { + lock (s_handleLock) + { + s_handleOf.Clear(); + s_exports.Clear(); + s_handles.Clear(); + } + } +} diff --git a/dotnet-bridge/Bridge.JsDelegate.cs b/dotnet-bridge/Bridge.JsDelegate.cs index 9b895ae..009ef01 100644 --- a/dotnet-bridge/Bridge.JsDelegate.cs +++ b/dotnet-bridge/Bridge.JsDelegate.cs @@ -131,9 +131,8 @@ private static DynamicMethod BuildJsDelegateInvoker(Type delegateType) internal static object? CallJsCallback(int id, object?[] args) => CallJsCallback(id, args, expectsResult: true); - // Calls the JS function registered under `id`. When it throws, a caller that expects a result - // gets a JsException (a delegate with a return value, an overridden member); a void delegate or - // event just returns, the error having been reported as an uncaught JS error. + // Calls the JS function `id`. A JS throw becomes a JsException with the JS stack; a callback + // that couldn't run (its worker is gone) only throws when a result is expected. internal static unsafe object? CallJsCallback(int id, object?[] args, bool expectsResult) { if (s_jsInvoker == null) return null; @@ -154,12 +153,23 @@ private static DynamicMethod BuildJsDelegateInvoker(Type delegateType) // Response is only set for non-void delegates; parse and free when present. if (respPtr != null && respLen > 0) { - string? jsError = null; + string? jsError = null, jsStack = null; + var couldNotRun = false; try { var span = new ReadOnlySpan(respPtr, respLen); if (span[0] == 0x0F) - jsError = span.Length > 5 ? new BinReader(span[1..]).ReadString32() : "the JavaScript callback could not run"; + { + // 0x0F, message, optional JS stack. An empty message means it couldn't run. + var r = new BinReader(span[1..]); + jsError = span.Length >= 5 ? r.ReadString32() : ""; + if (r.HasMore) jsStack = r.ReadString32(); + if (jsError.Length == 0) + { + couldNotRun = true; + jsError = "the JavaScript callback could not run"; + } + } else result = ParseCallbackResponse(span); } @@ -168,7 +178,7 @@ private static DynamicMethod BuildJsDelegateInvoker(Type delegateType) { Marshal.FreeHGlobal((IntPtr)respPtr); } - if (jsError != null && expectsResult) throw new JsException(jsError); + if (jsError != null && (expectsResult || !couldNotRun)) throw new JsException(jsError, jsStack); } return result; @@ -215,7 +225,9 @@ private static void WriteCallbackArg(ref BinWriter w, object? arg) if (arg is bool b) { w.WriteByte(b ? (byte)0x02 : (byte)0x01); return; } if (arg is int i) { w.WriteByte(0x03); w.WriteI32(i); return; } if (arg is uint u) { w.WriteByte(0x03); w.WriteI32((int)u); return; } - if (arg is long l) { w.WriteByte(0x04); w.WriteF64((double)l); return; } + if (arg is long l) { WriteInt64(ref w, l); return; } + if (arg is ulong ul) { WriteUInt64(ref w, ul); return; } + if (TryGetBytes(arg, out var bytes, out var kind)) { WriteTypedArray(ref w, bytes, kind); return; } if (arg is float f) { w.WriteByte(0x04); w.WriteF64((double)f); return; } if (arg is double d){ w.WriteByte(0x04); w.WriteF64(d); return; } if (arg is string s){ w.WriteByte(0x05); w.WriteString32(s); return; } @@ -233,8 +245,7 @@ private static void WriteCallbackArg(ref BinWriter w, object? arg) // Object: box in the handle map and send as a handle reference. // The JS side receives {__handle, __type} which can be turned into a // proxy via NSWinRT.dotnet.fromHandle(...). - var handleId = Interlocked.Increment(ref s_nextHandle); - s_handles[handleId] = arg; + var handleId = HandleFor(arg); var typeName = arg.GetType().FullName ?? arg.GetType().Name; w.WriteByte(0x06); w.WriteI32(handleId); @@ -263,6 +274,9 @@ private static void WriteCallbackArg(ref BinWriter w, object? arg) case 0x06: return (object)new HandleRef(r.ReadI32()); case 0x0A: return (object)new WinRtRef(r.ReadI64()); case 0x0D: return new JsJsonValue(r.ReadString32()); + case 0x10: return (object)r.ReadI64(); + case 0x11: return (object)unchecked((ulong)r.ReadI64()); + case 0x12: return r.ReadTypedArray(); case 0x07: // array: u32 count + N tagged items { var count = (int)r.ReadU32(); @@ -280,6 +294,8 @@ private static void WriteCallbackArg(ref BinWriter w, object? arg) case 0x05: arr[i] = r.ReadString32(); break; case 0x06: arr[i] = new HandleRef(r.ReadI32()); break; case 0x0A: arr[i] = new WinRtRef(r.ReadI64()); break; + case 0x10: arr[i] = r.ReadI64(); break; + case 0x11: arr[i] = unchecked((ulong)r.ReadI64()); break; default: arr[i] = null; break; } } @@ -297,5 +313,3 @@ internal static unsafe void CallJsCallbackVoid(int id, object?[] args) } -/// Thrown into managed code when a JavaScript callback that was expected to return a value threw. -public sealed class JsException(string message) : Exception(message); diff --git a/dotnet-bridge/Bridge.Members.cs b/dotnet-bridge/Bridge.Members.cs new file mode 100644 index 0000000..8d94adc --- /dev/null +++ b/dotnet-bridge/Bridge.Members.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace NativeScriptBridge; + +// A property or field: JS reads and writes both as properties. +internal sealed class DataMember +{ + private readonly PropertyInfo? _property; + private readonly FieldInfo? _field; + + public DataMember(PropertyInfo property) => _property = property; + public DataMember(FieldInfo field) => _field = field; + + public Type PropertyType => _property?.PropertyType ?? _field!.FieldType; + + public string Name => _property?.Name ?? _field!.Name; + + public object? GetValue(object? target) => _property is not null ? _property.GetValue(target) : _field!.GetValue(target); + + public void SetValue(object? target, object? value) + { + if (_property is not null) + { + _property.SetValue(target, value); + return; + } + if (_field!.IsInitOnly || _field.IsLiteral) + throw new FieldAccessException($"Cannot assign to read-only field '{_field.Name}' of .NET type '{_field.DeclaringType?.FullName}'"); + _field.SetValue(target, value); + } + + // The property or field `name` of `type`, or null. + public static DataMember? Find(Type type, string name, BindingFlags flags) + { + PropertyInfo? property = null; + try { property = type.GetProperty(name, flags); } + catch (AmbiguousMatchException) + { + // A property redeclared with `new` in a derived class: the most derived one. + property = type.GetProperties(flags).Where(p => p.Name == name && p.GetIndexParameters().Length == 0) + .OrderByDescending(p => Depth(p.DeclaringType)).FirstOrDefault(); + } + if (property is not null && property.GetIndexParameters().Length == 0) return new DataMember(property); + var field = type.GetField(name, flags); + return field is not null && !field.IsSpecialName ? new DataMember(field) : null; + } + + private static int Depth(Type? t) + { + var depth = 0; + for (; t is not null; t = t.BaseType) depth++; + return depth; + } +} + +public static partial class Bridge +{ + internal static DispatchResult BuildMembersResult(Type t) + { + const BindingFlags inst = BindingFlags.Public | BindingFlags.Instance; + const BindingFlags stat = BindingFlags.Public | BindingFlags.Static; + const BindingFlags family = BindingFlags.NonPublic | BindingFlags.Instance; + + var instProps = Array.FindAll(t.GetProperties(inst), p => p.GetIndexParameters().Length == 0); + var statProps = Array.FindAll(t.GetProperties(stat), p => p.GetIndexParameters().Length == 0); + var instFields = Array.FindAll(t.GetFields(inst), f => !f.IsSpecialName); + var statFields = Array.FindAll(t.GetFields(stat), f => !f.IsSpecialName); + + static string[] Names(IEnumerable members) => members.Select(m => m.Name).Distinct().ToArray(); + static bool ReadOnlyField(FieldInfo f) => f.IsInitOnly || f.IsLiteral; + + var instEvents = t.GetEvents(inst); + var statEvents = t.GetEvents(stat); + var protectedMethods = t.GetMethods(family).Where(m => (m.IsFamily || m.IsFamilyOrAssembly) && !m.IsSpecialName); + var protectedProps = t.GetProperties(family).Where(p => p.GetIndexParameters().Length == 0 + && (p.GetMethod ?? p.SetMethod) is { } a && (a.IsFamily || a.IsFamilyOrAssembly)); + + return DispatchResult.Members( + methods: Names(t.GetMethods(inst).Where(m => !m.IsSpecialName)), + props: Names(instProps.Where(p => p.GetGetMethod() != null).Concat(instFields)), + staticMethods: Names(t.GetMethods(stat).Where(m => !m.IsSpecialName)), + staticProps: Names(statProps.Where(p => p.GetGetMethod() != null).Concat(statFields)), + readonlyProps: Names(instProps.Where(p => p.GetGetMethod() != null && p.GetSetMethod() == null).Concat(instFields.Where(ReadOnlyField))), + readonlyStaticProps: Names(statProps.Where(p => p.GetGetMethod() != null && p.GetSetMethod() == null).Concat(statFields.Where(ReadOnlyField))), + writeonlyProps: Names(instProps.Where(p => p.GetGetMethod() == null && p.GetSetMethod() != null)), + writeonlyStaticProps: Names(statProps.Where(p => p.GetGetMethod() == null && p.GetSetMethod() != null)), + extras: + [ + ("events", Names(instEvents)), + ("staticEvents", Names(statEvents)), + ("protectedMethods", Names(protectedMethods)), + ("protectedProperties", Names(protectedProps)), + ("nestedTypes", Names(t.GetNestedTypes(BindingFlags.Public))), + ]); + } + + // Delegates added for JS functions, by function token, so remove_X(fn) removes the right one. + private sealed class EventHandlers + { + public readonly Dictionary<(string Event, int Token), List> Added = new(); + // The handler of each event assigned with `obj.Event = fn`. + public readonly Dictionary Assigned = new(); + } + + private static readonly ConditionalWeakTable s_instanceEvents = new(); + private static readonly Dictionary s_staticEvents = new(); + + private static (EventInfo Event, EventHandlers Handlers) EventOf(object? target, string typeName, string eventName) + { + var type = target?.GetType() ?? ResolveType(null, typeName) + ?? throw new TypeLoadException($"Type not found: {typeName}"); + var flags = BindingFlags.Public | (target is null ? BindingFlags.Static : BindingFlags.Instance); + var ev = type.GetEvent(eventName, flags) + ?? throw new MissingMemberException($"Event '{eventName}' not found on {type.FullName}"); + EventHandlers handlers; + if (target is null) + { + lock (s_staticEvents) + { + if (!s_staticEvents.TryGetValue(type, out handlers!)) s_staticEvents[type] = handlers = new EventHandlers(); + } + } + else + { + handlers = s_instanceEvents.GetValue(target, static _ => new EventHandlers()); + } + return (ev, handlers); + } + + /// `obj.add_Event(fn)` from JS: subscribes a delegate of the event's type calling `fn`. + public static void AddEventHandler(object? target, string typeName, string eventName, int token, JsFunctionRef fn) + { + var (ev, handlers) = EventOf(target, typeName, eventName); + var handler = MakeJsDelegate(ev.EventHandlerType!, fn.Id); + ev.AddEventHandler(target, handler); + lock (handlers) + { + if (!handlers.Added.TryGetValue((eventName, token), out var list)) + handlers.Added[(eventName, token)] = list = new List(); + list.Add(handler); + } + } + + /// `obj.remove_Event(fn)` from JS: unsubscribes the delegate added for `fn`. + public static void RemoveEventHandler(object? target, string typeName, string eventName, int token) + { + var (ev, handlers) = EventOf(target, typeName, eventName); + Delegate? handler = null; + lock (handlers) + { + if (handlers.Added.TryGetValue((eventName, token), out var list) && list.Count > 0) + { + handler = list[^1]; + list.RemoveAt(list.Count - 1); + if (list.Count == 0) handlers.Added.Remove((eventName, token)); + } + } + if (handler is not null) ev.RemoveEventHandler(target, handler); + } + + /// `obj.Event = fn` from JS: replaces the handler assigned before (null removes it). + public static void SetEventHandler(object? target, string typeName, string eventName, JsFunctionRef? fn) + { + var (ev, handlers) = EventOf(target, typeName, eventName); + Delegate? previous; + lock (handlers) + { + handlers.Assigned.Remove(eventName, out previous); + } + if (previous is not null) ev.RemoveEventHandler(target, previous); + if (fn is not { } f) return; + var handler = MakeJsDelegate(ev.EventHandlerType!, f.Id); + ev.AddEventHandler(target, handler); + lock (handlers) + { + handlers.Assigned[eventName] = handler; + } + } +} diff --git a/dotnet-bridge/Bridge.Proxy.cs b/dotnet-bridge/Bridge.Proxy.cs index c16910d..409af98 100644 --- a/dotnet-bridge/Bridge.Proxy.cs +++ b/dotnet-bridge/Bridge.Proxy.cs @@ -164,12 +164,12 @@ static string SignatureKey(string name, Type[] paramTypes) => name + "(" + string.Join(",", paramTypes.Select(p => p.FullName)) + ")"; // Override only the base virtuals JS actually asked for (memberSet). Anything not in - // that set is simply never touched, so the base implementation stays live in the vtable — - // no "call base" IL needed. (Skip ref/out and generic methods — unsupported.) + // that set is simply never touched, so the base implementation stays live in the vtable: + // no "call base" IL needed. (ref/out methods are skipped.) // Abstract members are always implemented (dispatching to JS, which reports a missing // implementation as an error) — a type with an unimplemented abstract member can't be created. var virtualMethods = baseType.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) - .Where(m => m.IsVirtual && !m.IsFinal && !m.IsGenericMethod && !m.IsConstructor + .Where(m => m.IsVirtual && !m.IsFinal && !m.IsConstructor && (m.IsPublic || m.IsFamily || m.IsFamilyOrAssembly) && (memberSet.Contains(m.Name) || m.IsAbstract)) .ToArray(); @@ -178,6 +178,11 @@ static string SignatureKey(string name, Type[] paramTypes) => { var parameters = mi.GetParameters(); if (parameters.Any(p => p.ParameterType.IsByRef)) continue; + if (mi.IsGenericMethodDefinition) + { + EmitGenericOverride(tb, mi); + continue; + } var paramTypes = parameters.Select(p => p.ParameterType).ToArray(); var sigKey = SignatureKey(mi.Name, paramTypes); @@ -241,6 +246,88 @@ void CollectInterfaces(Type t) return tb.CreateTypeInfo()!.AsType(); } + // Overrides a generic virtual (`T Convert(object)`) with a generic method that calls JS and + // converts the result to T. + private static void EmitGenericOverride(TypeBuilder tb, MethodInfo mi) + { + var mb = tb.DefineMethod(mi.Name, MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig, mi.CallingConvention); + var baseArgs = mi.GetGenericArguments(); + var gps = mb.DefineGenericParameters(baseArgs.Select(a => a.Name).ToArray()); + + Type Sub(Type t) + { + if (t.IsGenericParameter && t.DeclaringMethod is not null) return gps[t.GenericParameterPosition]; + if (t.IsArray) return t.GetArrayRank() == 1 ? Sub(t.GetElementType()!).MakeArrayType() : Sub(t.GetElementType()!).MakeArrayType(t.GetArrayRank()); + if (t.IsByRef) return Sub(t.GetElementType()!).MakeByRefType(); + if (t.IsGenericType && t.ContainsGenericParameters) + return t.GetGenericTypeDefinition().MakeGenericType(t.GetGenericArguments().Select(Sub).ToArray()); + return t; + } + + for (int i = 0; i < gps.Length; i++) + { + gps[i].SetGenericParameterAttributes(baseArgs[i].GenericParameterAttributes); + var constraints = baseArgs[i].GetGenericParameterConstraints().Select(Sub).ToArray(); + var baseConstraint = constraints.FirstOrDefault(c => !c.IsInterface); + if (baseConstraint is not null) gps[i].SetBaseTypeConstraint(baseConstraint); + var interfaces = constraints.Where(c => c.IsInterface).ToArray(); + if (interfaces.Length > 0) gps[i].SetInterfaceConstraints(interfaces); + } + + var paramTypes = mi.GetParameters().Select(p => Sub(p.ParameterType)).ToArray(); + var returnType = Sub(mi.ReturnType); + mb.SetReturnType(returnType); + mb.SetParameters(paramTypes); + + var il = mb.GetILGenerator(); + var argsLocal = il.DeclareLocal(typeof(object[])); + il.Emit(OpCodes.Ldc_I4, paramTypes.Length); + il.Emit(OpCodes.Newarr, typeof(object)); + il.Emit(OpCodes.Stloc, argsLocal); + for (int i = 0; i < paramTypes.Length; i++) + { + il.Emit(OpCodes.Ldloc, argsLocal); + il.Emit(OpCodes.Ldc_I4, i); + il.Emit(OpCodes.Ldarg, i + 1); + // `box` of a type parameter is a no-op for reference types. + if (paramTypes[i].IsGenericParameter || paramTypes[i].ContainsGenericParameters || paramTypes[i].IsValueType) + il.Emit(OpCodes.Box, paramTypes[i]); + il.Emit(OpCodes.Stelem_Ref); + } + var runtime = typeof(ProxyRuntime); + il.Emit(OpCodes.Ldarg_0); + il.Emit(OpCodes.Ldstr, mi.Name); + il.Emit(OpCodes.Ldloc, argsLocal); + if (returnType == typeof(void)) + il.Emit(OpCodes.Call, runtime.GetMethod(nameof(ProxyRuntime.InvokeVoid))!); + else + il.Emit(OpCodes.Call, runtime.GetMethod(nameof(ProxyRuntime.InvokeMethodTyped))!.MakeGenericMethod(returnType)); + il.Emit(OpCodes.Ret); + tb.DefineMethodOverride(mb, mi); + + // `super.Convert(...)`: a generic entry point to the base implementation. + if (mi.IsAbstract) return; + var baseCall = tb.DefineMethod(BaseCallPrefix + mi.Name, MethodAttributes.Public | MethodAttributes.HideBySig, CallingConventions.HasThis); + var bgps = baseCall.DefineGenericParameters(baseArgs.Select(a => a.Name).ToArray()); + for (int i = 0; i < bgps.Length; i++) bgps[i].SetGenericParameterAttributes(baseArgs[i].GenericParameterAttributes); + Type SubBase(Type t) + { + if (t.IsGenericParameter && t.DeclaringMethod is not null) return bgps[t.GenericParameterPosition]; + if (t.IsArray) return SubBase(t.GetElementType()!).MakeArrayType(); + if (t.IsGenericType && t.ContainsGenericParameters) + return t.GetGenericTypeDefinition().MakeGenericType(t.GetGenericArguments().Select(SubBase).ToArray()); + return t; + } + var baseParams = mi.GetParameters().Select(p => SubBase(p.ParameterType)).ToArray(); + baseCall.SetReturnType(SubBase(mi.ReturnType)); + baseCall.SetParameters(baseParams); + var bil = baseCall.GetILGenerator(); + bil.Emit(OpCodes.Ldarg_0); + for (int i = 0; i < baseParams.Length; i++) bil.Emit(OpCodes.Ldarg, i + 1); + bil.Emit(OpCodes.Call, mi.MakeGenericMethod(bgps)); + bil.Emit(OpCodes.Ret); + } + internal const string BaseCallPrefix = "__ns_base_"; internal const string JsSelfMember = "__ns_js_self"; diff --git a/dotnet-bridge/Bridge.TaskHelper.cs b/dotnet-bridge/Bridge.TaskHelper.cs index 5671e5b..790d48a 100644 --- a/dotnet-bridge/Bridge.TaskHelper.cs +++ b/dotnet-bridge/Bridge.TaskHelper.cs @@ -28,14 +28,15 @@ internal static void ScheduleTaskContinuation(int handleId, int resolveId, int r { try { + // Rejections pass the exception; JS makes an Error of it. if (completed.IsFaulted) { - var ex = completed.Exception?.InnerException ?? completed.Exception; - CallJsCallback(rejectId, [ex?.Message ?? "Task faulted"]); + var ex = (Exception?)completed.Exception?.InnerException ?? completed.Exception; + CallJsCallback(rejectId, [ex ?? new InvalidOperationException("Task faulted")]); } else if (completed.IsCanceled) { - CallJsCallback(rejectId, ["Task cancelled"]); + CallJsCallback(rejectId, [new TaskCanceledException(completed)]); } else { @@ -44,7 +45,7 @@ internal static void ScheduleTaskContinuation(int handleId, int resolveId, int r } catch (Exception ex) { - CallJsCallback(rejectId, [ex.Message]); + CallJsCallback(rejectId, [ex]); } }, TaskScheduler.Default); } diff --git a/dotnet-bridge/Bridge.cs b/dotnet-bridge/Bridge.cs index 914caa4..9505a3d 100644 --- a/dotnet-bridge/Bridge.cs +++ b/dotnet-bridge/Bridge.cs @@ -34,7 +34,7 @@ public static partial class Bridge // Keyed by (assembly name or "", type name): no key string is built per lookup. private static readonly ConcurrentDictionary<(string Assembly, string Type), Type?> s_typeCache = new(); private static readonly ConcurrentDictionary s_methodCache = new(); - private static readonly ConcurrentDictionary s_propCache = new(); + private static readonly ConcurrentDictionary s_propCache = new(); private static readonly ConcurrentDictionary s_ctorCache = new(); // Cache of attempted assembly loads (simple name -> Assembly or null if not found). private static readonly ConcurrentDictionary s_assemblyLoadCache @@ -50,6 +50,7 @@ public static partial class Bridge internal static void ClearCaches() { s_typeCache.Clear(); + s_missingTypes.Clear(); s_methodCache.Clear(); s_propCache.Clear(); s_ctorCache.Clear(); @@ -58,7 +59,7 @@ internal static void ClearCaches() s_boxInfo.Clear(); s_paramsCache.Clear(); s_collectionInfo.Clear(); - s_handles.Clear(); + ClearHandles(); s_nativePtrs.Clear(); s_nativePtrsReverse.Clear(); s_nextHandle = 0; @@ -157,6 +158,26 @@ public static string GetNamespaceAssemblyMapJson() try { return JsonSerializer.Serialize(map); } catch { return "{}"; } } + /// Loads the assembly at `path` (relative to the app directory unless absolute) and returns its + /// namespace map, as GetNamespaceAssemblyMapJson does. + public static string LoadAssembly(string path) + { + var full = Path.IsPathRooted(path) ? path : Path.Combine(AppContext.BaseDirectory, path); + var asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(Path.GetFullPath(full)); + var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + Type[] types; + try { types = asm.GetExportedTypes(); } + catch (ReflectionTypeLoadException e) { types = e.Types.Where(t => t != null).ToArray()!; } + var name = asm.GetName().Name ?? ""; + foreach (var t in types) + { + if (string.IsNullOrEmpty(t.Namespace)) continue; + map[t.Namespace] = name; + map.TryAdd(t.Namespace.Split('.')[0], name); + } + return JsonSerializer.Serialize(map); + } + static Bridge() { try @@ -212,6 +233,7 @@ static Bridge() s_assemblySearchDirs = dirs.Where(d => !string.IsNullOrEmpty(d)).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + AppDomain.CurrentDomain.AssemblyLoad += OnAssemblyLoad; } catch { @@ -517,52 +539,6 @@ public static unsafe int RegisterJsCallback( return 0; } - // Callback id registered by the runtime/JS that should receive unhandled - // managed exceptions and unobserved task exceptions. - private static int s_unhandledExceptionCallbackId = -1; - - // Called from JS/Rust to request that managed unhandled exceptions be - // forwarded to the registered JS callback id. The callback id must have - // been created on the Rust side and point to a JS function stored in the - // runtime's `DOTNET_JS_CALLBACKS` map. - public static int RegisterUnhandledExceptionCallback(int callbackId) - { - s_unhandledExceptionCallbackId = callbackId; - - // Subscribe once (idempotent). - try - { - AppDomain.CurrentDomain.UnhandledException -= OnAppDomainUnhandledException; - AppDomain.CurrentDomain.UnhandledException += OnAppDomainUnhandledException; - - TaskScheduler.UnobservedTaskException -= OnUnobservedTaskException; - TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; - - // Try to subscribe to Windows.CoreApplication.UnhandledErrorDetected if available - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - var coreAppType = asm.GetType("Windows.ApplicationModel.Core.CoreApplication"); - if (coreAppType == null) continue; - var ev = coreAppType.GetEvent("UnhandledErrorDetected"); - if (ev == null) continue; - try - { - var handlerType = ev.EventHandlerType; - var method = typeof(Bridge).GetMethod(nameof(CoreApplicationUnhandledErrorHandler), BindingFlags.NonPublic | BindingFlags.Static); - if (handlerType != null && method != null) - { - var del = Delegate.CreateDelegate(handlerType, method); - ev.AddEventHandler(null, del); - } - } - catch { } - } - } - catch { } - - return 0; - } - // IInspectable IID — the common interface for all WinRT objects. // WinRT APIs typically QI for IInspectable (not just IUnknown) when they // accept a polymorphic `object` parameter or need runtime class identity. @@ -693,56 +669,6 @@ private static IntPtr ObtainNativePtr(object obj) } } - private static void OnAppDomainUnhandledException(object? sender, UnhandledExceptionEventArgs e) - { - try - { - var ex = e.ExceptionObject as Exception; - var msg = ex?.ToString() ?? e.ExceptionObject?.ToString() ?? "(unknown)"; - SendUnhandledToJs("unhandled", msg); - } - catch { } - } - - private static void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) - { - try - { - var msg = e.Exception?.ToString() ?? "(unobserved task exception)"; - SendUnhandledToJs("unobservedTask", msg); - } - catch { } - } - - // Fallback handler for platform-specific CoreApplication unhandled events. - private static void CoreApplicationUnhandledErrorHandler(object? sender, object? args) - { - try - { - var msg = args?.ToString() ?? "(core unhandled)"; - SendUnhandledToJs("coreUnhandled", msg); - } - catch { } - } - - private static unsafe void SendUnhandledToJs(string kind, string message) - { - try - { - if (s_jsInvoker == null || s_unhandledExceptionCallbackId <= 0) return; - var payload = JsonSerializer.Serialize(new { kind = kind, message = message }); - var bytes = System.Text.Encoding.UTF8.GetBytes(payload); - fixed (byte* p = bytes) - { - byte* respPtr = null; - int respLen = 0; - s_jsInvoker(s_unhandledExceptionCallbackId, p, bytes.Length, &respPtr, &respLen); - if (respPtr != null && respLen > 0) Marshal.FreeHGlobal((IntPtr)respPtr); - } - } - catch { } - } - [UnmanagedCallersOnly(EntryPoint = "Invoke", CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])] public static unsafe int Invoke( diff --git a/dotnet-bridge/DispatchTypes.cs b/dotnet-bridge/DispatchTypes.cs index 82e1057..73e36f7 100644 --- a/dotnet-bridge/DispatchTypes.cs +++ b/dotnet-bridge/DispatchTypes.cs @@ -46,7 +46,7 @@ internal readonly struct CtorEntry(ConstructorInfo? ctor, ParameterInfo[] parame public readonly ParameterInfo[] Parameters = parameters; } -internal enum DispatchKind : byte { Void, Primitive, Handle, Collection, Members, TaskHandle } +internal enum DispatchKind : byte { Void, Primitive, Handle, Collection, Members, TaskHandle, Bytes } internal readonly struct DispatchResult { @@ -66,18 +66,22 @@ internal readonly struct DispatchResult private readonly string[]? _readonlyStaticProperties; private readonly string[]? _writeonlyProperties; private readonly string[]? _writeonlyStaticProperties; + // Member lists added after the first eight, by name (events, nested types, ...). + private readonly (string Key, string[] Names)[]? _extraLists; private DispatchResult(DispatchKind kind, object? value, Type? type, int handle, string? typeName, string[]? methods, string[]? props, string[]? staticMethods, string[]? staticProps, string[]? readonlyProps = null, string[]? readonlyStaticProps = null, - string[]? writeonlyProps = null, string[]? writeonlyStaticProps = null) + string[]? writeonlyProps = null, string[]? writeonlyStaticProps = null, + (string Key, string[] Names)[]? extraLists = null) { _kind = kind; _value = value; _type = type; _handle = handle; _typeName = typeName; _methods = methods; _properties = props; _staticMethods = staticMethods; _staticProperties = staticProps; _readonlyProperties = readonlyProps; _readonlyStaticProperties = readonlyStaticProps; _writeonlyProperties = writeonlyProps; _writeonlyStaticProperties = writeonlyStaticProps; + _extraLists = extraLists; } public static DispatchResult Primitive(object value, Type type) @@ -92,13 +96,18 @@ public static DispatchResult TaskHandle(int id, string typeName) public static DispatchResult Collection(IEnumerable items) => new(DispatchKind.Collection, items, null, 0, null, null, null, null, null); + // byte[], Memory, ArraySegment: a Uint8Array in JS. + public static DispatchResult Bytes(object value) + => new(DispatchKind.Bytes, value, null, 0, null, null, null, null, null); + public static DispatchResult Members( string[] methods, string[] props, string[] staticMethods, string[] staticProps, string[] readonlyProps, string[] readonlyStaticProps, - string[] writeonlyProps, string[] writeonlyStaticProps) + string[] writeonlyProps, string[] writeonlyStaticProps, + (string Key, string[] Names)[]? extras = null) => new(DispatchKind.Members, null, null, 0, null, methods, props, staticMethods, staticProps, - readonlyProps, readonlyStaticProps, writeonlyProps, writeonlyStaticProps); + readonlyProps, readonlyStaticProps, writeonlyProps, writeonlyStaticProps, extras); internal int HandleId() => _handle; @@ -142,6 +151,11 @@ public void WriteTo(Utf8JsonWriter w, JsonSerializerOptions opts) w.WriteEndObject(); break; + case DispatchKind.Bytes: + Bridge.TryGetBytes(_value!, out var jsonBytes, out _); + w.WriteBase64StringValue(jsonBytes); + break; + case DispatchKind.Collection: w.WriteStartArray(); foreach (var item in (IEnumerable)_value!) @@ -159,6 +173,8 @@ public void WriteTo(Utf8JsonWriter w, JsonSerializerOptions opts) WriteStringArray(w, "readonlyStaticProperties"u8, _readonlyStaticProperties!); WriteStringArray(w, "writeonlyProperties"u8, _writeonlyProperties!); WriteStringArray(w, "writeonlyStaticProperties"u8,_writeonlyStaticProperties!); + foreach (var (key, names) in _extraLists ?? []) + WriteStringArray(w, System.Text.Encoding.UTF8.GetBytes(key), names); w.WriteEndObject(); break; } @@ -177,7 +193,7 @@ private static void WriteStringArray(Utf8JsonWriter w, ReadOnlySpan name, // 0x03 = i32[4] 0x04 = f64[8] 0x05 = string[u32+utf8] // 0x06 = handle[i32 + u16+utf8 type] // 0x07 = array[u32 count + N tagged items] - // 0x08 = members{ methods, props, staticMethods, staticProps } + // 0x08 = members{ 8 name lists, then u16 count + (u16+utf8 key, name list) per extra list } // 0xFF = error[u32+utf8] public void WriteAsBin(ArrayBufferWriter outBuf) @@ -220,6 +236,11 @@ public void WriteAsBin(ArrayBufferWriter outBuf) } break; + case DispatchKind.Bytes: + Bridge.TryGetBytes(_value!, out var bytes, out var kind); + Bridge.WriteTypedArray(ref w, bytes, kind); + break; + case DispatchKind.Collection: var items = new List(); foreach (var item in (IEnumerable)_value!) items.Add(item); @@ -238,6 +259,13 @@ public void WriteAsBin(ArrayBufferWriter outBuf) WriteStringArrayBin(ref w, _readonlyStaticProperties!); WriteStringArrayBin(ref w, _writeonlyProperties!); WriteStringArrayBin(ref w, _writeonlyStaticProperties!); + var extras = _extraLists ?? []; + w.WriteU16((ushort)extras.Length); + foreach (var (key, names) in extras) + { + w.WriteString16(key); + WriteStringArrayBin(ref w, names); + } break; } } @@ -253,8 +281,10 @@ private static void WritePrimitiveBin(ref BinWriter w, object value) case ushort v: w.WriteByte(0x03); w.WriteI32(v); break; case int v: w.WriteByte(0x03); w.WriteI32(v); break; case uint v: w.WriteByte(0x04); w.WriteF64(v); break; - case long v: w.WriteByte(0x04); w.WriteF64(v); break; - case ulong v: w.WriteByte(0x04); w.WriteF64(v); break; + case long v: Bridge.WriteInt64(ref w, v); break; + case ulong v: Bridge.WriteUInt64(ref w, v); break; + case nint v: Bridge.WriteInt64(ref w, v); break; + case nuint v: Bridge.WriteUInt64(ref w, v); break; case float v: w.WriteByte(0x04); w.WriteF64(v); break; case double v: w.WriteByte(0x04); w.WriteF64(v); break; case decimal v: w.WriteByte(0x04); w.WriteF64((double)v); break; @@ -272,6 +302,11 @@ private static void WriteValueBin(ref BinWriter w, object? value) WritePrimitiveBin(ref w, value); return; } + if (Bridge.TryGetBytes(value, out var bytes, out var kind)) + { + Bridge.WriteTypedArray(ref w, bytes, kind); + return; + } if (value is Array enumerable) { var items = new List(); @@ -281,8 +316,7 @@ private static void WriteValueBin(ref BinWriter w, object? value) foreach (var item in items) WriteValueBin(ref w, item); return; } - var id = Interlocked.Increment(ref Bridge.s_nextHandle); - Bridge.s_handles[id] = value; + var id = Bridge.HandleFor(value); w.WriteByte(0x06); w.WriteI32(id); w.WriteString16(t.FullName ?? t.Name); diff --git a/runtime/src/global_fns.rs b/runtime/src/global_fns.rs index 8f36fe4..c0d97fa 100644 --- a/runtime/src/global_fns.rs +++ b/runtime/src/global_fns.rs @@ -1243,7 +1243,7 @@ pub(crate) fn handle_worker_poll_messages_blocking( /// Installed into every context. Provides NSWinRT, module loader, async helpers, /// proxy extension infrastructure, and the `Function.prototype.extend` NativeScript API. -const HELPER_SOURCE: &str = r#" +const HELPER_SOURCE: &str = concat!(r#" (function () { if (typeof globalThis.queueMicrotask !== 'function') { globalThis.queueMicrotask = function (callback) { @@ -1931,2311 +1931,459 @@ const HELPER_SOURCE: &str = r#" globalThis.NSWinRT = globalThis.NSWinRT || {}; globalThis.NSWinRT.interop = globalThis.NSWinRT.interop || globalThis.interop; - var proxyExtensions = []; - var proxyInstances = new Map(); - var nextProxyId = 1; +"#, include_str!("js/dotnet_subclass.js"), r#" - function ctorName(ctor) { - return (ctor && (ctor.__winrtProxyName__ || ctor.__typeName__ || ctor.name)) || 'Object'; + function asDelegate(handler) { + if (typeof handler === 'function') { + return handler; + } + if (handler && typeof handler.invoke === 'function') { + return handler.invoke.bind(handler); + } + throw new Error('NSWinRT.asDelegate(handler) expects a function or { invoke() } object'); } - // This IIFE runs before the NSWinRT.dotnet block below in script order, but by the - // time createManagedSubclass is actually CALLED (well after the whole bootstrap has - // finished evaluating) NSWinRT.dotnet.wrap already exists whenever - // __nsDotNetCreateJsSubclass does (both are registered together) — wraps a raw tagged - // handle value into a live DotNet instance proxy exactly like the dotnet block's own - // private `_wrap`, without needing access to that other IIFE's closure. - function _wrapDotNetHandle(value) { - var dotnet = globalThis.NSWinRT && globalThis.NSWinRT.dotnet; - return (dotnet && typeof dotnet.wrap === 'function') ? dotnet.wrap(value) : value; + function createEventEmitter() { + var listeners = []; + return { + add: function (handler) { + var normalized = asDelegate(handler); + listeners.push(normalized); + return { + dispose: function () { + var idx = listeners.indexOf(normalized); + if (idx >= 0) { + listeners.splice(idx, 1); + } + }, + }; + }, + emit: function () { + var args = Array.prototype.slice.call(arguments); + listeners.slice().forEach(function (listener) { + listener.apply(undefined, args); + }); + }, + count: function () { + return listeners.length; + }, + }; } - var typeDescriptorCache = Object.create(null); + globalThis.NSWinRT.asDelegate = asDelegate; + globalThis.NSWinRT.createEventEmitter = createEventEmitter; - function describeWinRTType(typeName) { - if (!typeName || typeof globalThis.__nsDescribeWinRTType !== 'function') { - return null; - } - if (Object.prototype.hasOwnProperty.call(typeDescriptorCache, typeName)) { - return typeDescriptorCache[typeName]; - } - try { - var raw = globalThis.__nsDescribeWinRTType(typeName); - typeDescriptorCache[typeName] = raw ? JSON.parse(raw) : null; - } catch (_) { - typeDescriptorCache[typeName] = null; - } - return typeDescriptorCache[typeName]; - } + var moduleCache = new Map(); - function buildFallbackParameterMetadata(fn) { - var params = []; - var count = typeof fn === 'function' && Number.isFinite(fn.length) ? fn.length : 0; - for (var i = 0; i < count; i++) { - params.push({ name: 'arg' + i, type: 'Object' }); + function esmExportAliasAssignments(list) { + if (!list || !list.trim()) { + return ''; } - return params; - } - function normalizeMethodMetadata(name, value, descriptors) { - for (var i = 0; i < descriptors.length; i++) { - var descriptor = descriptors[i]; - if (!descriptor || !Array.isArray(descriptor.methods)) { - continue; + var pairs = []; + list.split(',').forEach(function (part) { + var token = part.trim(); + if (!token) { + return; } - for (var j = 0; j < descriptor.methods.length; j++) { - var method = descriptor.methods[j]; - if (method && method.name === name) { - return { - name: method.name, - returnType: method.returnType || method.return_type || 'Void', - parameters: Array.isArray(method.parameters) ? method.parameters : [], - }; - } + var pieces = token.split(/\s+as\s+/i); + if (pieces.length === 2) { + pairs.push('exports.' + pieces[1].trim() + ' = ' + pieces[0].trim() + ';'); + } else { + pairs.push('exports.' + token + ' = ' + token + ';'); } - } + }); - return { - name: name, - returnType: name === 'init' ? 'Void' : 'Object', - parameters: buildFallbackParameterMetadata(value), - }; + return pairs.join('\n'); } - function normalizePropertyMetadata(name, descriptors) { - for (var i = 0; i < descriptors.length; i++) { - var descriptor = descriptors[i]; - if (!descriptor || !Array.isArray(descriptor.properties)) { - continue; - } - for (var j = 0; j < descriptor.properties.length; j++) { - var property = descriptor.properties[j]; - if (property && property.name === name) { - return { - name: property.name, - propType: property.propType || property.prop_type || 'Object', - readable: property.readable !== false, - writable: property.writable !== false, - }; - } - } - } + function transformEsmToRuntimeModule(source) { + var transformed = String(source || ''); - return { - name: name, - propType: 'Object', - readable: true, - writable: true, - }; - } + transformed = transformed.replace(/^[ \t]*import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, + 'const $1 = __nsImport("$2", __filename);'); + transformed = transformed.replace(/^[ \t]*import\s+\{\s*([^}]+)\s*\}\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, + 'const { $1 } = __nsImport("$2", __filename);'); + transformed = transformed.replace(/^[ \t]*import\s+([A-Za-z_$][\w$]*)\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, + 'const $1 = (function(m){ return (m && Object.prototype.hasOwnProperty.call(m, "default")) ? m.default : m; })(__nsImport("$2", __filename));'); + transformed = transformed.replace(/^[ \t]*import\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, + '__nsImport("$1", __filename);'); + transformed = transformed.replace(/\bimport\s*\(/g, '__nsDynamicImport('); - function collectProxyMethods(overrides, descriptors) { - var methods = []; - for (var key in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, key)) { - continue; - } - if (key === 'interfaces') { - continue; - } - if (typeof overrides[key] === 'function') { - methods.push(normalizeMethodMetadata(key, overrides[key], descriptors)); - } - } - methods.sort(function (left, right) { - return String(left && left.name || '').localeCompare(String(right && right.name || '')); + transformed = transformed.replace(/\bexport\s+default\s+/g, 'exports.default = '); + transformed = transformed.replace(/^[ \t]*export\s+\{\s*([^}]+)\s*\};?[ \t]*$/gm, function (_, list) { + return esmExportAliasAssignments(list); }); - return methods; - } - function collectProxyProperties(overrides, descriptors) { - var props = []; - for (var key in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, key)) { - continue; - } - if (key === 'interfaces') { - continue; - } - if (typeof overrides[key] !== 'function') { - props.push(normalizePropertyMetadata(key, descriptors)); - } - } - props.sort(function (left, right) { - return String(left && left.name || '').localeCompare(String(right && right.name || '')); + var exportedNames = []; + transformed = transformed.replace(/^[ \t]*export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/gm, function (_, keyword, name) { + exportedNames.push(name); + return keyword + ' ' + name; + }); + transformed = transformed.replace(/^[ \t]*export\s+function\s+([A-Za-z_$][\w$]*)/gm, function (_, name) { + exportedNames.push(name); + return 'function ' + name; + }); + transformed = transformed.replace(/^[ \t]*export\s+class\s+([A-Za-z_$][\w$]*)/gm, function (_, name) { + exportedNames.push(name); + return 'class ' + name; }); - return props; - } - function safeIdentifier(name) { - return String(name || '') - .replace(/[^A-Za-z0-9_]/g, '_') - .replace(/^([^A-Za-z_])/, '_$1') || 'ProxyType'; - } + if (exportedNames.length > 0) { + transformed += '\n' + exportedNames.map(function (name) { + return 'exports.' + name + ' = ' + name + ';'; + }).join('\n') + '\n'; + } - function autoProxyTypeName(baseCtor) { - var baseType = ctorName(baseCtor) || 'Object'; - var baseShort = safeIdentifier(baseType.split('.').pop()); - return 'NativeScript.Gen.' + baseShort + '_' + (proxyExtensions.length + 1); + return transformed; } - function renderProxyCSharp(meta) { - var typeName = meta.typeName || ('GeneratedProxy' + (proxyExtensions.length + 1)); - var safeTypeName = safeIdentifier(typeName.split('.').pop()); - var baseType = meta.baseType || 'object'; - var methodStubs = ''; - for (var i = 0; i < meta.methods.length; i++) { - var methodMeta = meta.methods[i]; - var methodName = safeIdentifier((methodMeta && methodMeta.name) || methodMeta); - methodStubs += - ' public object __ns_' + methodName + '(params object[] args)\\n' + - ' {\\n' + - ' return ProxyDispatcher.Invoke(this.__proxyId, "' + methodName + '", args);\\n' + - ' }\\n\\n'; + function executeRuntimeModule(source, filename) { + var modulePath = String(filename || ''); + if (!modulePath) { + throw new Error('executeRuntimeModule requires a module path'); + } + if (moduleCache.has(modulePath)) { + return moduleCache.get(modulePath); } - return ( - 'using System;\\n\\n' + - 'namespace NativeScriptGeneratedProxies\\n' + - '{\\n' + - ' public static class ProxyDispatcher\\n' + - ' {\\n' + - ' public static Func JsInvoke;\\n' + - ' public static object Invoke(int id, string method, object[] args)\\n' + - ' {\\n' + - ' var cb = JsInvoke;\\n' + - ' if (cb == null) throw new InvalidOperationException("JsInvoke callback is not registered.");\\n' + - ' return cb(id, method, args);\\n' + - ' }\\n' + - ' }\\n\\n' + - ' public class ' + safeTypeName + ' : ' + baseType + '\\n' + - ' {\\n' + - ' private readonly int __proxyId;\\n\\n' + - ' public ' + safeTypeName + '(int proxyId)\\n' + - ' {\\n' + - ' this.__proxyId = proxyId;\\n' + - ' }\\n\\n' + - methodStubs + - ' }\\n' + - '}\\n' - ); - } + var module = { exports: {} }; + moduleCache.set(modulePath, module.exports); - function renderProxyCsproj(meta) { - var asmName = safeIdentifier((meta.typeName || 'GeneratedProxy').split('.').pop()); - return ( - '\\n' + - ' \\n' + - ' net8.0-windows10.0.19041.0\\n' + - ' ' + asmName + '\\n' + - ' NativeScriptGeneratedProxies\\n' + - ' enable\\n' + - ' disable\\n' + - ' latest\\n' + - ' \\n' + - '\\n' - ); + var transformed = transformEsmToRuntimeModule(source); + var dirname = modulePath.replace(/[\\/][^\\/]*$/, ''); + var executor = new Function('exports', 'module', '__nsImport', '__nsDynamicImport', '__filename', '__dirname', transformed); + executor(module.exports, module, __nsImport, __nsDynamicImport, modulePath, dirname); + moduleCache.set(modulePath, module.exports); + return module.exports; } - function buildProxyMetadata(baseCtor, typeName, overrides, Extended) { - var baseType = ctorName(baseCtor); - var interfaceNames = Array.isArray(overrides.interfaces) - ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) - : []; - var descriptors = [describeWinRTType(baseType)]; - for (var i = 0; i < interfaceNames.length; i++) { - descriptors.push(describeWinRTType(interfaceNames[i])); - } - var namespace = ''; - var className = typeName || ''; - if (typeName) { - var splitIndex = typeName.lastIndexOf('.'); - if (splitIndex >= 0) { - namespace = typeName.slice(0, splitIndex); - className = typeName.slice(splitIndex + 1); - } - } - var meta = { - kind: 'windows-proxy', - typeName: typeName || '', - className: className || safeIdentifier((typeName || baseType || 'GeneratedProxy').split('.').pop()), - namespace: namespace || null, - baseType: baseType, - baseClass: baseType, - interfaces: interfaceNames, - methods: collectProxyMethods(overrides, descriptors.filter(Boolean)), - properties: collectProxyProperties(overrides, descriptors.filter(Boolean)), - isAutoGeneratedName: !typeName, - registeredAt: new Date().toISOString(), - registered: false, - generated: null, - }; - try { - Object.defineProperty(Extended, '__proxyMetadata__', { - value: meta, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Extended.__proxyMetadata__ = meta; - } - proxyExtensions.push(meta); - if (typeof globalThis.__nsProxyAutoCapture === 'function') { - try { - globalThis.__nsProxyAutoCapture(JSON.stringify(proxyExtensions)); - } catch (_) { - // Capture is best-effort; runtime behavior should remain unaffected. - } + function __nsImport(specifier, parentPath) { + if (typeof globalThis.__nsResolveModulePath !== 'function') { + throw new Error('Module resolver host function is not available'); } - - if (meta.interfaces.length > 0) { - if (typeof globalThis.__nsProxyCompileProject === 'function') { - try { registerProxy(meta); } catch (_) { } - } else if (typeof globalThis.__nsProxyWriteTextFile === 'function') { - try { emitProxy(meta); } catch (_) { } - } + if (typeof globalThis.__nsReadTextFile !== 'function') { + throw new Error('Module file reader host function is not available'); } - return meta; - } - function ensureProxyInstance(instance, overrides, ctor) { - if (!instance || typeof instance !== 'object') { - return -1; + var resolved = globalThis.__nsResolveModulePath( + String(specifier || ''), + parentPath ? String(parentPath) : '', + globalThis.__nsAppRoot || '' + ); + if (!resolved) { + throw new Error('Unable to resolve module: ' + specifier); } - - var proxyId = instance.__proxyId; - if (typeof proxyId !== 'number' || !Number.isFinite(proxyId)) { - proxyId = nextProxyId++; - try { - Object.defineProperty(instance, '__proxyId', { - value: proxyId, - writable: false, - configurable: true, - enumerable: false, - }); - } catch (_) { - instance.__proxyId = proxyId; - } + if (moduleCache.has(resolved)) { + return moduleCache.get(resolved); } - proxyInstances.set(proxyId, { - instance: instance, - overrides: overrides, - constructor: ctor, - }); - - return proxyId; + var source = globalThis.__nsReadTextFile(resolved); + return executeRuntimeModule(source, resolved); } - function makeExtendedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { - var hasName = typeof nameOrOverrides === 'string'; - var explicitTypeName = hasName ? nameOrOverrides : ''; - var typeName = explicitTypeName || autoProxyTypeName(baseCtor); - var overrides = hasName ? maybeOverrides : nameOrOverrides; - if (!overrides || typeof overrides !== 'object') { - overrides = {}; + function __nsDynamicImport(specifier, parentPath) { + if (typeof specifier !== 'string' || + (!specifier.startsWith('https://') && !specifier.startsWith('http://'))) { + return Promise.resolve().then(function () { + return __nsImport(specifier, parentPath); + }); } - function Extended() { - var instance; - var args = Array.prototype.slice.call(arguments); + try { + var sec = {}; try { - instance = Reflect.construct(baseCtor, args); - } catch (_) { - instance = {}; - } + var pkgText = globalThis.__nsReadTextFile( + (globalThis.__nsAppRoot || '') + '/package.json' + ); + sec = ((JSON.parse(pkgText) || {}).nativescript || {}).security || {}; + } catch (_) {} - for (var key in overrides) { - if (key === 'interfaces') { - continue; - } - var value = overrides[key]; - if (typeof value === 'function') { - try { - Object.defineProperty(instance, key, { - value: value, - writable: true, - configurable: true, - enumerable: true, - }); - } catch (_) { - instance[key] = value; - } - } else { - instance[key] = value; - } + if (!sec.allowRemoteModules) { + return Promise.reject(new Error( + 'Remote module imports are disabled. ' + + 'Set nativescript.security.allowRemoteModules=true in package.json' + )); } - if (typeof overrides.init === 'function') { - try { - var initResult = overrides.init.apply(instance, args); - if (initResult && typeof initResult === 'object') { - instance = initResult; - } - } catch (_) { - // Keep constructor resilient; init errors should not crash runtime. + var allowlist = sec.remoteModuleAllowlist; + if (Array.isArray(allowlist) && allowlist.length > 0) { + var isAllowed = allowlist.some(function (p) { + return typeof p === 'string' && specifier.startsWith(p); + }); + if (!isAllowed) { + return Promise.reject(new Error( + 'Remote module URL is not in the allowlist: ' + specifier + )); } } - if (Array.isArray(overrides.interfaces)) { - try { - Object.defineProperty(instance, '__interfaces__', { - value: overrides.interfaces.slice(), - writable: false, - configurable: true, - enumerable: false, - }); - } catch (_) { - instance.__interfaces__ = overrides.interfaces.slice(); - } + if (moduleCache.has(specifier)) { + return Promise.resolve(moduleCache.get(specifier)); } - ensureProxyInstance(instance, overrides, Extended); + var uri = new Windows.Foundation.Uri(specifier); + var client = new Windows.Web.Http.HttpClient(); - return instance; + return NSWinRT.toPromise(client.getStringAsync(uri)).then(function (source) { + return executeRuntimeModule(String(source), specifier); + }); + } catch (e) { + return Promise.reject(e instanceof Error ? e : new Error(String(e))); } + } - Extended.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); - Extended.prototype.constructor = Extended; + globalThis.__nsInvalidateModuleCacheEntry = function (resolvedPath) { + moduleCache.delete(String(resolvedPath || '')); + }; - for (var protoKey in overrides) { - if (protoKey === 'interfaces') { - continue; - } - Extended.prototype[protoKey] = overrides[protoKey]; - } + globalThis.__nsClearModuleCache = function () { + moduleCache.clear(); + }; - if (typeName) { - try { - Object.defineProperty(Extended, 'name', { - value: typeName, - configurable: true, - }); - } catch (_) { - // Non-critical metadata assignment. - } - } + globalThis.__nsEvalAsModule = function (source, filename) { + return executeRuntimeModule(source, filename); + }; + globalThis.NSWinRT.import = __nsImport; + globalThis.__nsDynamicImport = __nsDynamicImport; + globalThis.NSWinRT.dynamicImport = __nsDynamicImport; + })(); - Extended.__typeName__ = typeName || ctorName(baseCtor); - Extended.__nsWinRTClass__ = true; - var metadata = buildProxyMetadata(baseCtor, typeName, overrides, Extended); + globalThis.__runtimeVersion = "1.0.0"; - Extended.extend = function (nextNameOrOverrides, nextMaybeOverrides) { - return makeExtendedConstructor(Extended, nextNameOrOverrides, nextMaybeOverrides); + // The runtime registers host-facing timer entrypoints into the JS global: + // global.__ns__setTimeout + // global.__ns__setInterval + // global.__ns__clearTimeout + // global.__ns__clearInterval + // Embedders may replace these with + // platform-specific implementations. When no host implementation is + // available, the bootstrap provides default behaviors (throws for + // setTimeout/setInterval and no-op for clearTimeout/clearInterval). + (function () { + if (typeof globalThis.__ns__setTimeout !== 'function') { + globalThis.__ns__setTimeout = function () { + throw new Error('Timers are not available: embedder must provide global.__ns__setTimeout'); }; - - try { - Object.defineProperty(Extended, 'emitProxy', { - value: function (outDir) { - return NSWinRT.proxy.emit(metadata, outDir); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Extended.emitProxy = function (outDir) { - return NSWinRT.proxy.emit(metadata, outDir); - }; - } - - return Extended; - } - - if (typeof Function.prototype.extend !== 'function') { - Object.defineProperty(Function.prototype, 'extend', { - value: function (nameOrOverrides, maybeOverrides) { - // A WinRT class (or a JS class deriving from one): an ES2015 subclass, so - // instances get the composed-object construction of `class X extends Y`. - if (globalThis.NSWinRT && typeof globalThis.NSWinRT.extendClass === 'function') { - if (typeof this.__typeName__ === 'string') - return globalThis.NSWinRT.extendClass(this, nameOrOverrides, maybeOverrides); - // Object.extend({ interfaces: [...], ... }): a subclass of System.Object. - if (this === Object && globalThis.System) - return globalThis.NSWinRT.extendClass(globalThis.System.Object, nameOrOverrides, maybeOverrides); - } - return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - if (typeof Object.extend !== 'function') { - Object.defineProperty(Object, 'extend', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // Create a managed-backed subclass factory. This returns a constructor - // that, when invoked, asks the managed bridge to instantiate a real - // C# subclass whose vtable forwards virtual calls back into the - // provided JS `overrides` object via the DotNet bridge. - function makeManagedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { - var hasName = typeof nameOrOverrides === 'string'; - var explicitTypeName = hasName ? nameOrOverrides : ''; - var typeName = explicitTypeName || autoProxyTypeName(baseCtor); - var overrides = hasName ? maybeOverrides : nameOrOverrides; - if (!overrides || typeof overrides !== 'object') overrides = {}; - - function Managed() { - var args = Array.prototype.slice.call(arguments); - // Pass the actual base ctor type name as the assembly param so the - // bridge can fall back to it when no static proxy exists for typeName. - var baseTypeName = ctorName(baseCtor) || ''; - var obj = NSWinRT.proxy.createManagedSubclass(baseTypeName, typeName || '', overrides); - if (typeof overrides.init === 'function') { - try { - var initResult = overrides.init.apply(obj, args); - if (initResult && typeof initResult === 'object') obj = initResult; - } catch (_) { } - } - return obj; - } - - Managed.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); - Managed.prototype.constructor = Managed; - Managed.extend = function (nextNameOrOverrides, nextMaybeOverrides) { - return makeManagedConstructor(Managed, nextNameOrOverrides, nextMaybeOverrides); - }; - try { - Object.defineProperty(Managed, 'name', { value: typeName || ctorName(baseCtor), configurable: true }); - } catch (_) { } - Managed.__typeName__ = typeName || ctorName(baseCtor); - Managed.__nsWinRTClass__ = true; - var meta = buildProxyMetadata(baseCtor, typeName, overrides, Managed); - try { - Object.defineProperty(Managed, 'emitProxy', { - value: function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Managed.emitProxy = function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }; - } - return Managed; - } - - if (typeof Function.prototype.extendManaged !== 'function') { - Object.defineProperty(Function.prototype, 'extendManaged', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - if (typeof Object.extendManaged !== 'function') { - Object.defineProperty(Object, 'extendManaged', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // Convenience alias for hosts that expect a global BaseClass helper. - globalThis.BaseClass = globalThis.BaseClass || {}; - if (typeof globalThis.BaseClass.extend !== 'function') { - globalThis.BaseClass.extend = function (baseCtor, nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(baseCtor || Object, nameOrOverrides, maybeOverrides); - }; - } - - // Registry mapping each WinRT-extended prototype to its lazy factory. - // Keyed by the new Child.prototype created in __extends_winrt so that - // the Parent.apply override can look up the right factory per subclass. - var __nsExtendRegistry__ = new WeakMap(); - - function __extends_winrt(Child, Parent) { - var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; - extendStatics(Child, Parent); - - // Lazy factory: deferred until first `new Child()` so that TypeScript - // has had a chance to populate Child.prototype with override methods. - var _extended = null; - var getExtended = function () { - if (!_extended) { - var overrides = {}; - var interfaces = []; - var proto = Child.prototype; - if (proto) { - Object.getOwnPropertyNames(proto).forEach(function (key) { - if (key === 'constructor') return; - if (key === 'interfaces') { - var iv = proto[key]; - if (Array.isArray(iv)) interfaces = iv; - return; - } - overrides[key] = proto[key]; - }); - } - if (interfaces.length) overrides.interfaces = interfaces; - var name = Child.__winrtProxyName__ || Child.name || ''; - var hasBridge = typeof globalThis.__nsDotNetCreateJsSubclass === 'function'; - if (Child.__winrtProxyName__ || (interfaces.length > 0 && hasBridge)) { - _extended = makeManagedConstructor(Parent, name, overrides); - } else { - // No bridge or no interfaces: pure JS proxy via Reflect.construct. - _extended = makeExtendedConstructor(Parent, name, overrides); - } - } - return _extended; - }; - - var newProto = Object.create((Parent && Parent.prototype) || Object.prototype); - newProto.constructor = Child; - Child.prototype = newProto; - __nsExtendRegistry__.set(newProto, getExtended); - - // Patch Parent.apply/call once per parent class. The WeakMap lookup - // inside the override picks the correct factory per subclass so that - // multiple classes extending the same parent coexist correctly. - if (!Parent.__nsApplyPatched__) { - Parent.__nsApplyPatched__ = true; - - Parent.apply = function (thiz, args) { - if (thiz && thiz.__nsContainer__) return; - var factory = __nsExtendRegistry__.get(Object.getPrototypeOf(thiz)); - if (!factory) return; - thiz.__nsContainer__ = true; - try { - var Extended = factory(); - var argArr = Array.isArray(args) ? args : (args ? Array.prototype.slice.call(args) : []); - if (argArr.length > 0) { - thiz.__proto__ = new (Function.prototype.bind.apply(Extended, [null].concat(argArr)))(); - } else { - thiz.__proto__ = new Extended(); - } - return thiz.__proto__; - } finally { - delete thiz.__nsContainer__; - } - }; - - Parent.call = function (thiz) { - return Parent.apply(thiz, Array.prototype.slice.call(arguments, 1)); - }; - } - } - - // Global __extends that TypeScript compiled output resolves via - // `var __extends = (this && this.__extends) || ...`. - // Intercepts WinRT parent classes; falls back to standard TS behaviour. - if (typeof globalThis.__extends !== 'function') { - var __extends_ts_inner = function (d, b) { - if (b !== null && typeof b !== 'function') { - throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); - } - var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; - - globalThis.__extends = function (d, b) { - if (b && b.__nsWinRTClass__) { - __extends_winrt(d, b); - } else { - __extends_ts_inner(d, b); - } - }; - } - - // TypeScript decorator helpers, as the iOS/Android runtimes provide them: compiled - // (`experimentalDecorators`) code calls a bare `__decorate(...)`. Core's globals module - // binds tslib's copies too, but an ES-module bundle can evaluate decorated core modules - // before it runs. - if (typeof globalThis.__decorate !== 'function') { - globalThis.__decorate = function (decorators, target, key, desc) { - var c = arguments.length; - var r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; - if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') { - r = Reflect.decorate(decorators, target, key, desc); - } else { - for (var i = decorators.length - 1; i >= 0; i--) { - if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - } - } - return c > 3 && r && Object.defineProperty(target, key, r), r; - }; - } - if (typeof globalThis.__param !== 'function') { - globalThis.__param = function (paramIndex, decorator) { - return function (target, key) { decorator(target, key, paramIndex); }; - }; - } - if (typeof globalThis.__metadata !== 'function') { - globalThis.__metadata = function (metadataKey, metadataValue) { - if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') { - return Reflect.metadata(metadataKey, metadataValue); - } - }; - } - - // `return global.__native(this)` ends TypeScript subclass constructors written for - // Android/iOS; here `this` already is the native-backed object. - if (typeof globalThis.__native !== 'function') { - globalThis.__native = function (obj) { return obj; }; - } - - // @NativeClass / @NativeClass(): marks a class that extends a native (WinRT/.NET) class. - // Extending works without it; it exists so code written for iOS/Android, where the build - // transforms decorated classes, runs unchanged. - if (typeof globalThis.NativeClass !== 'function') { - Object.defineProperty(globalThis, 'NativeClass', { - value: function NativeClass(target) { - if (typeof target === 'function') return target; - return function (cls) { return cls; }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // @Interfaces([IFoo, IBar]) — attach WinRT interface list to a class. - if (typeof globalThis.Interfaces !== 'function') { - Object.defineProperty(globalThis, 'Interfaces', { - value: function Interfaces(interfacesList) { - return function (target) { - target.prototype.interfaces = Array.isArray(interfacesList) ? interfacesList.slice() : []; - return target; - }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - - if (typeof globalThis.CSharpProxy !== 'function') { - Object.defineProperty(globalThis, 'CSharpProxy', { - value: function CSharpProxy(name) { - return function (target) { - target.__winrtProxyName__ = typeof name === 'string' ? name : ''; - return target; - }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - function defaultProxyOutDir(meta) { - var typeName = (meta && meta.typeName) ? meta.typeName : 'GeneratedProxy'; - var safe = safeIdentifier(typeName.split('.').pop()); - return './generated-proxies/' + safe; - } - - function emitProxy(meta, outDir) { - if (!meta || typeof meta !== 'object') { - throw new Error('NSWinRT.proxy.emit(meta[, outDir]) expects a proxy metadata object'); - } - if (typeof globalThis.__nsProxyWriteTextFile !== 'function') { - throw new Error('Host proxy file emitter is not available'); - } - - var dir = outDir || defaultProxyOutDir(meta); - var csprojPath = dir + '/Proxy.csproj'; - var csPath = dir + '/Proxy.g.cs'; - var csproj = renderProxyCsproj(meta); - var source = renderProxyCSharp(meta); - - globalThis.__nsProxyWriteTextFile(csprojPath, csproj); - globalThis.__nsProxyWriteTextFile(csPath, source); - - meta.generated = { - dir: dir, - csprojPath: csprojPath, - csPath: csPath, - }; - - return meta.generated; - } - - function compileProxy(meta, outDir, configuration) { - if (typeof globalThis.__nsProxyCompileProject !== 'function') { - throw new Error('Host proxy compiler is not available'); - } - var generated = emitProxy(meta, outDir); - var result = globalThis.__nsProxyCompileProject(generated.csprojPath, configuration || 'Debug'); - generated.build = result; - return generated; - } - - function registerProxy(meta, outDir, configuration) { - var generated = compileProxy(meta, outDir, configuration); - var manifest = { - kind: 'windows-proxy', - typeName: meta.typeName, - baseType: meta.baseType, - interfaces: meta.interfaces, - methods: meta.methods, - properties: meta.properties, - generated: generated, - registration: { - hostCanLoadAssemblies: false, - note: 'Assembly build succeeded, but runtime CLR proxy activation is not wired yet. Dynamic JS fallback remains active.', - }, - }; - if (typeof globalThis.__nsProxyRegisterManifest === 'function') { - globalThis.__nsProxyRegisterManifest(JSON.stringify(manifest)); - } - meta.registered = true; - meta.registration = manifest.registration; - return manifest; - } - - function invokeProxyById(proxyId, methodName, argsArray) { - var entry = proxyInstances.get(proxyId); - if (!entry) { - throw new Error('Proxy instance not found for id ' + proxyId); - } - - var target = entry.instance; - var method = target && target[methodName]; - if (typeof method !== 'function') { - throw new Error('Proxy method "' + methodName + '" is not defined on proxy id ' + proxyId); - } - - return method.apply(target, Array.isArray(argsArray) ? argsArray : []); - } - - globalThis.__nsInvokeProxyJs = invokeProxyById; - - globalThis.NSWinRT.proxy = { - getExtensions: function () { - return proxyExtensions.slice(); - }, - emit: emitProxy, - compile: compileProxy, - register: registerProxy, - createManagedSubclass: function (assembly, typeName, overrides) { - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') { - throw new Error('__nsDotNetCreateJsSubclass is not available in this runtime'); - } - if (!overrides || typeof overrides !== 'object') overrides = {}; - - var interfaceNames = Array.isArray(overrides.interfaces) - ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) - : []; - - // Literal CLR member names this JS object actually overrides. The bridge only - // emits an IL override for base virtuals named here (everything else falls - // through to the real base implementation) and implements exactly these - // interface members from JS (others fall back to default/no-op). Plain - // functions are method overrides; accessor descriptors become get_/set_- - // prefixed property overrides — matching the literal CLR accessor names the - // dynamic proxy already dispatches by. Read via getOwnPropertyDescriptor - // throughout so collecting names never invokes a JS getter as a side effect. - var memberNames = []; - for (var mkey in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, mkey)) continue; - if (mkey === 'interfaces' || mkey === 'init') continue; - var mdesc = Object.getOwnPropertyDescriptor(overrides, mkey); - if (!mdesc) continue; - if (typeof mdesc.get === 'function' || typeof mdesc.set === 'function') { - if (typeof mdesc.get === 'function') memberNames.push('get_' + mkey); - if (typeof mdesc.set === 'function') memberNames.push('set_' + mkey); - } else if (typeof mdesc.value === 'function') { - memberNames.push(mkey); - } - } - - var dispatcher = function () { - var a = Array.prototype.slice.call(arguments); - try { - var target = _wrapDotNetHandle(a[0]); - var method = a[1]; - var margs = a.slice(2).map(_wrapDotNetHandle); - if (target && typeof method === 'string') { - var fn = overrides[method]; - if (typeof fn === 'function') return fn.apply(target, Array.isArray(margs) ? margs : []); - } - } catch (_) { } - }; - var handle = globalThis.__nsDotNetCreateJsSubclass(assembly || '', typeName || '', interfaceNames, memberNames, dispatcher); - var obj = _wrapDotNetHandle(handle); - ensureProxyInstance(obj, overrides, function () { }); - return obj; - }, - invokeById: invokeProxyById, - listRegisteredManifests: function () { - if (typeof globalThis.__nsProxyListManifests === 'function') { - return globalThis.__nsProxyListManifests(); - } - return []; - }, - }; - - function asDelegate(handler) { - if (typeof handler === 'function') { - return handler; - } - if (handler && typeof handler.invoke === 'function') { - return handler.invoke.bind(handler); - } - throw new Error('NSWinRT.asDelegate(handler) expects a function or { invoke() } object'); - } - - function createEventEmitter() { - var listeners = []; - return { - add: function (handler) { - var normalized = asDelegate(handler); - listeners.push(normalized); - return { - dispose: function () { - var idx = listeners.indexOf(normalized); - if (idx >= 0) { - listeners.splice(idx, 1); - } - }, - }; - }, - emit: function () { - var args = Array.prototype.slice.call(arguments); - listeners.slice().forEach(function (listener) { - listener.apply(undefined, args); - }); - }, - count: function () { - return listeners.length; - }, - }; - } - - globalThis.NSWinRT.asDelegate = asDelegate; - globalThis.NSWinRT.createEventEmitter = createEventEmitter; - - var moduleCache = new Map(); - - function esmExportAliasAssignments(list) { - if (!list || !list.trim()) { - return ''; - } - - var pairs = []; - list.split(',').forEach(function (part) { - var token = part.trim(); - if (!token) { - return; - } - var pieces = token.split(/\s+as\s+/i); - if (pieces.length === 2) { - pairs.push('exports.' + pieces[1].trim() + ' = ' + pieces[0].trim() + ';'); - } else { - pairs.push('exports.' + token + ' = ' + token + ';'); - } - }); - - return pairs.join('\n'); - } - - function transformEsmToRuntimeModule(source) { - var transformed = String(source || ''); - - transformed = transformed.replace(/^[ \t]*import\s+\*\s+as\s+([A-Za-z_$][\w$]*)\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, - 'const $1 = __nsImport("$2", __filename);'); - transformed = transformed.replace(/^[ \t]*import\s+\{\s*([^}]+)\s*\}\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, - 'const { $1 } = __nsImport("$2", __filename);'); - transformed = transformed.replace(/^[ \t]*import\s+([A-Za-z_$][\w$]*)\s+from\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, - 'const $1 = (function(m){ return (m && Object.prototype.hasOwnProperty.call(m, "default")) ? m.default : m; })(__nsImport("$2", __filename));'); - transformed = transformed.replace(/^[ \t]*import\s+['\"]([^'\"]+)['\"];?[ \t]*$/gm, - '__nsImport("$1", __filename);'); - transformed = transformed.replace(/\bimport\s*\(/g, '__nsDynamicImport('); - - transformed = transformed.replace(/\bexport\s+default\s+/g, 'exports.default = '); - transformed = transformed.replace(/^[ \t]*export\s+\{\s*([^}]+)\s*\};?[ \t]*$/gm, function (_, list) { - return esmExportAliasAssignments(list); - }); - - var exportedNames = []; - transformed = transformed.replace(/^[ \t]*export\s+(const|let|var)\s+([A-Za-z_$][\w$]*)/gm, function (_, keyword, name) { - exportedNames.push(name); - return keyword + ' ' + name; - }); - transformed = transformed.replace(/^[ \t]*export\s+function\s+([A-Za-z_$][\w$]*)/gm, function (_, name) { - exportedNames.push(name); - return 'function ' + name; - }); - transformed = transformed.replace(/^[ \t]*export\s+class\s+([A-Za-z_$][\w$]*)/gm, function (_, name) { - exportedNames.push(name); - return 'class ' + name; - }); - - if (exportedNames.length > 0) { - transformed += '\n' + exportedNames.map(function (name) { - return 'exports.' + name + ' = ' + name + ';'; - }).join('\n') + '\n'; - } - - return transformed; - } - - function executeRuntimeModule(source, filename) { - var modulePath = String(filename || ''); - if (!modulePath) { - throw new Error('executeRuntimeModule requires a module path'); - } - if (moduleCache.has(modulePath)) { - return moduleCache.get(modulePath); - } - - var module = { exports: {} }; - moduleCache.set(modulePath, module.exports); - - var transformed = transformEsmToRuntimeModule(source); - var dirname = modulePath.replace(/[\\/][^\\/]*$/, ''); - var executor = new Function('exports', 'module', '__nsImport', '__nsDynamicImport', '__filename', '__dirname', transformed); - executor(module.exports, module, __nsImport, __nsDynamicImport, modulePath, dirname); - moduleCache.set(modulePath, module.exports); - return module.exports; - } - - function __nsImport(specifier, parentPath) { - if (typeof globalThis.__nsResolveModulePath !== 'function') { - throw new Error('Module resolver host function is not available'); - } - if (typeof globalThis.__nsReadTextFile !== 'function') { - throw new Error('Module file reader host function is not available'); - } - - var resolved = globalThis.__nsResolveModulePath( - String(specifier || ''), - parentPath ? String(parentPath) : '', - globalThis.__nsAppRoot || '' - ); - if (!resolved) { - throw new Error('Unable to resolve module: ' + specifier); - } - if (moduleCache.has(resolved)) { - return moduleCache.get(resolved); - } - - var source = globalThis.__nsReadTextFile(resolved); - return executeRuntimeModule(source, resolved); - } - - function __nsDynamicImport(specifier, parentPath) { - if (typeof specifier !== 'string' || - (!specifier.startsWith('https://') && !specifier.startsWith('http://'))) { - return Promise.resolve().then(function () { - return __nsImport(specifier, parentPath); - }); - } - - try { - var sec = {}; - try { - var pkgText = globalThis.__nsReadTextFile( - (globalThis.__nsAppRoot || '') + '/package.json' - ); - sec = ((JSON.parse(pkgText) || {}).nativescript || {}).security || {}; - } catch (_) {} - - if (!sec.allowRemoteModules) { - return Promise.reject(new Error( - 'Remote module imports are disabled. ' + - 'Set nativescript.security.allowRemoteModules=true in package.json' - )); - } - - var allowlist = sec.remoteModuleAllowlist; - if (Array.isArray(allowlist) && allowlist.length > 0) { - var isAllowed = allowlist.some(function (p) { - return typeof p === 'string' && specifier.startsWith(p); - }); - if (!isAllowed) { - return Promise.reject(new Error( - 'Remote module URL is not in the allowlist: ' + specifier - )); - } - } - - if (moduleCache.has(specifier)) { - return Promise.resolve(moduleCache.get(specifier)); - } - - var uri = new Windows.Foundation.Uri(specifier); - var client = new Windows.Web.Http.HttpClient(); - - return NSWinRT.toPromise(client.getStringAsync(uri)).then(function (source) { - return executeRuntimeModule(String(source), specifier); - }); - } catch (e) { - return Promise.reject(e instanceof Error ? e : new Error(String(e))); - } - } - - globalThis.__nsInvalidateModuleCacheEntry = function (resolvedPath) { - moduleCache.delete(String(resolvedPath || '')); - }; - - globalThis.__nsClearModuleCache = function () { - moduleCache.clear(); - }; - - globalThis.__nsEvalAsModule = function (source, filename) { - return executeRuntimeModule(source, filename); - }; - globalThis.NSWinRT.import = __nsImport; - globalThis.__nsDynamicImport = __nsDynamicImport; - globalThis.NSWinRT.dynamicImport = __nsDynamicImport; - })(); - - globalThis.__runtimeVersion = "1.0.0"; - - // The runtime registers host-facing timer entrypoints into the JS global: - // global.__ns__setTimeout - // global.__ns__setInterval - // global.__ns__clearTimeout - // global.__ns__clearInterval - // Embedders may replace these with - // platform-specific implementations. When no host implementation is - // available, the bootstrap provides default behaviors (throws for - // setTimeout/setInterval and no-op for clearTimeout/clearInterval). - (function () { - if (typeof globalThis.__ns__setTimeout !== 'function') { - globalThis.__ns__setTimeout = function () { - throw new Error('Timers are not available: embedder must provide global.__ns__setTimeout'); - }; - } - if (typeof globalThis.__ns__setInterval !== 'function') { - globalThis.__ns__setInterval = function () { - throw new Error('Timers are not available: embedder must provide global.__ns__setInterval'); - }; - } - if (typeof globalThis.__ns__clearTimeout !== 'function') { - globalThis.__ns__clearTimeout = function () { }; - } - if (typeof globalThis.__ns__clearInterval !== 'function') { - globalThis.__ns__clearInterval = function () { }; - } - })(); - - // Callbacks run once per frame from the host's pump (runtime_pump_timers, driven by - // CompositionTarget.Rendering), never by blocking the UI thread on vsync: see - // runtime/src/animation_frames.rs. Callbacks requested during a frame run in the next. - (function () { - var _nextId = 0; - var _pending = new Map(); - - globalThis.requestAnimationFrame = function requestAnimationFrame(callback) { - if (typeof callback !== 'function') return 0; - var id = ++_nextId; - _pending.set(id, callback); - if (_pending.size === 1) __nsRequestFrame(); - return id; - }; - - globalThis.cancelAnimationFrame = function cancelAnimationFrame(id) { - _pending.delete(id); - }; - - globalThis.__nsRunAnimationFrames = function (ts) { - if (_pending.size === 0) return; - var cbs = Array.from(_pending.values()); - _pending.clear(); - for (var i = 0; i < cbs.length; i++) { - try { cbs[i](ts); } catch (e) { - console.log('rAF error:', e && e.message || e); - } - } - }; - })(); - - // The native classes are installed by install_url_globals() before this - // script runs. This block adds the JS-layer searchParams accessor - // (mutation propagation + caching), Symbol.iterator, and the blob-URL API. - (function () { - // searchParams: lazily created, mutations propagate back to URL.search - Object.defineProperty(URL.prototype, 'searchParams', { - get: function () { - if (this._searchParams == null) { - var self = this; - var sp = new URLSearchParams(this.search); - Object.defineProperty(sp, '_url', { enumerable: false, writable: false, value: self }); - var _append = sp.append.bind(sp); - sp.append = function (name, value) { _append(name, value); self.search = sp.toString(); }; - var _delete = sp.delete.bind(sp); - sp.delete = function (name, value) { _delete(name, value); self.search = sp.toString(); }; - var _set = sp.set.bind(sp); - sp.set = function (name, value) { _set(name, value); self.search = sp.toString(); }; - var _sort = sp.sort.bind(sp); - sp.sort = function () { _sort(); self.search = sp.toString(); }; - this._searchParams = sp; - } - return this._searchParams; - }, - configurable: true, - }); - - // Make URLSearchParams iterable (entries by default) - URLSearchParams.prototype[Symbol.iterator] = URLSearchParams.prototype.entries; - - var BLOB_STORE = new Map(); - var _blobSeq = 0; - URL.createObjectURL = function (object, options) { - if (!(object instanceof globalThis.Blob)) return null; - var id = (++_blobSeq).toString(16).padStart(8, '0') + '-' - + Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0') + '-4' - + Math.floor(Math.random() * 0x1000).toString(16).padStart(3, '0') + '-' - + (Math.floor(Math.random() * 4) + 8).toString(16) - + Math.floor(Math.random() * 0x1000).toString(16).padStart(3, '0') + '-' - + Math.floor(Math.random() * 0x1000000000000).toString(16).padStart(12, '0'); - var url = 'blob:nativescript/' + id; - BLOB_STORE.set(url, { blob: object, type: object.type, ext: options && options.ext }); - return url; - }; - URL.revokeObjectURL = function (url) { BLOB_STORE.delete(url); }; - URL.InternalAccessor = { - getData: function (url) { return BLOB_STORE.get(url) || null; }, - }; - })(); - - // ada-url (C++) doesn't expose URLPattern; this JS polyfill covers the - // common cases: :name capture groups, * / ** wildcards, literal segments. - (function () { - if (typeof URLPattern !== 'undefined') return; - - function _esc(s) { - return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); - } - - function _compile(pat) { - if (pat === undefined || pat === null) pat = '*'; - var s = String(pat); - var groups = []; - var re = ''; - var i = 0; - while (i < s.length) { - var c = s[i]; - if (c === ':') { - var j = i + 1; - while (j < s.length && /\w/.test(s[j])) j++; - var name = s.slice(i + 1, j); - groups.push(name); - re += '([^/?#]+?)'; - i = j; - } else if (c === '*' && s[i + 1] === '*') { - groups.push('0'); - re += '(.*)'; - i += 2; - } else if (c === '*') { - groups.push('0'); - re += '([^/?#]*)'; - i++; - } else { - re += _esc(c); - i++; - } - } - return { re: new RegExp('^' + re + '$'), groups: groups }; - } - - function _match(compiled, value) { - var m = String(value || '').match(compiled.re); - if (!m) return null; - var groups = {}; - for (var i = 0; i < compiled.groups.length; i++) { - groups[compiled.groups[i]] = m[i + 1] !== undefined ? m[i + 1] : undefined; - } - return { input: String(value || ''), groups: groups }; - } - - function URLPattern(init, baseURL) { - if (typeof init === 'string') { - try { - var u = new URL(init, baseURL || 'http://_placeholder_'); - init = { - protocol: u.protocol.slice(0, -1), - hostname: u.hostname, - port: u.port, - pathname: u.pathname, - search: u.search ? u.search.slice(1) : '', - hash: u.hash ? u.hash.slice(1) : '', - }; - } catch (e) { - init = { pathname: init }; - } - } - init = init || {}; - var opt = function (v, d) { return v !== undefined && v !== null ? String(v) : (d !== undefined ? d : '*'); }; - this.protocol = opt(init.protocol); - this.username = opt(init.username); - this.password = opt(init.password); - this.hostname = opt(init.hostname); - this.port = opt(init.port); - this.pathname = opt(init.pathname, '/*'); - this.search = opt(init.search); - this.hash = opt(init.hash); - this._pc = _compile(this.protocol); - this._uc = _compile(this.username); - this._pw = _compile(this.password); - this._hc = _compile(this.hostname); - this._oc = _compile(this.port); - this._ac = _compile(this.pathname); - this._sc = _compile(this.search); - this._xc = _compile(this.hash); } - - URLPattern.prototype.test = function (input, baseURL) { - return this.exec(input, baseURL) !== null; - }; - - URLPattern.prototype.exec = function (input, baseURL) { - var url; - try { - if (typeof input === 'string') { - url = new URL(input, baseURL || undefined); - } else if (input && input.href) { - url = new URL(input.href); - } else { - url = input || {}; - } - } catch (e) { return null; } - - var rm = _match(this._pc, url.protocol ? url.protocol.slice(0, -1) : ''); - var um = _match(this._uc, url.username || ''); - var pm = _match(this._pw, url.password || ''); - var hm = _match(this._hc, url.hostname || ''); - var om = _match(this._oc, url.port || ''); - var am = _match(this._ac, url.pathname || '/'); - var sm = _match(this._sc, url.search ? url.search.slice(1) : ''); - var xm = _match(this._xc, url.hash ? url.hash.slice(1) : ''); - - if (!rm || !um || !pm || !hm || !om || !am || !sm || !xm) return null; - return { - inputs: [input], - protocol: rm, username: um, password: pm, - hostname: hm, port: om, pathname: am, - search: sm, hash: xm, + if (typeof globalThis.__ns__setInterval !== 'function') { + globalThis.__ns__setInterval = function () { + throw new Error('Timers are not available: embedder must provide global.__ns__setInterval'); }; - }; - - globalThis.URLPattern = URLPattern; - })(); - - // Requires the dotnet-bridge project to be published into - // /dotnet-bridge/publish/DotNetBridge.dll - (function () { - if (typeof globalThis.__nsDotNetInvokeBin !== 'function') return; - - // Binary protocol: no JSON.stringify / JSON.parse. - // __nsDotNetInvokeBin(handle, typeName, assembly, method, ...args) - // throws on error, returns the result value directly. - function _invoke(req) { - var handle = (req.handle !== undefined && req.handle !== null) ? req.handle : -1; - var args = req.args; - var typeName = req.typeName || '', assembly = req.assembly || '', method = req.method || ''; - if (!args || args.length === 0) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method); - if (args.length === 1) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, args[0]); - return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, ...args); - } - - // _invoke with a function's own `arguments`, unwrapped straight into the native call (no - // request object or intermediate arrays). - function _invokeWith(handle, typeName, assembly, method, args) { - var invoke = globalThis.__nsDotNetInvokeBin; - switch (args.length) { - case 0: return invoke(handle, typeName, assembly, method); - case 1: return invoke(handle, typeName, assembly, method, _unwrap(args[0])); - case 2: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1])); - case 3: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1]), _unwrap(args[2])); - } - // Built by push so the array stays packed: apply on a holey array takes V8's slow path. - var call = [handle, typeName, assembly, method]; - for (var i = 0; i < args.length; i++) call.push(_unwrap(args[i])); - return invoke.apply(null, call); - } - - // Populated lazily on first access; avoids repeated bridge round-trips. - var _typeInfoCache = {}; - // A type's member lists get `kinds` (member name -> _K_* flags), so the proxy traps look a name up - // instead of scanning the lists on every access. - var _K_PROP = 1, _K_WO = 2, _K_RO = 4, _K_SPROP = 8, _K_SMETHOD = 16, _K_SWO = 32, _K_SRO = 64; - function _withKinds(info) { - var kinds = Object.create(null); - function add(list, flag) { - if (!list) return; - for (var i = 0; i < list.length; i++) kinds[list[i]] = (kinds[list[i]] | 0) | flag; - } - add(info.properties, _K_PROP); - add(info.writeonlyProperties, _K_WO); - add(info.readonlyProperties, _K_RO); - add(info.staticProperties, _K_SPROP); - add(info.staticMethods, _K_SMETHOD); - add(info.writeonlyStaticProperties, _K_SWO); - add(info.readonlyStaticProperties, _K_SRO); - info.kinds = kinds; - return info; - } - function _kindsOf(info) { return info.kinds || _withKinds(info).kinds; } - - // "get_X" / "set_X" accessor names, made once per property name. - var _getterNames = Object.create(null), _setterNames = Object.create(null); - function _getterName(prop) { return _getterNames[prop] || (_getterNames[prop] = 'get_' + prop); } - function _setterName(prop) { return _setterNames[prop] || (_setterNames[prop] = 'set_' + prop); } - - // The native COM pointer of a handle's object as the BigInt the runtime reads from `__native_ptr`, - // or undefined when it has none. The bridge makes it for a plain .NET object only on request. - function _nativePtrOf(handle) { - try { - var p = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNativePtrForHandle', args: [handle] }); - if (typeof p !== 'number' || p === 0) return undefined; - return typeof BigInt === 'function' ? BigInt(p) : p; - } catch (_) { - return undefined; - } - } - var _emptyInfo = _withKinds({ methods: [], properties: [], staticMethods: [], staticProperties: [], readonlyProperties: [], readonlyStaticProperties: [], writeonlyProperties: [], writeonlyStaticProperties: [] }); - // Optional mapping for namespace prefixes -> assembly simple-name. - // Exact namespaces are preferred first, then progressively shorter - // prefixes are tried as a fallback. - var _namespaceAssemblyMap = Object.create(null); - - // _resolveAssembly results by type name, cleared whenever the namespace map changes. - var _assemblyCache = Object.create(null); - - function _resolveAssembly(typeName) { - if (!typeName || typeof typeName !== 'string') return ''; - var cached = _assemblyCache[typeName]; - if (cached !== undefined) return cached; - var resolved = ''; - var probe = typeName; - while (probe) { - var assembly = _namespaceAssemblyMap[probe]; - if (typeof assembly === 'string' && assembly) { resolved = assembly; break; } - var lastDot = probe.lastIndexOf('.'); - if (lastDot < 0) break; - probe = probe.substring(0, lastDot); - } - _assemblyCache[typeName] = resolved; - return resolved; - } - - // When the JS GC collects a DotNet proxy the registry fires the - // callback with the managed handle id, releasing the CLR reference. - // Explicit sw.release() still works for deterministic teardown. - var _dotNetFinalizers = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(function (handle) { - try { _invoke({ handle: handle, method: '__release', args: [] }); } catch (e) {} - }) - : null; - - // Auto-populate namespace→assembly map from the managed side. - // GetNamespaceAssemblyMapJson scans the app directory for assemblies and - // returns a JSON map of { rootNamespace: assemblySimpleName }. - // Runs once at IIFE init; any failure is silently ignored. - try { - var _autoNsJson = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNamespaceAssemblyMapJson', args: [] }); - if (typeof _autoNsJson === 'string' && _autoNsJson) { - var _parsed = JSON.parse(_autoNsJson); - if (_parsed && typeof _parsed === 'object') { - for (var _k in _parsed) { - if (Object.prototype.hasOwnProperty.call(_parsed, _k) && typeof _parsed[_k] === 'string') { - _namespaceAssemblyMap[_k] = _parsed[_k]; - } - } - } - } - } catch (_e) {} - - function _getTypeInfo(assembly, typeName) { - if (!typeName) return _emptyInfo; - var cached = _typeInfoCache[typeName]; - if (cached !== undefined) return cached; - var info = _emptyInfo; - try { - // Respect an explicitly-provided assembly name. When empty, - // let the managed side attempt resolution (BCL types via Type.GetType). - var asm = (typeof assembly === 'string') ? assembly : ''; - var members = _invoke({ assembly: asm, typeName: typeName, method: '__members__', args: [] }); - if (members && typeof members === 'object') info = _withKinds(members); - } catch (e) {} - _typeInfoCache[typeName] = info; - return info; - } - - function _unwrap(v) { - if (v && typeof v === 'object' && typeof v.__handle === 'number') return { __handle: v.__handle }; - if (Array.isArray(v)) return v.map(_unwrap); - // A JS Date reaches DateTime / DateTimeOffset parameters as its ISO 8601 string. - if (v instanceof Date) return isNaN(v.getTime()) ? null : v.toISOString(); - // A JS function passed where .NET expects a delegate gets .NET objects as arguments, the same - // proxies a method result would be, rather than raw handles. - if (typeof v === 'function' && typeof v.__dotnetType__ !== 'string' && typeof v.__handle !== 'number') - return _callbackFor(v); - return v; - } - - // The wrapper sent for a JS callback: one per function, so passing it again sends the same one. - var _callbacks = typeof WeakMap === 'function' ? new WeakMap() : null; - function _callbackFor(fn) { - var wrapped = _callbacks && _callbacks.get(fn); - if (wrapped) return wrapped; - wrapped = function () { return fn.apply(this, Array.prototype.map.call(arguments, _wrap)); }; - if (_callbacks) _callbacks.set(fn, wrapped); - return wrapped; } - - // .NET collections (List, Dictionary, ObservableCollection) act like JS arrays: `length`, - // `list[i]`, for...of, and the array methods (map, filter, ...) on a copy of their items. - // _collectionKind is the bridge's CollectionKind flags (1 enumerable, 2 count, 4 indexed) by type. - var _collectionKinds = Object.create(null); - var _arrayMethods = Object.create(null); - ['forEach', 'map', 'filter', 'reduce', 'reduceRight', 'some', 'every', 'find', 'findIndex', 'findLast', - 'findLastIndex', 'indexOf', 'lastIndexOf', 'includes', 'join', 'slice', 'concat', 'flat', 'flatMap', - 'entries', 'keys', 'values', 'at', 'toJSON'].forEach(function (name) { _arrayMethods[name] = 1; }); - - function _bridgeCall(method, args) { - return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: method, args: args }); - } - - function _isIndex(prop) { - var c = prop.charCodeAt(0); - return c >= 48 && c <= 57 && /^(0|[1-9]\d*)$/.test(prop); - } - - function _collectionKind(handle, typeName) { - var kind = typeName ? _collectionKinds[typeName] : undefined; - if (kind !== undefined) return kind; - try { kind = _bridgeCall('CollectionKind', [{ __handle: handle }]) | 0; } catch (_) { kind = 0; } - if (typeName) _collectionKinds[typeName] = kind; - return kind; - } - - function _collectionItems(handle) { - return _wrap(_bridgeCall('CollectionItems', [{ __handle: handle }])) || []; - } - - // `prop` of a collection proxy when it is one of the array-like members, else undefined. - function _collectionMember(handle, typeName, prop) { - var kind = _collectionKind(handle, typeName); - if (prop === 'length') return (kind & 2) ? _bridgeCall('CollectionCount', [{ __handle: handle }]) : undefined; - if (_arrayMethods[prop] === 1) { - if (!(kind & 1)) return undefined; - return function () { - var items = _collectionItems(handle); - return prop === 'toJSON' ? items : Array.prototype[prop].apply(items, arguments); - }; - } - return (kind & 4) ? _wrap(_bridgeCall('ItemAt', [{ __handle: handle }, +prop])) : undefined; + if (typeof globalThis.__ns__clearTimeout !== 'function') { + globalThis.__ns__clearTimeout = function () { }; } - - // Value types whose JS string is their .NET ToString(), so `${DateTime.Now}` reads as a date. - var _netToString = { 'System.DateTime': 1, 'System.DateTimeOffset': 1, 'System.TimeSpan': 1 }; - - // Makes sw.Stop() and sw.Elapsed both work naturally. - // The proxy is registered with _dotNetFinalizers so the CLR reference - // is released automatically when JS GC collects the proxy. - function _makeDotNetInstance(handle, assembly, typeName, isTask, nativePtr) { - _getTypeInfo(assembly, typeName); - // Method functions by name, made on first access. - var methods = null; - var ptrKnown = nativePtr !== undefined; - // The proxy target holds the plain JS fields set on the object. - var proxy = new Proxy({}, { - get: function (target, prop) { - if (typeof prop === 'symbol') { - if (prop === Symbol.iterator && (_collectionKind(handle, typeName) & 1)) - return function () { return _collectionItems(handle)[Symbol.iterator](); }; - return target[prop]; - } - if (prop === '__handle') return handle; - if (prop === '__type') return typeName; - if (prop === '__isTask') return isTask === true; - if (prop === '__native_ptr') { - if (!ptrKnown) { ptrKnown = true; nativePtr = _nativePtrOf(handle); } - return nativePtr; - } - if (prop === 'release') return function () { - _invoke({ handle: handle, method: '__release', args: [] }); - }; - // Like a plain object, so `'' + obj` and comparisons fall through to toString. - if (prop === 'valueOf') return function () { return proxy; }; - if (prop === 'toString') return function () { - if (_netToString[typeName] === 1) return String(_invoke({ handle: handle, method: 'ToString', args: [] })); - return '[DotNetObject ' + typeName + ' #' + handle + ']'; - }; - // A Task/ValueTask (awaitable) result is thenable, so `await obj.SomethingAsync()` works; - // any other .NET object must not look like a promise. - if (prop === 'then') { - if (isTask !== true) return undefined; - return function (onFulfilled, onRejected) { - return globalThis.NSWinRT.dotnet.taskToPromise({ __handle: handle, __isTask: true }).then(onFulfilled, onRejected); - }; - } - if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop]; - // Re-read info in case it was populated after construction. - var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; - // Write-only (setter, no getter): reading it is an error. - if (kind & _K_WO) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); - if (kind & _K_PROP) - return _wrap(_invoke({ handle: handle, method: _getterName(prop), args: [] })); - if (prop === 'length' || _arrayMethods[prop] === 1 || _isIndex(prop)) { - var member = _collectionMember(handle, typeName, prop); - if (member !== undefined) return member; - } - // Not a native property: a function for method dispatch. - if (methods === null) methods = Object.create(null); - return methods[prop] || (methods[prop] = function () { - return _wrap(_invokeWith(handle, '', '', prop, arguments)); - }); - }, - set: function (target, prop, value) { - if (typeof prop !== 'symbol') { - var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; - // Read-only (getter, no setter): assigning it is an error. - if (kind & _K_RO) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); - // Writable (read-write or write-only): invoke the setter. - if (kind & (_K_PROP | _K_WO)) { - _invoke({ handle: handle, method: _setterName(prop), args: [_unwrap(value)] }); - return true; - } - if (_isIndex(prop) && (_collectionKind(handle, typeName) & 4)) { - _bridgeCall('SetItemAt', [{ __handle: handle }, +prop, _unwrap(value)]); - return true; - } - } - // Not a native property: a plain JS field of this object. - target[prop] = value; - return true; - }, - }); - if (_dotNetFinalizers) _dotNetFinalizers.register(proxy, handle); - return proxy; + if (typeof globalThis.__ns__clearInterval !== 'function') { + globalThis.__ns__clearInterval = function () { }; } + })(); - function _wrap(value) { - if (value == null) return null; - if (Array.isArray(value)) return value.map(_wrap); - if (typeof value === 'object' && typeof value.__handle === 'number') { - var typeName = value.__type || ''; - var jsSelf = _jsSelfByHandle && _deref(_jsSelfByHandle.get(value.__handle)); - if (jsSelf) return jsSelf; - // A JS subclass instance whose JS object was collected: its dispatcher revives one. - if (typeName.indexOf('NSWinRTDynamicProxies.') === 0) { - try { _invoke({ handle: value.__handle, method: '__ns_js_self', args: [] }); } catch (_) {} - jsSelf = _deref(_jsSelfByHandle.get(value.__handle)); - if (jsSelf) return jsSelf; - } - var assembly = _resolveAssembly(typeName); - return _makeDotNetInstance(value.__handle, assembly, typeName, value.__isTask === true, value.__native_ptr); - } - return value; - } + // Callbacks run once per frame from the host's pump (runtime_pump_timers, driven by + // CompositionTarget.Rendering), never by blocking the UI thread on vsync: see + // runtime/src/animation_frames.rs. Callbacks requested during a frame run in the next. + (function () { + var _nextId = 0; + var _pending = new Map(); - globalThis.NSWinRT.dotnet = { - invoke: function (assembly, typeName, method, args) { - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: (args || []).map(_unwrap) })); - }, - get: function (assembly, typeName, prop) { - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: 'get_' + prop, args: [] })); - }, - fromHandle: function (handle, typeName) { - var assembly = _resolveAssembly(typeName || ''); - return _makeDotNetInstance(handle, assembly, typeName || ''); - }, - // Exposed so other IIFEs in this bootstrap (e.g. the managed-subclass/proxy block, - // which runs in its own closure earlier in the script and has no direct access to - // this IIFE's private `_wrap`) can wrap a raw tagged handle value the same way. - wrap: _wrap, - // Lazily register a namespace prefix and optional assembly mapping. - // Example: NSWinRT.dotnet.registerNamespace('com', 'NativeScript'); - registerNamespace: function(rootName, assemblyName) { - if (!rootName || typeof rootName !== 'string') return; - var name = String(rootName); - var root = name.split('.')[0]; - if (assemblyName && typeof assemblyName === 'string') { - _assemblyCache = Object.create(null); - _namespaceAssemblyMap[name] = assemblyName; - if (!_namespaceAssemblyMap[root]) { - _namespaceAssemblyMap[root] = assemblyName; - } - } - if (root in globalThis) return; - try { - Object.defineProperty(globalThis, root, { - configurable: true, - enumerable: true, - get: function() { - var proxy = _makeNamespaceProxy(root); - try { Object.defineProperty(globalThis, root, { value: proxy, writable: true, configurable: true, enumerable: true }); } catch (_) {} - return proxy; - }, - // A namespace root never blocks app code from using the name for its own value - // (`var App = ...` in a classic script assigns through this accessor). - set: function(value) { - try { Object.defineProperty(globalThis, root, { value: value, writable: true, configurable: true, enumerable: true }); } catch (_) {} - } - }); - } catch (_) { - try { globalThis[root] = _makeNamespaceProxy(root); } catch (_) {} - } - }, - registerNamespaces: function(arr) { - if (!Array.isArray(arr)) return; - for (var i = 0; i < arr.length; i++) this.registerNamespace(arr[i]); - }, + globalThis.requestAnimationFrame = function requestAnimationFrame(callback) { + if (typeof callback !== 'function') return 0; + var id = ++_nextId; + _pending.set(id, callback); + if (_pending.size === 1) __nsRequestFrame(); + return id; }; - // ── .NET types as JS classes ──────────────────────────────────────────────── - // A .NET type proxy (System.Text.StringBuilder, MyApp.Native.Greeter) is a constructor with a - // stable `prototype`, so it can be extended the way Java and Objective-C classes are on Android - // and iOS: - // class Cat extends MyApp.Animal { constructor(n) { super(n); } Speak() { return super.Speak(); } } - // const Dog = MyApp.Animal.extend('Dog', { Speak() { return this.super.Speak(); } }); - // const calc = new MyApp.ICalculator({ Compute(a, b) { return a * b; } }); - // Constructing a subclass creates the JS object first, then a managed subclass of the .NET type - // whose overridden virtuals (and implemented interface members) call back into that object. - // `super.X()` reaches the base implementation through the non-virtual `__ns_base_X` entry point - // the bridge emits for every overridden member. Native code handing the instance back to JS gets - // the same JS object. - var _nsProxyCache = Object.create(null); - var _namespaceExtras = { System: Object.create(null) }; - var _typeKindCache = Object.create(null); - var _typeProtoCache = Object.create(null); - var _subclassInfoCache = new WeakMap(); - var _jsSelfByHandle = new Map(); - - // ── Lifetime of JS subclass instances ─────────────────────────────────────── - // An instance is a JS object plus the managed object it drives. The JS object holds the managed - // one through its handle; the managed object reaches the JS object only through its dispatcher, - // which holds it weakly. So once JS drops the JS object, its handle is released and the managed - // object goes as soon as .NET/native code drops it too (the bridge then unpins the dispatcher). - // When native code calls into an instance whose JS object was collected, a new JS object of the - // same class is made for it: fields its constructor set are gone, as on Android when a JS object - // used only from native code is collected. Subclasses of WinRT UI classes are kept strongly while - // they are in a live XAML tree. - var _canWeakRef = typeof WeakRef === 'function'; - var _selfFinalizers = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(function (handle) { - if (!_deref(_jsSelfByHandle.get(handle))) _jsSelfByHandle.delete(handle); - try { _invoke({ handle: handle, method: '__release', args: [] }); } catch (_) {} - }) - : null; - - function _weak(obj) { return _canWeakRef ? new WeakRef(obj) : obj; } - function _deref(ref) { return ref && _canWeakRef && ref instanceof WeakRef ? ref.deref() : ref; } - - // Gives `self` the bridge identity in `raw` and registers it as the JS object of `entry`'s instance. - function _adopt(entry, self, raw) { - Object.defineProperty(self, '__handle', { value: raw.__handle }); - Object.defineProperty(self, '__nsOverrides', { value: entry.overrides }); - if (!entry.winrt) { - Object.defineProperty(self, '__type', { value: raw.__type || entry.typeName }); - if (raw.__native_ptr !== undefined) Object.defineProperty(self, '__native_ptr', { value: raw.__native_ptr }); - } - entry.ref = _weak(self); - _jsSelfByHandle.set(raw.__handle, _weak(self)); - if (_selfFinalizers) _selfFinalizers.register(self, raw.__handle); - if (entry.winrt) { - entry.strong = self; - _attachedEntries.add(entry); - } - _sweepAttached(); - return self; - } - - // The JS object of `entry`'s instance, made anew when the previous one was collected. - function _selfFor(entry, raw) { - var self = _deref(entry.ref); - if (self) return self; - if (entry.constructing) throw new Error(entry.typeName + ' called into its JS subclass before the instance existed'); - if (!raw || typeof raw.__handle !== 'number') throw new Error('The ' + entry.typeName + ' subclass instance is gone'); - if (entry.winrt) { - self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, entry.typeName); - Object.setPrototypeOf(self, entry.proto); - } else { - self = Object.create(entry.proto); - } - return _adopt(entry, self, raw); - } - - // The dispatcher native code calls the instance's overridden and interface members through. - function _makeDispatcher(entry) { - return function (selfHandle, member) { - var self = _selfFor(entry, selfHandle); - if (member === '__ns_js_self') return undefined; - _sweepAttached(); - var margs = Array.prototype.slice.call(arguments, 2).map(_wrap); - return _dispatchToJs(self, entry.baseProto, entry.typeName, member, margs); - }; - } + globalThis.cancelAnimationFrame = function cancelAnimationFrame(id) { + _pending.delete(id); + }; - // Subclasses of WinRT classes held strongly (entry.strong) while in a live XAML tree, re-checked at - // most every two seconds when subclass instances are created or called into. - var _attachedEntries = new Set(); - var _lastAttachedSweep = 0; - function _sweepAttached() { - var now = Date.now(); - if (now - _lastAttachedSweep < 2000) return; - _lastAttachedSweep = now; - _attachedEntries.forEach(function (entry) { - var self = _deref(entry.ref); - if (!self) { - _attachedEntries.delete(entry); - return; + globalThis.__nsRunAnimationFrames = function (ts) { + if (_pending.size === 0) return; + var cbs = Array.from(_pending.values()); + _pending.clear(); + for (var i = 0; i < cbs.length; i++) { + try { cbs[i](ts); } catch (e) { + console.log('rAF error:', e && e.message || e); } - var attached = false; - try { attached = self.XamlRoot != null; } catch (_) {} - entry.strong = attached ? self : undefined; - }); - } - var _baseCallPrefix = '__ns_base_'; - - function _has(list, name) { - return !!list && list.indexOf(name) >= 0; - } - - function _typeKind(path) { - var kind = _typeKindCache[path]; - if (kind !== undefined) return kind; - try { - kind = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'TypeKind', args: [path] }) || ''; - } catch (_) { - kind = ''; - } - _typeKindCache[path] = kind; - return kind; - } - - // Full name of a .NET type proxy, or of the .NET base of a JS class deriving from one. - function _dotnetTypeName(type) { - if (typeof type === 'string') return type; - if (!type || (typeof type !== 'function' && typeof type !== 'object')) return ''; - var name = type.__dotnetType__; - return typeof name === 'string' ? name : ''; - } - - // The handle of an instance of a JS subclass (an own property); -1 for anything else, such as - // a class prototype further down the chain, without running any property trap. - function _ownHandle(o) { - if (!o || (typeof o !== 'object' && typeof o !== 'function')) return -1; - var d = Object.getOwnPropertyDescriptor(o, '__handle'); - return d && typeof d.value === 'number' ? d.value : -1; - } - - function _baseCallName(self, member) { - var overrides = self.__nsOverrides; - return overrides && overrides.has(member) ? _baseCallPrefix + member : member; - } - - // A native member of `self` (an instance of a JS subclass), reached through the .NET type's - // prototype: properties are read through their getter, methods become functions. Members the JS - // class overrides resolve to the base implementation — only `super.X` gets this far for them. - function _baseMember(path, self, prop) { - if (prop === 'toString') return function () { return String(_invoke({ handle: self.__handle, method: 'ToString', args: [] })); }; - if (prop === 'valueOf') return function () { return this; }; - var info = _getTypeInfo(_resolveAssembly(path), path); - var overrides = self.__nsOverrides; - if (_has(info.properties, prop) || (overrides && overrides.has('get_' + prop))) { - return _wrap(_invoke({ handle: self.__handle, method: _baseCallName(self, 'get_' + prop), args: [] })); - } - if (_has(info.writeonlyProperties, prop)) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); - if (_has(info.methods, prop) || (overrides && overrides.has(prop))) { - var method = _baseCallName(self, prop); - return function () { - var args = Array.prototype.slice.call(arguments).map(_unwrap); - return _wrap(_invoke({ handle: self.__handle, method: method, args: args })); - }; } - return undefined; - } - - // The `prototype` of a .NET type: the end of every JS subclass's prototype chain. Reads resolve - // native members against the receiving instance; writes go to native properties, and anything - // else becomes a plain JS field of the instance. - function _typePrototype(path) { - var cached = _typeProtoCache[path]; - if (cached) return cached; - var proto = new Proxy(Object.create(null), { - get: function (_, prop, receiver) { - if (typeof prop === 'symbol') return undefined; - if (_ownHandle(receiver) < 0) { - // Read off the prototype itself — TypeScript's ES5 super call - // `_super.prototype.Method.call(this, ...)`: a function that runs the base - // implementation on whichever instance it is called with. - var typeInfo = _getTypeInfo(_resolveAssembly(path), path); - if (!_has(typeInfo.methods, prop)) return undefined; - return function () { - if (_ownHandle(this) < 0) throw new TypeError(path + '.prototype.' + prop + ' called on an object that is not a ' + path); - return _baseMember(path, this, prop).apply(this, arguments); - }; - } - return _baseMember(path, receiver, prop); - }, - set: function (_, prop, value, receiver) { - if (typeof prop !== 'symbol' && _ownHandle(receiver) >= 0) { - var info = _getTypeInfo(_resolveAssembly(path), path); - if (_has(info.readonlyProperties, prop)) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); - if (_has(info.properties, prop) || _has(info.writeonlyProperties, prop)) { - _invoke({ handle: receiver.__handle, method: _baseCallName(receiver, 'set_' + prop), args: [_unwrap(value)] }); - return true; - } - } - Object.defineProperty(receiver, prop, { value: value, writable: true, enumerable: true, configurable: true }); - return true; - }, - }); - _typeProtoCache[path] = proto; - return proto; - } + }; + })(); - // Members (literal CLR names: methods, get_X/set_X accessors) and interfaces a JS class adds on - // top of its .NET base, from every class between it and the base. - function _subclassInfo(proto, baseProto) { - var cached = _subclassInfoCache.get(proto); - if (cached) return cached; - var members = []; - var overrides = new Set(); - var interfaces = []; - var seen = Object.create(null); - function addInterfaces(list) { - if (!Array.isArray(list)) return; - for (var i = 0; i < list.length; i++) { - var name = _dotnetTypeName(list[i]) || (list[i] && (list[i].__typeName__ || list[i].name)) || ''; - if (name && interfaces.indexOf(name) < 0) interfaces.push(name); - } - } - for (var p = proto; p && p !== baseProto; p = Object.getPrototypeOf(p)) { - var keys = Object.getOwnPropertyNames(p); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - if (key === 'constructor' || key === 'interfaces' || key === 'super' || key === 'init' || seen[key]) continue; - seen[key] = true; - var d = Object.getOwnPropertyDescriptor(p, key); - if (typeof d.get === 'function') { members.push('get_' + key); overrides.add('get_' + key); } - if (typeof d.set === 'function') { members.push('set_' + key); overrides.add('set_' + key); } - if (typeof d.value === 'function') { members.push(key); overrides.add(key); } + // The native classes are installed by install_url_globals() before this + // script runs. This block adds the JS-layer searchParams accessor + // (mutation propagation + caching), Symbol.iterator, and the blob-URL API. + (function () { + // searchParams: lazily created, mutations propagate back to URL.search + Object.defineProperty(URL.prototype, 'searchParams', { + get: function () { + if (this._searchParams == null) { + var self = this; + var sp = new URLSearchParams(this.search); + Object.defineProperty(sp, '_url', { enumerable: false, writable: false, value: self }); + var _append = sp.append.bind(sp); + sp.append = function (name, value) { _append(name, value); self.search = sp.toString(); }; + var _delete = sp.delete.bind(sp); + sp.delete = function (name, value) { _delete(name, value); self.search = sp.toString(); }; + var _set = sp.set.bind(sp); + sp.set = function (name, value) { _set(name, value); self.search = sp.toString(); }; + var _sort = sp.sort.bind(sp); + sp.sort = function () { _sort(); self.search = sp.toString(); }; + this._searchParams = sp; } - if (Object.prototype.hasOwnProperty.call(p, 'interfaces')) addInterfaces(p.interfaces); - var ctor = Object.prototype.hasOwnProperty.call(p, 'constructor') ? p.constructor : null; - if (ctor && Object.prototype.hasOwnProperty.call(ctor, 'interfaces')) addInterfaces(ctor.interfaces); - } - var info = { members: members, overrides: overrides, interfaces: interfaces }; - _subclassInfoCache.set(proto, info); - return info; - } - - // The JS-defined member `key` of `self` (an own field, or a member of one of its JS classes). - function _jsMember(self, key, baseProto) { - for (var o = self; o && o !== baseProto; o = Object.getPrototypeOf(o)) { - var d = Object.getOwnPropertyDescriptor(o, key); - if (d) return d; - } - return null; - } + return this._searchParams; + }, + configurable: true, + }); - // Native code calling an overridden member (or an implemented interface member) of `self`. - function _dispatchToJs(self, baseProto, typeName, member, args) { - var accessor = /^(get|set)[_:](.+)$/.exec(member); - if (accessor) { - var ad = _jsMember(self, accessor[2], baseProto); - if (ad) { - if (accessor[1] === 'get') { - if (typeof ad.get === 'function') return ad.get.call(self); - if ('value' in ad && typeof ad.value !== 'function') return ad.value; - } else if (typeof ad.set === 'function') { - ad.set.call(self, args[0]); - return undefined; - } else if ('value' in ad && ad.writable) { - self[accessor[2]] = args[0]; - return undefined; - } - } - } - var md = _jsMember(self, member, baseProto); - if (md && typeof md.value === 'function') return md.value.apply(self, args); - throw new Error((self.constructor && self.constructor.name || typeName) + ' does not implement ' + member); - } + // Make URLSearchParams iterable (entries by default) + URLSearchParams.prototype[Symbol.iterator] = URLSearchParams.prototype.entries; - function _constructSubclass(path, args, proto) { - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') - throw new Error('Extending .NET types is not available in this runtime'); - var baseProto = _typePrototype(path); - var info = _subclassInfo(proto, baseProto); - var self = Object.create(proto); - // A virtual the base constructor calls reaches `self` before it has its handle. - var entry = { proto: proto, baseProto: baseProto, typeName: path, overrides: info.overrides, winrt: false, ref: self }; - var raw = globalThis.__nsDotNetCreateJsSubclass('', path, info.interfaces, info.members, _makeDispatcher(entry), args.map(_unwrap)); - if (!raw || typeof raw.__handle !== 'number') throw new Error('Could not create an instance of a subclass of ' + path); - return _adopt(entry, self, raw); - } + var BLOB_STORE = new Map(); + var _blobSeq = 0; + URL.createObjectURL = function (object, options) { + if (!(object instanceof globalThis.Blob)) return null; + var id = (++_blobSeq).toString(16).padStart(8, '0') + '-' + + Math.floor(Math.random() * 0x10000).toString(16).padStart(4, '0') + '-4' + + Math.floor(Math.random() * 0x1000).toString(16).padStart(3, '0') + '-' + + (Math.floor(Math.random() * 4) + 8).toString(16) + + Math.floor(Math.random() * 0x1000).toString(16).padStart(3, '0') + '-' + + Math.floor(Math.random() * 0x1000000000000).toString(16).padStart(12, '0'); + var url = 'blob:nativescript/' + id; + BLOB_STORE.set(url, { blob: object, type: object.type, ext: options && options.ext }); + return url; + }; + URL.revokeObjectURL = function (url) { BLOB_STORE.delete(url); }; + URL.InternalAccessor = { + getData: function (url) { return BLOB_STORE.get(url) || null; }, + }; + })(); - // `new SomeInterface({ ... })`: an object implementing the interface with the given members. - function _implementInterface(path, impl) { - var proto = Object.create(_typePrototype('System.Object')); - Object.defineProperties(proto, Object.getOwnPropertyDescriptors(impl)); - Object.defineProperty(proto, 'interfaces', { value: [path] }); - return _constructSubclass('System.Object', [], proto); - } + // ada-url (C++) doesn't expose URLPattern; this JS polyfill covers the + // common cases: :name capture groups, * / ** wildcards, literal segments. + (function () { + if (typeof URLPattern !== 'undefined') return; - // `this.super` inside a class made by extend(): the parent class's members bound to `self`. - function _superOf(self, Cls) { - var parent = Object.getPrototypeOf(Cls.prototype); - return new Proxy(Object.create(null), { - get: function (_, prop) { - var value = Reflect.get(parent, prop, self); - return typeof value === 'function' ? value.bind(self) : value; - }, - }); + function _esc(s) { + return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); } - // Base.extend([name,] members): an ES2015 subclass of Base with the given members; `init`, when - // present, runs after construction with the constructor arguments. - function _extendClass(Base, nameOrMembers, maybeMembers) { - var hasName = typeof nameOrMembers === 'string'; - var members = (hasName ? maybeMembers : nameOrMembers) || {}; - var Extended = class extends Base { - constructor() { - super(...arguments); - if (typeof members.init === 'function') members.init.apply(this, arguments); + function _compile(pat) { + if (pat === undefined || pat === null) pat = '*'; + var s = String(pat); + var groups = []; + var re = ''; + var i = 0; + while (i < s.length) { + var c = s[i]; + if (c === ':') { + var j = i + 1; + while (j < s.length && /\w/.test(s[j])) j++; + var name = s.slice(i + 1, j); + groups.push(name); + re += '([^/?#]+?)'; + i = j; + } else if (c === '*' && s[i + 1] === '*') { + groups.push('0'); + re += '(.*)'; + i += 2; + } else if (c === '*') { + groups.push('0'); + re += '([^/?#]*)'; + i++; + } else { + re += _esc(c); + i++; } - }; - var descriptors = Object.getOwnPropertyDescriptors(members); - delete descriptors.init; - if (descriptors.interfaces) { - Object.defineProperty(Extended.prototype, 'interfaces', { value: members.interfaces }); - delete descriptors.interfaces; - } - Object.defineProperties(Extended.prototype, descriptors); - Object.defineProperty(Extended.prototype, 'super', { - get: function () { return _superOf(this, Extended); }, - }); - if (hasName) Object.defineProperty(Extended, 'name', { value: nameOrMembers.split('.').pop() }); - return Extended; - } - - // `_super.call(this, ...)` / `_super.apply(this, args)` in TypeScript's ES5 output (what the - // @NativeClass transform emits): construct the subclass instance `thisArg` was created for. - function _es5SuperCall(path, thisArg, args) { - var proto = thisArg && Object.getPrototypeOf(thisArg); - if (!proto || !Object.prototype.isPrototypeOf.call(_typePrototype(path), proto)) - throw new TypeError('Class constructor ' + path + ' cannot be invoked without \'new\''); - return _constructSubclass(path, args, proto); - } - - function _hasInstance(path, value) { - if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false; - if (Object.prototype.isPrototypeOf.call(_typePrototype(path), value)) return true; - if (typeof value.__handle !== 'number') return false; - try { - return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'IsInstanceOf', args: [{ __handle: value.__handle }, path] }) === true; - } catch (_) { - return false; } + return { re: new RegExp('^' + re + '$'), groups: groups }; } - // ── WinRT classes as JS base classes ──────────────────────────────────────── - // `class FixedPanel extends Microsoft.UI.Xaml.Controls.Panel { MeasureOverride(size) { ... } }`: - // the WinRT constructor hands `super(...)` here. The instance is a composed object created by - // the .NET bridge from the C#/WinRT projection of the class (the way a C# subclass is), so XAML - // calling an overridable member (MeasureOverride, ArrangeOverride, OnApplyTemplate, ...) lands in - // the JS override. The JS object is the ordinary WinRT wrapper of that object, with the subclass - // prototype spliced in front of the class's native members. - var _linkedWinRTPrototypes = new WeakSet(); - - // The WinRT class a JS class derives from: the nearest constructor with its own __typeName__. - function _winrtBaseOf(ctor) { - for (var c = ctor; c; c = Object.getPrototypeOf(c)) { - if (Object.prototype.hasOwnProperty.call(c, '__typeName__')) return c; + function _match(compiled, value) { + var m = String(value || '').match(compiled.re); + if (!m) return null; + var groups = {}; + for (var i = 0; i < compiled.groups.length; i++) { + groups[compiled.groups[i]] = m[i + 1] !== undefined ? m[i + 1] : undefined; } - return null; - } - - // `super.X(...)` for a member a JS subclass of a WinRT class overrides: the base implementation, - // through the bridge (the native member would dispatch back into the override). - function _winrtBaseCall(handle, prop) { - return function () { - var args = Array.prototype.slice.call(arguments).map(_unwrap); - return _wrap(_invoke({ handle: handle, method: _baseCallPrefix + prop, args: args })); - }; + return { input: String(value || ''), groups: groups }; } - // Puts a layer under the JS subclass prototypes that resolves the class's native members and - // sends `super.X()` for overridden members to the base implementation. On the classic engine the - // class's `prototype` is separate from its instances' native prototype, so the layer goes under the - // class's `prototype` once; on the napi engines they're the same object, so it goes under the - // subclass prototype that sits directly on it. - function _linkWinRTSubclassPrototype(base, nativeProto, proto) { - if (!base || !nativeProto) return; - var target = base.prototype; - if (target === nativeProto) { - target = proto; - while (target && Object.getPrototypeOf(target) !== nativeProto) target = Object.getPrototypeOf(target); - } - if (!target || _linkedWinRTPrototypes.has(target)) return; - _linkedWinRTPrototypes.add(target); - var layer = new Proxy(Object.create(nativeProto), { - get: function (_, prop, receiver) { - var handle = _ownHandle(receiver); - if (typeof prop !== 'string') return Reflect.get(nativeProto, prop, receiver); - if (handle >= 0) { - if (receiver.__nsOverrides && receiver.__nsOverrides.has(prop)) return _winrtBaseCall(handle, prop); - return Reflect.get(nativeProto, prop, receiver); - } - // Read off a prototype — TypeScript's ES5 `_super.prototype.X.call(this, ...)`: decide - // between the base implementation and the native member when it's called. - var native = Reflect.get(nativeProto, prop, receiver); - if (typeof native !== 'function') return native; - return function () { - var h = _ownHandle(this); - if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(prop)) - return _winrtBaseCall(h, prop).apply(this, arguments); - return native.apply(this, arguments); + function URLPattern(init, baseURL) { + if (typeof init === 'string') { + try { + var u = new URL(init, baseURL || 'http://_placeholder_'); + init = { + protocol: u.protocol.slice(0, -1), + hostname: u.hostname, + port: u.port, + pathname: u.pathname, + search: u.search ? u.search.slice(1) : '', + hash: u.hash ? u.hash.slice(1) : '', }; - }, - has: function (_, prop) { return prop in nativeProto; }, - }); - Object.setPrototypeOf(target, layer); - } - - // napi engines: the class `prototype` is the native prototype itself, so TypeScript's ES5 - // `_super.prototype.X.call(this, ...)` reaches the native member directly. Wrap each native member a - // subclass overrides (once) so that, called on a JS subclass instance, it runs the base - // implementation instead of dispatching back into the override. - function _guardNativeMembers(nativeProto, overrides) { - overrides.forEach(function (name) { - for (var p = nativeProto; p; p = Object.getPrototypeOf(p)) { - var d = Object.getOwnPropertyDescriptor(p, name); - if (!d) continue; - if (typeof d.value === 'function' && !d.value.__nsGuarded && d.writable) { - var native = d.value; - var guarded = function () { - var h = _ownHandle(this); - if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(name)) - return _winrtBaseCall(h, name).apply(this, arguments); - return native.apply(this, arguments); - }; - Object.defineProperty(guarded, '__nsGuarded', { value: true }); - Object.defineProperty(p, name, { value: guarded, writable: true, configurable: true, enumerable: d.enumerable }); - } - return; + } catch (e) { + init = { pathname: init }; } - }); + } + init = init || {}; + var opt = function (v, d) { return v !== undefined && v !== null ? String(v) : (d !== undefined ? d : '*'); }; + this.protocol = opt(init.protocol); + this.username = opt(init.username); + this.password = opt(init.password); + this.hostname = opt(init.hostname); + this.port = opt(init.port); + this.pathname = opt(init.pathname, '/*'); + this.search = opt(init.search); + this.hash = opt(init.hash); + this._pc = _compile(this.protocol); + this._uc = _compile(this.username); + this._pw = _compile(this.password); + this._hc = _compile(this.hostname); + this._oc = _compile(this.port); + this._ac = _compile(this.pathname); + this._sc = _compile(this.search); + this._xc = _compile(this.hash); } - // Called by the WinRT class constructors for `super(...)` from a JS subclass (newTarget) or - // TypeScript's ES5 `_super.call(this, ...)` (es5This). Returns undefined when this isn't a subclass - // construction or the .NET bridge isn't available; the constructor then builds a plain instance. - globalThis.__nsConstructWinRTSubclass = function (typeName, newTarget, args, es5This) { - var proto; - var base; - if (newTarget) { - proto = newTarget.prototype; - base = _winrtBaseOf(Object.getPrototypeOf(newTarget)); - } else { - proto = es5This && Object.getPrototypeOf(es5This); - base = proto && proto.constructor ? _winrtBaseOf(Object.getPrototypeOf(proto.constructor)) : null; - if (!base || base.__typeName__ !== typeName || Object.prototype.hasOwnProperty.call(proto.constructor, '__typeName__')) - return undefined; - } - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function' || typeof globalThis.__nsWrapWinRTPointer !== 'function') - return undefined; - var baseProto = base ? base.prototype : null; - var info = _subclassInfo(proto, baseProto); - var entry = { proto: proto, baseProto: baseProto, typeName: typeName, overrides: info.overrides, winrt: true, constructing: true }; - var raw = globalThis.__nsDotNetCreateJsSubclass('', typeName, info.interfaces, info.members, _makeDispatcher(entry), (args || []).map(_unwrap)); - entry.constructing = false; - if (!raw || raw.__native_ptr === undefined || raw.__native_ptr === null) - throw new Error('Could not create a WinRT object for a subclass of ' + typeName); - var self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, typeName); - var nativeProto = Object.getPrototypeOf(self); - _linkWinRTSubclassPrototype(base, nativeProto, proto); - if (base && base.prototype === nativeProto) _guardNativeMembers(nativeProto, info.overrides); - Object.setPrototypeOf(self, proto); - return _adopt(entry, self, raw); + URLPattern.prototype.test = function (input, baseURL) { + return this.exec(input, baseURL) !== null; }; - globalThis.NSWinRT.extendClass = _extendClass; - - // System.Diagnostics.Stopwatch.StartNew() → static method call - // System.Environment.MachineName → static property get - // new System.Text.StringBuilder(64) → constructor - // sw.Stop() → instance method - // sw.Elapsed → instance property - function _makeNamespaceProxy(path) { - var cached = _nsProxyCache[path]; - if (cached) return cached; - // Static method functions and child namespace/type proxies by name, made on first access. - var members = Object.create(null); - // Plain JS fields assigned to the namespace or type, made on first assignment. - var fields = null; - function _node() {} - var proxy = new Proxy(_node, { - get: function (_, prop) { - if (prop === Symbol.hasInstance) return function (value) { return _hasInstance(path, value); }; - if (typeof prop === 'symbol') return undefined; - // Prevent string-coercion methods from descending into sub-proxies. - // V8's console.log / JSON.stringify call toString/valueOf on unknown objects; - // returning a sub-proxy causes the apply trap to fire with a non-existent - // type name, producing a spurious "Type not found" bridge error. - if (prop === 'toString' || prop === 'valueOf') - return function() { return '[.NET ' + path + ']'; }; - var extras = _namespaceExtras[path]; - if (extras && Object.prototype.hasOwnProperty.call(extras, prop)) return extras[prop]; - if (fields !== null && prop in fields) return fields[prop]; - var member = members[prop]; - if (member !== undefined) return member; - if (prop === '__dotnetType__') return path; - if (prop === 'prototype') return _typePrototype(path); - // Not a WinRT class: TypeScript's __extends must take the standard path. - if (prop === '__nsWinRTClass__' || prop === 'then') return undefined; - var assembly = _resolveAssembly(path); - var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; - // Write-only static property: reading it is an error. - if (kind & _K_SWO) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); - // Readable static property: resolve value immediately. - if (kind & _K_SPROP) - return _wrap(_invoke({ assembly: assembly, typeName: path, method: _getterName(prop), args: [] })); - // Static method: a function, made once. - if (kind & _K_SMETHOD) { - return members[prop] = function () { - return _wrap(_invokeWith(-1, path, _resolveAssembly(path), prop, arguments)); - }; - } - if (prop === 'extend') return function (nameOrMembers, maybeMembers) { return _extendClass(this, nameOrMembers, maybeMembers); }; - if (prop === 'call') return function (thisArg) { return _es5SuperCall(path, thisArg, Array.prototype.slice.call(arguments, 1)); }; - if (prop === 'apply') return function (thisArg, args) { return _es5SuperCall(path, thisArg, args ? Array.prototype.slice.call(args) : []); }; - // Namespace / sub-type: keep descending. - return members[prop] = _makeNamespaceProxy(path + '.' + prop); - }, - set: function (_, prop, value) { - if (typeof prop === 'symbol') return true; - var assembly = _resolveAssembly(path); - var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; - // Read-only static property: assignment is an error. - if (kind & _K_SRO) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); - // Writable (read-write or write-only): invoke the setter. - if (kind & (_K_SPROP | _K_SWO)) { - _invoke({ assembly: assembly, typeName: path, method: _setterName(prop), args: [_unwrap(value)] }); - return true; - } - // Not a native property: a plain JS field. - (fields || (fields = Object.create(null)))[prop] = value; - return true; - }, - apply: function (_, _this, args) { - var lastDot = path.lastIndexOf('.'); - // No dot means this is a top-level name being called directly — - // there is no type+method pair to dispatch, so bail out. - if (lastDot <= 0) return undefined; - var typeName = path.substring(0, lastDot); - var method = path.substring(lastDot + 1); - var assembly = _resolveAssembly(typeName); - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: args.map(_unwrap) })); - }, - construct: function (_, args, newTarget) { - // `super(...)` from a JS subclass. - if (newTarget && newTarget !== proxy) return _constructSubclass(path, args, newTarget.prototype); - if (args.length === 1 && args[0] && typeof args[0] === 'object' && typeof args[0].__handle !== 'number' - && _typeKind(path) === 'interface') - return _implementInterface(path, args[0]); - var assembly = _resolveAssembly(path); - return _wrap(_invoke({ assembly: assembly, typeName: path, method: '.ctor', args: args.map(_unwrap) })); - }, - }); - _nsProxyCache[path] = proxy; - return proxy; - } - // `System` stays the .NET namespace when app code assigns it: @nativescript/core sets - // `global.System = { import() }` (a SystemJS shim). Assigned members are kept and take - // precedence (`System.import(...)` keeps working); everything else is .NET. - (function () { - var systemProxy = _makeNamespaceProxy('System'); - Object.defineProperty(globalThis, 'System', { - configurable: true, - enumerable: true, - get: function () { return systemProxy; }, - set: function (value) { - if (value && (typeof value === 'object' || typeof value === 'function')) - Object.keys(value).forEach(function (k) { _namespaceExtras.System[k] = value[k]; }); - }, - }); - })(); - globalThis.Microsoft = _makeNamespaceProxy('Microsoft'); - globalThis.NativeScript = _makeNamespaceProxy('NativeScript'); - - // Every other root namespace of the app's assemblies (C# sources compiled into the app, - // plugin and NuGet assemblies) is a lazy global too, so `new MyCompany.Native.Foo()` works - // with no registration step, the way Java packages and Objective-C classes are globals on - // Android and iOS. WinRT owns Windows/Microsoft/NativeScript; the rest are implementation - // namespaces of the toolchain that no app reaches for. - (function () { - var reserved = { Windows: 1, Microsoft: 1, NativeScript: 1, System: 1, NativeScriptBridge: 1, NSWinRTDynamicProxies: 1, - WinRT: 1, ABI: 1, XamlGeneratedNamespace: 1, FxResources: 1, Internal: 1, Interop: 1, MS: 1, Mono: 1 }; - for (var ns in _namespaceAssemblyMap) { - if (ns.indexOf('.') !== -1 || reserved[ns] === 1 || !/^[A-Za-z_$][\w$]*$/.test(ns)) continue; - globalThis.NSWinRT.dotnet.registerNamespace(ns); - } - })(); - - // Creates a typed .NET BCL delegate (not a WinRT COM delegate). - // Use this when the API expects a managed System.Action, - // System.EventHandler, etc. rather than a WinRT delegate interface. - // For WinRT delegate types use NSWinRT.asDelegate instead. - if (typeof globalThis.__nsDotNetAwaitTask === 'function') { - globalThis.NSWinRT.dotnet.taskToPromise = function (obj) { - var h = obj && typeof obj.__handle === 'number' && obj.__isTask === true ? obj.__handle : -1; - if (h < 0) return Promise.resolve(obj); - return new Promise(function (resolve, reject) { - globalThis.__nsDotNetAwaitTask(h, function (value) { resolve(_wrap(value)); }, reject); - }); - }; - } - - if (typeof globalThis.__nsDotNetCreateDelegate === 'function') { - globalThis.NSWinRT.dotnet.asDelegate = function(typeNameOrFn, fn) { - var typeName, callback; - if (typeof typeNameOrFn === 'function') { - typeName = ''; - callback = typeNameOrFn; + URLPattern.prototype.exec = function (input, baseURL) { + var url; + try { + if (typeof input === 'string') { + url = new URL(input, baseURL || undefined); + } else if (input && input.href) { + url = new URL(input.href); } else { - typeName = typeNameOrFn || ''; - callback = fn; + url = input || {}; } - if (typeof callback !== 'function') - throw new TypeError('NSWinRT.dotnet.asDelegate: callback must be a function'); - var wrapped = function() { - var args = Array.prototype.slice.call(arguments).map(_wrap); - return callback.apply(null, args); - }; - return globalThis.__nsDotNetCreateDelegate(typeName, wrapped); - }; - } + } catch (e) { return null; } + + var rm = _match(this._pc, url.protocol ? url.protocol.slice(0, -1) : ''); + var um = _match(this._uc, url.username || ''); + var pm = _match(this._pw, url.password || ''); + var hm = _match(this._hc, url.hostname || ''); + var om = _match(this._oc, url.port || ''); + var am = _match(this._ac, url.pathname || '/'); + var sm = _match(this._sc, url.search ? url.search.slice(1) : ''); + var xm = _match(this._xc, url.hash ? url.hash.slice(1) : ''); - if (typeof globalThis.__nsRunOnUIThread === 'function') { - globalThis.NSWinRT.runOnUIThread = function(fn) { - if (typeof fn !== 'function') throw new TypeError('NSWinRT.runOnUIThread: expected a function'); - return globalThis.__nsRunOnUIThread(fn); + if (!rm || !um || !pm || !hm || !om || !am || !sm || !xm) return null; + return { + inputs: [input], + protocol: rm, username: um, password: pm, + hostname: hm, port: om, pathname: am, + search: sm, hash: xm, }; - } + }; + + globalThis.URLPattern = URLPattern; })(); +"#, include_str!("js/dotnet.js"), r#" + (function () { if (typeof globalThis.__nsWin32Call !== 'function') return; @@ -4594,7 +2742,7 @@ const HELPER_SOURCE: &str = r#" console.warn('WeakRef.clear() is non-standard and has been deprecated. It does nothing and the call can be safely removed.'); }; })(); - "#; + "#); /// Calls the .NET bridge with a JSON request string and returns the JSON /// response string. Throws a JS error if the host is not initialised or the @@ -4693,6 +2841,12 @@ pub(crate) fn handle_dotnet_invoke_binary( } } + // A release gives back the exports its proxy took; without a count the handle goes outright. + if op == 0x04 && args.length() > 4 { + let count = args.get(4).int32_value(scope).unwrap_or(-1); + req.extend_from_slice(&count.to_le_bytes()); + } + if op == 0x01 || op == 0x02 { bin_write_str16(&mut req, method.as_bytes()); } @@ -4708,7 +2862,10 @@ pub(crate) fn handle_dotnet_invoke_binary( release_collected_js_callbacks(); match crate::dotnet::call_dotnet_binary_with(&req, |response| bin_read_response(scope, response)) { Ok(Ok(v8_val)) => retval.set(v8_val), - Ok(Err(e)) | Err(e) => throw_js_error(scope, &e), + Ok(Err(exception)) => { + scope.throw_exception(exception); + } + Err(e) => throw_js_error(scope, &e), } } @@ -4790,16 +2947,23 @@ pub(crate) unsafe extern "C" fn invoke_dotnet_js_callback( // with the Windows LocalAlloc/LMEM_FIXED allocator for compatibility. let mut out_buf: Vec = Vec::new(); if tc.has_caught() { - let msg = tc - .exception() + let exception = tc.exception(); + let msg = exception .map(|ex| ex.to_rust_string_lossy(tc)) .unwrap_or_else(|| "JavaScript exception".to_string()); + let stack = exception.and_then(|ex| ex.to_object(tc)).and_then(|obj| { + let key = v8::String::new(tc, "stack")?; + let value = obj.get(tc, key.into())?; + value.is_string().then(|| value.to_rust_string_lossy(tc)) + }); crate::store_last_js_error(msg.clone()); tc.reset(); - // 0x0F: the callback threw. A managed caller that expects a result gets a JsException; - // a void delegate/event ignores it (the error is reported like any uncaught JS error). + // 0x0F: the callback threw. Message, then JS stack (a JsException in .NET). out_buf.push(0x0F); bin_write_str32(&mut out_buf, msg.as_bytes()); + if let Some(stack) = stack { + bin_write_str32(&mut out_buf, stack.as_bytes()); + } } else { match result_val { Some(rv) => { @@ -4970,7 +3134,9 @@ pub(crate) fn handle_dotnet_create_delegate( match crate::dotnet::call_dotnet_binary(&req) { Ok(response) => match bin_read_response(scope, &response) { Ok(v) => retval.set(v), - Err(e) => throw_js_error(scope, &e), + Err(exception) => { + scope.throw_exception(exception); + } }, Err(e) => throw_js_error(scope, &e), } @@ -5077,7 +3243,9 @@ pub(crate) fn handle_dotnet_create_js_subclass( match crate::dotnet::call_dotnet_binary(&req) { Ok(response) => match bin_read_response(scope, &response) { Ok(v) => retval.set(v), - Err(e) => throw_js_error(scope, &e), + Err(exception) => { + scope.throw_exception(exception); + } }, Err(e) => throw_js_error(scope, &e), } @@ -5189,6 +3357,10 @@ fn bin_write_v8_arg( return; } + if bin_write_v8_bigint(buf, arg) || bin_write_v8_bytes(buf, arg) { + return; + } + // A JS array (for an array, collection or params parameter): tag 0x07, a u32 count, then // each item encoded like an argument. if let Ok(array) = v8::Local::::try_from(arg) { @@ -5293,6 +3465,142 @@ fn bin_write_v8_arg( } +/// A BigInt as a .NET long (tag 0x10), or ulong (0x11) when it only fits that. +fn bin_write_v8_bigint(buf: &mut Vec, arg: v8::Local) -> bool { + let Ok(big) = v8::Local::::try_from(arg) else { + return false; + }; + let (signed, lossless) = big.i64_value(); + if lossless { + buf.push(0x10); + buf.extend_from_slice(&signed.to_le_bytes()); + } else { + buf.push(0x11); + buf.extend_from_slice(&big.u64_value().0.to_le_bytes()); + } + true +} + +/// A typed array, ArrayBuffer or DataView as tag 0x12: element kind, byte length, bytes. +fn bin_write_v8_bytes(buf: &mut Vec, arg: v8::Local) -> bool { + if !arg.is_array_buffer_view() && !arg.is_array_buffer() { + return false; + } + let kind: u8 = if arg.is_int8_array() { + 1 + } else if arg.is_int16_array() { + 2 + } else if arg.is_uint16_array() { + 3 + } else if arg.is_int32_array() { + 4 + } else if arg.is_uint32_array() { + 5 + } else if arg.is_float32_array() { + 6 + } else if arg.is_float64_array() { + 7 + } else if arg.is_big_int64_array() { + 8 + } else if arg.is_big_uint64_array() { + 9 + } else { + 0 + }; + if let Ok(view) = v8::Local::::try_from(arg) { + let len = view.byte_length(); + buf.push(0x12); + buf.push(kind); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + let start = buf.len(); + buf.resize(start + len, 0); + view.copy_contents(&mut buf[start..]); + return true; + } + let Ok(array_buffer) = v8::Local::::try_from(arg) else { + return false; + }; + let len = array_buffer.byte_length(); + buf.push(0x12); + buf.push(0); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + match array_buffer.data() { + Some(data) => buf.extend_from_slice(unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, len) }), + None => buf.resize(buf.len() + len, 0), + } + true +} + +/// Bytes from the bridge (tag 0x12) as a JS typed array of `kind` over a new ArrayBuffer. +fn v8_typed_array<'s>(scope: &mut v8::PinScope<'s, '_>, kind: u8, bytes: &[u8]) -> v8::Local<'s, v8::Value> { + let len = bytes.len(); + let array_buffer = v8::ArrayBuffer::new(scope, len); + if let Some(data) = array_buffer.data() { + unsafe { std::ptr::copy_nonoverlapping(bytes.as_ptr(), data.as_ptr() as *mut u8, len) }; + } + let typed: Option> = match kind { + 1 => v8::Int8Array::new(scope, array_buffer, 0, len).map(Into::into), + 2 => v8::Int16Array::new(scope, array_buffer, 0, len / 2).map(Into::into), + 3 => v8::Uint16Array::new(scope, array_buffer, 0, len / 2).map(Into::into), + 4 => v8::Int32Array::new(scope, array_buffer, 0, len / 4).map(Into::into), + 5 => v8::Uint32Array::new(scope, array_buffer, 0, len / 4).map(Into::into), + 6 => v8::Float32Array::new(scope, array_buffer, 0, len / 4).map(Into::into), + 7 => v8::Float64Array::new(scope, array_buffer, 0, len / 8).map(Into::into), + 8 => v8::BigInt64Array::new(scope, array_buffer, 0, len / 8).map(Into::into), + 9 => v8::BigUint64Array::new(scope, array_buffer, 0, len / 8).map(Into::into), + _ => v8::Uint8Array::new(scope, array_buffer, 0, len).map(Into::into), + }; + typed.unwrap_or_else(|| array_buffer.into()) +} + +/// The Error for a bridge error (0xFF), with `dotnetType`, `dotnetStack` and `nativeException` +/// (a raw handle the JS layer wraps) when the bridge sends them. +fn bin_error_value<'s>(scope: &mut v8::PinScope<'s, '_>, bytes: &[u8]) -> v8::Local<'s, v8::Value> { + let mut pos = 1usize; + let message = bin_take_str(bytes, &mut pos, 4).unwrap_or("the .NET call failed"); + let text = v8::String::new(scope, message).unwrap_or_else(|| v8::String::empty(scope)); + let error = v8::Exception::error(scope, text); + let Some(obj) = error.to_object(scope) else { + return error; + }; + let type_name = bin_take_str(bytes, &mut pos, 2); + let stack = bin_take_str(bytes, &mut pos, 4); + let handle = bytes.get(pos..pos + 4).map(|b| i32::from_le_bytes(b.try_into().unwrap())); + let set = |scope: &mut v8::PinScope<'s, '_>, key: &str, value: v8::Local<'s, v8::Value>| { + if let Some(k) = v8::String::new(scope, key) { + obj.set(scope, k.into(), value); + } + }; + if let (Some(type_name), Some(stack)) = (type_name, stack) { + let t = v8::String::new(scope, type_name).unwrap_or_else(|| v8::String::empty(scope)); + set(scope, "dotnetType", t.into()); + let s = v8::String::new(scope, stack).unwrap_or_else(|| v8::String::empty(scope)); + set(scope, "dotnetStack", s.into()); + if let Some(id) = handle.filter(|id| *id >= 0) { + let native = v8::Object::new(scope); + if let (Some(hk), Some(tk)) = (v8::String::new(scope, "__handle"), v8::String::new(scope, "__type")) { + native.set(scope, hk.into(), v8::Integer::new(scope, id).into()); + native.set(scope, tk.into(), t.into()); + } + set(scope, "nativeException", native.into()); + } + } + error +} + +/// A length-prefixed (u16 or u32, per `prefix`) UTF-8 string at `pos`, advancing past it. +fn bin_take_str<'b>(bytes: &'b [u8], pos: &mut usize, prefix: usize) -> Option<&'b str> { + let len_bytes = bytes.get(*pos..*pos + prefix)?; + let len = if prefix == 2 { + u16::from_le_bytes(len_bytes.try_into().ok()?) as usize + } else { + u32::from_le_bytes(len_bytes.try_into().ok()?) as usize + }; + let text = std::str::from_utf8(bytes.get(*pos + prefix..*pos + prefix + len)?).ok()?; + *pos += prefix + len; + Some(text) +} + /// A plain JS object (`{ Width: 120, Height: 40 }`) as JSON, for a .NET struct/record parameter or /// return value (tag 0x0D): the bridge deserializes it into the expected type. Class instances, /// arrays and functions are not plain objects. @@ -5346,6 +3654,10 @@ fn bin_write_v8_value( } } + if bin_write_v8_bigint(buf, arg) || bin_write_v8_bytes(buf, arg) { + return; + } + if arg.is_array() { if let Some(arr) = v8::Local::::try_from(arg).ok() { let len = arr.length() as u32; @@ -5412,15 +3724,22 @@ fn bin_write_v8_value( buf.push(0x00); } +/// A bridge response as a JS value, or the exception to throw for it. fn bin_read_response<'s>( scope: &mut v8::PinScope<'s, '_>, bytes: &[u8], -) -> Result, String> { +) -> Result, v8::Local<'s, v8::Value>> { if bytes.is_empty() { return Ok(v8::null(scope).into()); } + if bytes[0] == 0xFF { + return Err(bin_error_value(scope, bytes)); + } let mut pos = 0usize; - bin_read_value(scope, bytes, &mut pos) + bin_read_value(scope, bytes, &mut pos).map_err(|message| { + let text = v8::String::new(scope, &message).unwrap_or_else(|| v8::String::empty(scope)); + v8::Exception::error(scope, text) + }) } fn bin_read_value<'s>( @@ -5461,6 +3780,28 @@ fn bin_read_value<'s>( .unwrap_or_else(|| v8::null(scope).into())) } + 0x10 => { + let v = i64::from_le_bytes(bytes.get(*pos..*pos + 8).ok_or("i64 read")?.try_into().map_err(|_| "i64 read")?); + *pos += 8; + Ok(v8::BigInt::new_from_i64(scope, v).into()) + } + + 0x11 => { + let v = u64::from_le_bytes(bytes.get(*pos..*pos + 8).ok_or("u64 read")?.try_into().map_err(|_| "u64 read")?); + *pos += 8; + Ok(v8::BigInt::new_from_u64(scope, v).into()) + } + + 0x12 => { + // binary data: element kind u8 + u32 byte length + bytes -> typed array + let kind = *bytes.get(*pos).ok_or("bytes kind")?; + let len = u32::from_le_bytes(bytes.get(*pos + 1..*pos + 5).ok_or("bytes len")?.try_into().map_err(|_| "bytes len")?) as usize; + *pos += 5; + let data = bytes.get(*pos..*pos + len).ok_or("bytes")?; + *pos += len; + Ok(v8_typed_array(scope, kind, data)) + } + 0x0D => { // struct/record as JSON: u32 len + utf8 -> plain object let len = u32::from_le_bytes(bytes[*pos..*pos + 4].try_into().map_err(|_| "json len")?) @@ -5574,6 +3915,28 @@ fn bin_read_value<'s>( let k = v8::String::new(scope, key).ok_or("v8 str")?; obj.set(scope, k.into(), arr.into()); } + // Extra named lists (events, nested types, ...): u16 count, then a key and a list each. + let extra = match bytes.get(*pos..*pos + 2) { + Some(b) => u16::from_le_bytes(b.try_into().unwrap()) as usize, + None => 0, + }; + if extra > 0 { + *pos += 2; + } + for _ in 0..extra { + let key = bin_take_str(bytes, pos, 2).ok_or("member list key")?.to_string(); + let count = u16::from_le_bytes(bytes.get(*pos..*pos + 2).ok_or("member count")?.try_into().unwrap()) as usize; + *pos += 2; + let arr = v8::Array::new(scope, count as i32); + for i in 0..count { + let s = bin_take_str(bytes, pos, 2).ok_or("member name")?; + if let Some(sv) = v8::String::new(scope, s) { + arr.set_index(scope, i as u32, sv.into()); + } + } + let k = v8::String::new(scope, &key).ok_or("v8 str")?; + obj.set(scope, k.into(), arr.into()); + } Ok(obj.into()) } diff --git a/runtime/src/js/dotnet.js b/runtime/src/js/dotnet.js new file mode 100644 index 0000000..708fe6f --- /dev/null +++ b/runtime/src/js/dotnet.js @@ -0,0 +1,1255 @@ +// NSWinRT.dotnet: .NET objects, namespaces and delegates as JS values. +// Shared by both engines: global_fns.rs HELPER_SOURCE (classic) and napi_engine/dotnet.rs. + +// Requires the dotnet-bridge project to be published into +// /dotnet-bridge/publish/DotNetBridge.dll +(function () { + if (typeof globalThis.__nsDotNetInvokeBin !== 'function') return; + globalThis.NSWinRT = globalThis.NSWinRT || {}; + + // Binary protocol: no JSON.stringify / JSON.parse. + // __nsDotNetInvokeBin(handle, typeName, assembly, method, ...args) + // throws on error, returns the result value directly. + function _invoke(req) { + var handle = (req.handle !== undefined && req.handle !== null) ? req.handle : -1; + var args = req.args; + var typeName = req.typeName || '', assembly = req.assembly || '', method = req.method || ''; + try { + if (!args || args.length === 0) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method); + if (args.length === 1) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, args[0]); + return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, ...args); + } catch (e) { + throw _dotnetError(e); + } + } + + // _invoke with a function's own `arguments`, unwrapped straight into the native call (no + // request object or intermediate arrays). + function _invokeWith(handle, typeName, assembly, method, args) { + var invoke = globalThis.__nsDotNetInvokeBin; + try { + for (var s = 0; s < args.length; s++) { + var a = args[s]; + if (a !== null && typeof a === 'object' && (a instanceof _NamedArgs || _isOutParam(a))) + return _invokeSpecial(handle, typeName, assembly, method, args); + } + switch (args.length) { + case 0: return invoke(handle, typeName, assembly, method); + case 1: return invoke(handle, typeName, assembly, method, _unwrap(args[0])); + case 2: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1])); + case 3: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1]), _unwrap(args[2])); + } + // Built by push so the array stays packed: apply on a holey array takes V8's slow path. + var call = [handle, typeName, assembly, method]; + for (var i = 0; i < args.length; i++) call.push(_unwrap(args[i])); + return invoke.apply(null, call); + } catch (e) { + throw _dotnetError(e); + } + } + + // `NSWinRT.dotnet.named({ b: 2 })` as the last argument: arguments by parameter name. + function _NamedArgs(values) { this.values = values; } + + // `NSWinRT.interop.out()` (an OutParam): a reference for a ref or out parameter. + function _isOutParam(a) { return Object.prototype.hasOwnProperty.call(a, '__nswinrt_out_param__'); } + + // A call with references or named arguments: `&Name` returns [result, ...arguments after the + // call] to fill the references in; `Name#a,b` names the trailing arguments. + function _invokeSpecial(handle, typeName, assembly, method, args) { + var call = [handle, typeName, assembly, '']; + var refs = false, named = null; + for (var i = 0; i < args.length; i++) { + var a = args[i]; + if (a !== null && typeof a === 'object' && a instanceof _NamedArgs) { + named = Object.keys(a.values || {}); + for (var k = 0; k < named.length; k++) call.push(_unwrap(a.values[named[k]])); + break; + } + if (a !== null && typeof a === 'object' && _isOutParam(a)) { + refs = true; + call.push(_unwrap(a.value === undefined ? null : a.value)); + } else { + call.push(_unwrap(a)); + } + } + call[3] = (refs ? '&' : '') + method + (named && named.length ? '#' + named.join(',') : ''); + var result = globalThis.__nsDotNetInvokeBin.apply(null, call); + if (!refs) return result; + for (var j = 0; j < args.length; j++) { + if (args[j] !== null && typeof args[j] === 'object' && _isOutParam(args[j])) args[j].value = _wrap(result[1 + j]); + } + return result[0]; + } + + // Turns a bridge error's raw `nativeException` into a proxy and appends the .NET stack. + function _dotnetError(e) { + if (!e || typeof e !== 'object' || e.__nsDotNet === true) return e; + var raw = e.nativeException; + if (!raw || typeof raw !== 'object') return e; + try { Object.defineProperty(e, '__nsDotNet', { value: true }); } catch (_) {} + e.nativeException = typeof raw.__handle === 'number' ? _wrap(raw) : raw; + if (typeof e.dotnetStack === 'string' && e.dotnetStack && typeof e.stack === 'string') e.stack += '\n' + e.dotnetStack; + return e; + } + + // An Error for a .NET exception passed to JS as a value (a faulted Task, an unhandled exception). + function _errorFromException(value) { + var ex = _wrap(value); + if (!ex || typeof ex !== 'object' || typeof ex.__handle !== 'number') + return value instanceof Error ? value : new Error(String(value)); + var message = ''; + try { message = String(ex.Message); } catch (_) {} + var err = new Error(message); + err.dotnetType = ex.__type; + try { err.dotnetStack = ex.StackTrace || ''; } catch (_) { err.dotnetStack = ''; } + err.nativeException = ex; + try { Object.defineProperty(err, '__nsDotNet', { value: true }); } catch (_) {} + if (err.dotnetStack && typeof err.stack === 'string') err.stack += '\n' + err.dotnetStack; + return err; + } + + // Populated lazily on first access; avoids repeated bridge round-trips. + var _typeInfoCache = {}; + // A type's member lists get `kinds` (member name -> _K_* flags), so the proxy traps look a name up + // instead of scanning the lists on every access. + var _K_PROP = 1, _K_WO = 2, _K_RO = 4, _K_SPROP = 8, _K_SMETHOD = 16, _K_SWO = 32, _K_SRO = 64, + _K_EVENT = 128, _K_SEVENT = 256, _K_METHOD = 512; + function _withKinds(info) { + var kinds = Object.create(null); + function add(list, flag) { + if (!list) return; + for (var i = 0; i < list.length; i++) kinds[list[i]] = (kinds[list[i]] | 0) | flag; + } + add(info.properties, _K_PROP); + add(info.writeonlyProperties, _K_WO); + add(info.readonlyProperties, _K_RO); + add(info.staticProperties, _K_SPROP); + add(info.staticMethods, _K_SMETHOD); + add(info.writeonlyStaticProperties, _K_SWO); + add(info.readonlyStaticProperties, _K_SRO); + add(info.events, _K_EVENT); + add(info.staticEvents, _K_SEVENT); + add(info.methods, _K_METHOD); + info.kinds = kinds; + return info; + } + function _kindsOf(info) { return info.kinds || _withKinds(info).kinds; } + + // "get_X" / "set_X" accessor names, made once per property name. + var _getterNames = Object.create(null), _setterNames = Object.create(null); + function _getterName(prop) { return _getterNames[prop] || (_getterNames[prop] = 'get_' + prop); } + function _setterName(prop) { return _setterNames[prop] || (_setterNames[prop] = 'set_' + prop); } + + // The native COM pointer of a handle's object as the BigInt the runtime reads from `__native_ptr`, + // or undefined when it has none. The bridge makes it for a plain .NET object only on request. + function _nativePtrOf(handle) { + try { + var p = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNativePtrForHandle', args: [handle] }); + if (typeof p !== 'number' || p === 0) return undefined; + return typeof BigInt === 'function' ? BigInt(p) : p; + } catch (_) { + return undefined; + } + } + var _emptyInfo = _withKinds({ methods: [], properties: [], staticMethods: [], staticProperties: [], readonlyProperties: [], readonlyStaticProperties: [], writeonlyProperties: [], writeonlyStaticProperties: [] }); + // Optional mapping for namespace prefixes -> assembly simple-name. + // Exact namespaces are preferred first, then progressively shorter + // prefixes are tried as a fallback. + var _namespaceAssemblyMap = Object.create(null); + + // _resolveAssembly results by type name, cleared whenever the namespace map changes. + var _assemblyCache = Object.create(null); + + function _resolveAssembly(typeName) { + if (!typeName || typeof typeName !== 'string') return ''; + var cached = _assemblyCache[typeName]; + if (cached !== undefined) return cached; + var resolved = ''; + // A closed generic (`MyLib.Box`1[System.String]`) lives where its definition does. + var cut = typeName.search(/[`\[]/); + var probe = cut > 0 ? typeName.substring(0, cut) : typeName; + while (probe) { + var assembly = _namespaceAssemblyMap[probe]; + if (typeof assembly === 'string' && assembly) { resolved = assembly; break; } + var lastDot = probe.lastIndexOf('.'); + if (lastDot < 0) break; + probe = probe.substring(0, lastDot); + } + _assemblyCache[typeName] = resolved; + return resolved; + } + + // One proxy per live .NET object. The bridge counts each time it sends a handle; `seen` is how + // many of those this proxy took, given back together when it is collected or released. + var _proxyByHandle = new Map(); + var _canWeakRefProxies = typeof WeakRef === 'function'; + var _dotNetFinalizers = typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(function (entry) { _releaseEntry(entry); }) + : null; + + function _releaseEntry(entry) { + if (_proxyByHandle.get(entry.handle) === entry) _proxyByHandle.delete(entry.handle); + var seen = entry.seen; + if (seen === 0) return; + entry.seen = 0; + try { + if (seen < 0) globalThis.__nsDotNetInvokeBin(entry.handle, '', '', '__release'); + else globalThis.__nsDotNetInvokeBin(entry.handle, '', '', '__release', seen); + } catch (e) {} + } + + // Registers `proxy` as the proxy of `handle`; seen < 0 releases the handle outright. + function _track(proxy, handle, seen) { + var entry = { handle: handle, seen: seen, ref: _canWeakRefProxies ? new WeakRef(proxy) : null }; + if (_canWeakRefProxies) _proxyByHandle.set(handle, entry); + if (_dotNetFinalizers) _dotNetFinalizers.register(proxy, entry, entry); + return entry; + } + + // Auto-populate namespace→assembly map from the managed side. + // GetNamespaceAssemblyMapJson scans the app directory for assemblies and + // returns a JSON map of { rootNamespace: assemblySimpleName }. + // Runs once at IIFE init; any failure is silently ignored. + try { + var _autoNsJson = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNamespaceAssemblyMapJson', args: [] }); + if (typeof _autoNsJson === 'string' && _autoNsJson) { + var _parsed = JSON.parse(_autoNsJson); + if (_parsed && typeof _parsed === 'object') { + for (var _k in _parsed) { + if (Object.prototype.hasOwnProperty.call(_parsed, _k) && typeof _parsed[_k] === 'string') { + _namespaceAssemblyMap[_k] = _parsed[_k]; + } + } + } + } + } catch (_e) {} + + // `handle`, when given, is an instance of the type: its members are read from it when the type + // can't be found by name. + function _getTypeInfo(assembly, typeName, handle) { + if (!typeName) return _emptyInfo; + var cached = _typeInfoCache[typeName]; + if (cached !== undefined) return cached; + var info = _emptyInfo; + try { + // Respect an explicitly-provided assembly name. When empty, + // let the managed side attempt resolution (BCL types via Type.GetType). + var asm = (typeof assembly === 'string') ? assembly : ''; + var members = _invoke({ assembly: asm, typeName: typeName, method: '__members__', args: [] }); + if (members && typeof members === 'object') info = _withKinds(members); + } catch (e) { + if (typeof handle === 'number' && handle >= 0) { + try { + var own = _invoke({ handle: handle, method: '__members__', args: [] }); + if (own && typeof own === 'object') info = _withKinds(own); + } catch (_) {} + } + } + _typeInfoCache[typeName] = info; + return info; + } + + function _unwrap(v) { + if (v && typeof v === 'object' && typeof v.__handle === 'number') return { __handle: v.__handle }; + if (Array.isArray(v)) return v.map(_unwrap); + // A JS Date reaches DateTime / DateTimeOffset parameters as its ISO 8601 string. + if (v instanceof Date) return isNaN(v.getTime()) ? null : v.toISOString(); + // A JS function passed where .NET expects a delegate gets .NET objects as arguments, the same + // proxies a method result would be, rather than raw handles. + if (typeof v === 'function' && typeof v.__dotnetType__ !== 'string' && typeof v.__handle !== 'number') + return _callbackFor(v); + return v; + } + + // The wrapper sent for a JS callback: one per function, so passing it again sends the same one. + var _callbacks = typeof WeakMap === 'function' ? new WeakMap() : null; + function _callbackFor(fn) { + var wrapped = _callbacks && _callbacks.get(fn); + if (wrapped) return wrapped; + wrapped = function () { return fn.apply(this, Array.prototype.map.call(arguments, _wrap)); }; + if (_callbacks) _callbacks.set(fn, wrapped); + return wrapped; + } + + // .NET collections (List, Dictionary, ObservableCollection) act like JS arrays: `length`, + // `list[i]`, for...of, and the array methods (map, filter, ...) on a copy of their items. + // _collectionKind is the bridge's CollectionKind flags (1 enumerable, 2 count, 4 indexed) by type. + var _collectionKinds = Object.create(null); + var _arrayMethods = Object.create(null); + ['forEach', 'map', 'filter', 'reduce', 'reduceRight', 'some', 'every', 'find', 'findIndex', 'findLast', + 'findLastIndex', 'indexOf', 'lastIndexOf', 'includes', 'join', 'slice', 'concat', 'flat', 'flatMap', + 'entries', 'keys', 'values', 'at', 'toJSON'].forEach(function (name) { _arrayMethods[name] = 1; }); + + function _bridgeCall(method, args) { + return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: method, args: args }); + } + + function _isIndex(prop) { + var c = prop.charCodeAt(0); + return c >= 48 && c <= 57 && /^(0|[1-9]\d*)$/.test(prop); + } + + function _collectionKind(handle, typeName) { + var kind = typeName ? _collectionKinds[typeName] : undefined; + if (kind !== undefined) return kind; + try { kind = _bridgeCall('CollectionKind', [{ __handle: handle }]) | 0; } catch (_) { kind = 0; } + if (typeName) _collectionKinds[typeName] = kind; + return kind; + } + + function _collectionItems(handle) { + return _wrap(_bridgeCall('CollectionItems', [{ __handle: handle }])) || []; + } + + // `prop` of a collection proxy when it is one of the array-like members, else undefined. + function _collectionMember(handle, typeName, prop) { + var kind = _collectionKind(handle, typeName); + if (prop === 'length') return (kind & 2) ? _bridgeCall('CollectionCount', [{ __handle: handle }]) : undefined; + if (_arrayMethods[prop] === 1) { + if (!(kind & 1)) return undefined; + return function () { + var items = _collectionItems(handle); + return prop === 'toJSON' ? items : Array.prototype[prop].apply(items, arguments); + }; + } + return (kind & 4) ? _wrap(_bridgeCall('ItemAt', [{ __handle: handle }, +prop])) : undefined; + } + + // A token per JS function subscribed to a .NET event, so remove_X(fn) finds its delegate. + var _fnTokens = typeof WeakMap === 'function' ? new WeakMap() : null; + var _nextFnToken = 1; + function _fnToken(fn) { + var token = _fnTokens && _fnTokens.get(fn); + if (!token) { + token = _nextFnToken++; + if (_fnTokens) _fnTokens.set(fn, token); + } + return token; + } + + // The event name of `add_X` / `remove_X` when X is an event with flag `flag` in `kinds`. + function _eventOf(prop, kinds, flag) { + var name = prop.charCodeAt(0) === 97 && prop.indexOf('add_') === 0 ? prop.substring(4) + : prop.charCodeAt(0) === 114 && prop.indexOf('remove_') === 0 ? prop.substring(7) : null; + return name !== null && ((kinds[name] | 0) & flag) ? name : null; + } + + // `add_X(fn)` / `remove_X(fn)` of an instance (handle) or a type's static event (typeName). + function _eventAccessor(add, handle, typeName, name) { + return function (fn) { + var target = handle >= 0 ? { __handle: handle } : null; + // A delegate made with asDelegate (or one .NET returned) is added and removed as itself. + if (typeof fn !== 'function' || typeof fn.__handle === 'number' || typeof fn.__dotnetType__ === 'string') + return _wrap(_invoke({ handle: handle, typeName: typeName, assembly: _resolveAssembly(typeName), method: (add ? 'add_' : 'remove_') + name, args: [_unwrap(fn)] })); + if (add) _bridgeCall('AddEventHandler', [target, typeName, name, _fnToken(fn), _callbackFor(fn)]); + else _bridgeCall('RemoveEventHandler', [target, typeName, name, _fnToken(fn)]); + }; + } + + // `obj.X = fn` for an event X: replaces the handler assigned before; null removes it. + function _assignEvent(handle, typeName, name, fn) { + if (fn !== null && fn !== undefined && typeof fn !== 'function') + throw new TypeError('Assign a function (or null) to the .NET event \'' + name + '\''); + _bridgeCall('SetEventHandler', [handle >= 0 ? { __handle: handle } : null, typeName, name, fn ? _callbackFor(fn) : null]); + } + + // The .NET name of a type argument for `.of(...)`: a .NET type proxy, a type name, or the JS + // constructor of a primitive. + function _typeArgName(t) { + if (typeof t === 'string') return t; + if (t === String) return 'System.String'; + if (t === Number) return 'System.Double'; + if (t === Boolean) return 'System.Boolean'; + if (t === Object) return 'System.Object'; + if (t === Date) return 'System.DateTime'; + if (typeof BigInt === 'function' && t === BigInt) return 'System.Int64'; + var name = t && (typeof t === 'function' || typeof t === 'object') ? t.__dotnetType__ : undefined; + if (typeof name === 'string' && name) return name; + throw new TypeError('Not a .NET type: ' + String(t)); + } + + function _typeArgList(args) { + return Array.prototype.map.call(args, _typeArgName).join(','); + } + + // Adds `.of(T1, ...)` to a method function, for explicit generic type arguments. + function _withTypeArgs(fn, call) { + fn.of = function () { + var suffix = '<' + _typeArgList(arguments) + '>'; + return function () { return call(suffix, arguments); }; + }; + return fn; + } + + // Makes sw.Stop() and sw.Elapsed both work naturally. + // The proxy is registered with _dotNetFinalizers so the CLR reference + // is released automatically when JS GC collects the proxy. + function _makeDotNetInstance(handle, assembly, typeName, isTask, nativePtr) { + _getTypeInfo(assembly, typeName, handle); + // Method functions by name, made on first access. + var methods = null; + // Handlers assigned to events (`obj.Click = fn`), by event name. + var assigned = null; + var ptrKnown = nativePtr !== undefined; + // The proxy target holds the plain JS fields set on the object. + var proxy = new Proxy({}, { + get: function (target, prop) { + if (typeof prop === 'symbol') { + if (prop === Symbol.iterator && (_collectionKind(handle, typeName) & 1)) + return function () { return _collectionItems(handle)[Symbol.iterator](); }; + return target[prop]; + } + if (prop === '__handle') return handle; + if (prop === '__type') return typeName; + if (prop === '__isTask') return isTask === true; + if (prop === '__native_ptr') { + if (!ptrKnown) { ptrKnown = true; nativePtr = _nativePtrOf(handle); } + return nativePtr; + } + if (prop === 'release') return function () { + var entry = _proxyByHandle.get(handle); + if (entry && _dotNetFinalizers) _dotNetFinalizers.unregister(entry); + _releaseEntry(entry || { handle: handle, seen: -1 }); + }; + // Like a plain object, so `'' + obj` and comparisons fall through to toString. + if (prop === 'valueOf') return function () { return proxy; }; + // The object's .NET ToString(). + if (prop === 'toString') return function () { + return String(_invoke({ handle: handle, method: 'ToString', args: [] })); + }; + // A Task/ValueTask (awaitable) result is thenable, so `await obj.SomethingAsync()` works; + // any other .NET object must not look like a promise. + if (prop === 'then') { + if (isTask !== true) return undefined; + return function (onFulfilled, onRejected) { + return globalThis.NSWinRT.dotnet.taskToPromise({ __handle: handle, __isTask: true }).then(onFulfilled, onRejected); + }; + } + if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop]; + // Re-read info in case it was populated after construction. + var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; + // Write-only (setter, no getter): reading it is an error. + if (kind & _K_WO) + throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); + if (kind & _K_PROP) + return _wrap(_invoke({ handle: handle, method: _getterName(prop), args: [] })); + if (kind & _K_EVENT) return (assigned && assigned[prop]) || null; + if (prop === 'length' || _arrayMethods[prop] === 1 || _isIndex(prop)) { + var member = _collectionMember(handle, typeName, prop); + if (member !== undefined) return member; + } + if (methods !== null && methods[prop]) return methods[prop]; + // A key of a string-keyed dictionary (`dict.key`, `dict['key']`). + if (!(kind & _K_METHOD) && (_collectionKind(handle, typeName) & 8)) { + var hit = _bridgeCall('DictionaryTryGet', [{ __handle: handle }, prop]); + if (hit && hit[0] === true) return _wrap(hit[1]); + } + // Not a native property: a function for method dispatch. + if (methods === null) methods = Object.create(null); + var ev = _eventOf(prop, _kindsOf(_typeInfoCache[typeName] || _emptyInfo), _K_EVENT); + if (ev !== null) return methods[prop] = _eventAccessor(prop.charCodeAt(0) === 97, handle, '', ev); + return methods[prop] = _withTypeArgs(function () { + return _wrap(_invokeWith(handle, '', '', prop, arguments)); + }, function (suffix, args) { + return _wrap(_invokeWith(handle, '', '', prop + suffix, args)); + }); + }, + set: function (target, prop, value) { + if (typeof prop !== 'symbol') { + var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; + // Read-only (getter, no setter): assigning it is an error. + if (kind & _K_RO) + throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); + // Writable (read-write or write-only): invoke the setter. + if (kind & (_K_PROP | _K_WO)) { + _invoke({ handle: handle, method: _setterName(prop), args: [_unwrap(value)] }); + return true; + } + if (kind & _K_EVENT) { + _assignEvent(handle, '', prop, value); + (assigned || (assigned = Object.create(null)))[prop] = value || null; + return true; + } + if (_isIndex(prop) && (_collectionKind(handle, typeName) & 4)) { + _bridgeCall('SetItemAt', [{ __handle: handle }, +prop, _unwrap(value)]); + return true; + } + if (!(kind & _K_METHOD) && (_collectionKind(handle, typeName) & 8)) { + _bridgeCall('DictionarySet', [{ __handle: handle }, prop, _unwrap(value)]); + return true; + } + } + // Not a native property: a plain JS field of this object. + target[prop] = value; + return true; + }, + }); + return proxy; + } + + function _wrap(value) { + if (value == null) return null; + if (Array.isArray(value)) return value.map(_wrap); + if (typeof value === 'object' && typeof value.__handle === 'number') { + var typeName = value.__type || ''; + var jsSelf = _jsSelfByHandle && _deref(_jsSelfByHandle.get(value.__handle)); + if (jsSelf) return jsSelf; + // A JS subclass instance whose JS object was collected: its dispatcher revives one. + if (typeName.indexOf('NSWinRTDynamicProxies.') === 0) { + try { _invoke({ handle: value.__handle, method: '__ns_js_self', args: [] }); } catch (_) {} + jsSelf = _deref(_jsSelfByHandle.get(value.__handle)); + if (jsSelf) return jsSelf; + } + var entry = _proxyByHandle.get(value.__handle); + var existing = entry && entry.ref && entry.ref.deref(); + if (existing) { + entry.seen++; + return existing; + } + var assembly = _resolveAssembly(typeName); + var proxy = _makeDotNetInstance(value.__handle, assembly, typeName, value.__isTask === true, value.__native_ptr); + _track(proxy, value.__handle, 1); + return proxy; + } + return value; + } + + globalThis.NSWinRT.dotnet = { + invoke: function (assembly, typeName, method, args) { + return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: (args || []).map(_unwrap) })); + }, + get: function (assembly, typeName, prop) { + return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: 'get_' + prop, args: [] })); + }, + fromHandle: function (handle, typeName) { + var entry = _proxyByHandle.get(handle); + var existing = entry && entry.ref && entry.ref.deref(); + if (existing) return existing; + var assembly = _resolveAssembly(typeName || ''); + var proxy = _makeDotNetInstance(handle, assembly, typeName || ''); + _track(proxy, handle, -1); + return proxy; + }, + // Arguments by parameter name, as the last argument: `F(x, named({ digits: 2 }))`. + named: function (values) { return new _NamedArgs(values || {}); }, + // Loads an assembly (a path relative to the app, or absolute) and makes its namespaces globals. + loadAssembly: function (path) { + var map = JSON.parse(_bridgeCall('LoadAssembly', [String(path)]) || '{}'); + _assemblyCache = Object.create(null); + for (var cached in _typeInfoCache) if (_typeInfoCache[cached] === _emptyInfo) delete _typeInfoCache[cached]; + for (var kind in _typeKindCache) if (_typeKindCache[kind] === '') delete _typeKindCache[kind]; + for (var ns in map) { + if (typeof map[ns] !== 'string') continue; + _namespaceAssemblyMap[ns] = map[ns]; + if (ns.indexOf('.') < 0 && /^[A-Za-z_$][\w$]*$/.test(ns)) this.registerNamespace(ns); + } + return Object.keys(map); + }, + // Exposed so other IIFEs in this bootstrap (e.g. the managed-subclass/proxy block, + // which runs in its own closure earlier in the script and has no direct access to + // this IIFE's private `_wrap`) can wrap a raw tagged handle value the same way. + wrap: _wrap, + // Lazily register a namespace prefix and optional assembly mapping. + // Example: NSWinRT.dotnet.registerNamespace('com', 'NativeScript'); + registerNamespace: function(rootName, assemblyName) { + if (!rootName || typeof rootName !== 'string') return; + var name = String(rootName); + var root = name.split('.')[0]; + if (assemblyName && typeof assemblyName === 'string') { + _assemblyCache = Object.create(null); + _namespaceAssemblyMap[name] = assemblyName; + if (!_namespaceAssemblyMap[root]) { + _namespaceAssemblyMap[root] = assemblyName; + } + } + if (root in globalThis) return; + try { + Object.defineProperty(globalThis, root, { + configurable: true, + enumerable: true, + get: function() { + var proxy = _makeNamespaceProxy(root); + try { Object.defineProperty(globalThis, root, { value: proxy, writable: true, configurable: true, enumerable: true }); } catch (_) {} + return proxy; + }, + // A namespace root never blocks app code from using the name for its own value + // (`var App = ...` in a classic script assigns through this accessor). + set: function(value) { + try { Object.defineProperty(globalThis, root, { value: value, writable: true, configurable: true, enumerable: true }); } catch (_) {} + } + }); + } catch (_) { + try { globalThis[root] = _makeNamespaceProxy(root); } catch (_) {} + } + }, + registerNamespaces: function(arr) { + if (!Array.isArray(arr)) return; + for (var i = 0; i < arr.length; i++) this.registerNamespace(arr[i]); + }, + }; + + // .NET types as JS classes + // A .NET type proxy (System.Text.StringBuilder, MyApp.Native.Greeter) is a constructor with a + // stable `prototype`, so it can be extended the way Java and Objective-C classes are on Android + // and iOS: + // class Cat extends MyApp.Animal { constructor(n) { super(n); } Speak() { return super.Speak(); } } + // const Dog = MyApp.Animal.extend('Dog', { Speak() { return this.super.Speak(); } }); + // const calc = new MyApp.ICalculator({ Compute(a, b) { return a * b; } }); + // Constructing a subclass creates the JS object first, then a managed subclass of the .NET type + // whose overridden virtuals (and implemented interface members) call back into that object. + // `super.X()` reaches the base implementation through the non-virtual `__ns_base_X` entry point + // the bridge emits for every overridden member. Native code handing the instance back to JS gets + // the same JS object. + var _nsProxyCache = Object.create(null); + var _namespaceExtras = { System: Object.create(null) }; + var _typeKindCache = Object.create(null); + var _typeProtoCache = Object.create(null); + var _subclassInfoCache = new WeakMap(); + var _jsSelfByHandle = new Map(); + + // Lifetime of JS subclass instances + // An instance is a JS object plus the managed object it drives. The JS object holds the managed + // one through its handle; the managed object reaches the JS object only through its dispatcher, + // which holds it weakly. So once JS drops the JS object, its handle is released and the managed + // object goes as soon as .NET/native code drops it too (the bridge then unpins the dispatcher). + // When native code calls into an instance whose JS object was collected, a new JS object of the + // same class is made for it: fields its constructor set are gone, as on Android when a JS object + // used only from native code is collected. Subclasses of WinRT UI classes are kept strongly while + // they are in a live XAML tree. + var _canWeakRef = typeof WeakRef === 'function'; + var _selfFinalizers = typeof FinalizationRegistry === 'function' + ? new FinalizationRegistry(function (handle) { + if (!_deref(_jsSelfByHandle.get(handle))) _jsSelfByHandle.delete(handle); + try { _invoke({ handle: handle, method: '__release', args: [] }); } catch (_) {} + }) + : null; + + function _weak(obj) { return _canWeakRef ? new WeakRef(obj) : obj; } + function _deref(ref) { return ref && _canWeakRef && ref instanceof WeakRef ? ref.deref() : ref; } + + // Gives `self` the bridge identity in `raw` and registers it as the JS object of `entry`'s instance. + function _adopt(entry, self, raw) { + Object.defineProperty(self, '__handle', { value: raw.__handle }); + Object.defineProperty(self, '__nsOverrides', { value: entry.overrides }); + if (!entry.winrt) { + Object.defineProperty(self, '__type', { value: raw.__type || entry.typeName }); + if (raw.__native_ptr !== undefined) Object.defineProperty(self, '__native_ptr', { value: raw.__native_ptr }); + } + entry.ref = _weak(self); + _jsSelfByHandle.set(raw.__handle, _weak(self)); + if (_selfFinalizers) _selfFinalizers.register(self, raw.__handle); + if (entry.winrt) { + entry.strong = self; + _attachedEntries.add(entry); + } + _sweepAttached(); + return self; + } + + // The JS object of `entry`'s instance, made anew when the previous one was collected. + function _selfFor(entry, raw) { + var self = _deref(entry.ref); + if (self) return self; + if (entry.constructing) throw new Error(entry.typeName + ' called into its JS subclass before the instance existed'); + if (!raw || typeof raw.__handle !== 'number') throw new Error('The ' + entry.typeName + ' subclass instance is gone'); + if (entry.winrt) { + self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, entry.typeName); + Object.setPrototypeOf(self, entry.proto); + } else { + self = Object.create(entry.proto); + } + return _adopt(entry, self, raw); + } + + // The dispatcher native code calls the instance's overridden and interface members through. + function _makeDispatcher(entry) { + return function (selfHandle, member) { + var self = _selfFor(entry, selfHandle); + if (member === '__ns_js_self') return undefined; + _sweepAttached(); + var margs = Array.prototype.slice.call(arguments, 2).map(_wrap); + return _dispatchToJs(self, entry.baseProto, entry.typeName, member, margs); + }; + } + + // Subclasses of WinRT classes held strongly (entry.strong) while in a live XAML tree, re-checked at + // most every two seconds when subclass instances are created or called into. + var _attachedEntries = new Set(); + var _lastAttachedSweep = 0; + function _sweepAttached() { + var now = Date.now(); + if (now - _lastAttachedSweep < 2000) return; + _lastAttachedSweep = now; + _attachedEntries.forEach(function (entry) { + var self = _deref(entry.ref); + if (!self) { + _attachedEntries.delete(entry); + return; + } + var attached = false; + try { attached = self.XamlRoot != null; } catch (_) {} + entry.strong = attached ? self : undefined; + }); + } + var _baseCallPrefix = '__ns_base_'; + + function _has(list, name) { + return !!list && list.indexOf(name) >= 0; + } + + function _typeKind(path) { + var kind = _typeKindCache[path]; + if (kind !== undefined) return kind; + try { + kind = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'TypeKind', args: [path] }) || ''; + } catch (_) { + kind = ''; + } + _typeKindCache[path] = kind; + return kind; + } + + // Full name of a .NET type proxy, or of the .NET base of a JS class deriving from one. + function _dotnetTypeName(type) { + if (typeof type === 'string') return type; + if (!type || (typeof type !== 'function' && typeof type !== 'object')) return ''; + var name = type.__dotnetType__; + return typeof name === 'string' ? name : ''; + } + + // The handle of an instance of a JS subclass (an own property); -1 for anything else, such as + // a class prototype further down the chain, without running any property trap. + function _ownHandle(o) { + if (!o || (typeof o !== 'object' && typeof o !== 'function')) return -1; + var d = Object.getOwnPropertyDescriptor(o, '__handle'); + return d && typeof d.value === 'number' ? d.value : -1; + } + + function _baseCallName(self, member) { + var overrides = self.__nsOverrides; + return overrides && overrides.has(member) ? _baseCallPrefix + member : member; + } + + // A native member of `self` (an instance of a JS subclass), reached through the .NET type's + // prototype: properties are read through their getter, methods become functions. Members the JS + // class overrides resolve to the base implementation: only `super.X` gets this far for them. + function _baseMember(path, self, prop) { + if (prop === 'toString') return function () { return String(_invoke({ handle: self.__handle, method: 'ToString', args: [] })); }; + if (prop === 'valueOf') return function () { return this; }; + var info = _getTypeInfo(_resolveAssembly(path), path); + var overrides = self.__nsOverrides; + if (_has(info.properties, prop) || _has(info.protectedProperties, prop) || (overrides && overrides.has('get_' + prop))) { + return _wrap(_invoke({ handle: self.__handle, method: _baseCallName(self, 'get_' + prop), args: [] })); + } + if (_has(info.writeonlyProperties, prop)) + throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); + if (_has(info.methods, prop) || _has(info.protectedMethods, prop) || (overrides && overrides.has(prop))) { + var method = _baseCallName(self, prop); + return function () { + var args = Array.prototype.slice.call(arguments).map(_unwrap); + return _wrap(_invoke({ handle: self.__handle, method: method, args: args })); + }; + } + return undefined; + } + + // The `prototype` of a .NET type: the end of every JS subclass's prototype chain. Reads resolve + // native members against the receiving instance; writes go to native properties, and anything + // else becomes a plain JS field of the instance. + function _typePrototype(path) { + var cached = _typeProtoCache[path]; + if (cached) return cached; + var proto = new Proxy(Object.create(null), { + get: function (_, prop, receiver) { + if (typeof prop === 'symbol') return undefined; + if (_ownHandle(receiver) < 0) { + // Read off the prototype itself (TypeScript's ES5 super call + // `_super.prototype.Method.call(this, ...)`): a function that runs the base + // implementation on whichever instance it is called with. + var typeInfo = _getTypeInfo(_resolveAssembly(path), path); + if (!_has(typeInfo.methods, prop)) return undefined; + return function () { + if (_ownHandle(this) < 0) throw new TypeError(path + '.prototype.' + prop + ' called on an object that is not a ' + path); + return _baseMember(path, this, prop).apply(this, arguments); + }; + } + return _baseMember(path, receiver, prop); + }, + set: function (_, prop, value, receiver) { + if (typeof prop !== 'symbol' && _ownHandle(receiver) >= 0) { + var info = _getTypeInfo(_resolveAssembly(path), path); + if (_has(info.readonlyProperties, prop)) + throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); + if (_has(info.properties, prop) || _has(info.writeonlyProperties, prop) || _has(info.protectedProperties, prop)) { + _invoke({ handle: receiver.__handle, method: _baseCallName(receiver, 'set_' + prop), args: [_unwrap(value)] }); + return true; + } + } + Object.defineProperty(receiver, prop, { value: value, writable: true, enumerable: true, configurable: true }); + return true; + }, + }); + _typeProtoCache[path] = proto; + return proto; + } + + // Members (literal CLR names: methods, get_X/set_X accessors) and interfaces a JS class adds on + // top of its .NET base, from every class between it and the base. + function _subclassInfo(proto, baseProto) { + var cached = _subclassInfoCache.get(proto); + if (cached) return cached; + var members = []; + var overrides = new Set(); + var interfaces = []; + var seen = Object.create(null); + function addInterfaces(list) { + if (!Array.isArray(list)) return; + for (var i = 0; i < list.length; i++) { + var name = _dotnetTypeName(list[i]) || (list[i] && (list[i].__typeName__ || list[i].name)) || ''; + if (name && interfaces.indexOf(name) < 0) interfaces.push(name); + } + } + for (var p = proto; p && p !== baseProto; p = Object.getPrototypeOf(p)) { + var keys = Object.getOwnPropertyNames(p); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (key === 'constructor' || key === 'interfaces' || key === 'super' || key === 'init' || seen[key]) continue; + seen[key] = true; + var d = Object.getOwnPropertyDescriptor(p, key); + if (typeof d.get === 'function') { members.push('get_' + key); overrides.add('get_' + key); } + if (typeof d.set === 'function') { members.push('set_' + key); overrides.add('set_' + key); } + if (typeof d.value === 'function') { members.push(key); overrides.add(key); } + } + if (Object.prototype.hasOwnProperty.call(p, 'interfaces')) addInterfaces(p.interfaces); + var ctor = Object.prototype.hasOwnProperty.call(p, 'constructor') ? p.constructor : null; + if (ctor && Object.prototype.hasOwnProperty.call(ctor, 'interfaces')) addInterfaces(ctor.interfaces); + } + var info = { members: members, overrides: overrides, interfaces: interfaces }; + _subclassInfoCache.set(proto, info); + return info; + } + + // The JS-defined member `key` of `self` (an own field, or a member of one of its JS classes). + function _jsMember(self, key, baseProto) { + for (var o = self; o && o !== baseProto; o = Object.getPrototypeOf(o)) { + var d = Object.getOwnPropertyDescriptor(o, key); + if (d) return d; + } + return null; + } + + // Native code calling an overridden member (or an implemented interface member) of `self`. + function _dispatchToJs(self, baseProto, typeName, member, args) { + var accessor = /^(get|set)[_:](.+)$/.exec(member); + if (accessor) { + var ad = _jsMember(self, accessor[2], baseProto); + if (ad) { + if (accessor[1] === 'get') { + if (typeof ad.get === 'function') return ad.get.call(self); + if ('value' in ad && typeof ad.value !== 'function') return ad.value; + } else if (typeof ad.set === 'function') { + ad.set.call(self, args[0]); + return undefined; + } else if ('value' in ad && ad.writable) { + self[accessor[2]] = args[0]; + return undefined; + } + } + } + var md = _jsMember(self, member, baseProto); + if (md && typeof md.value === 'function') return md.value.apply(self, args); + throw new Error((self.constructor && self.constructor.name || typeName) + ' does not implement ' + member); + } + + function _constructSubclass(path, args, proto) { + if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') + throw new Error('Extending .NET types is not available in this runtime'); + var baseProto = _typePrototype(path); + var info = _subclassInfo(proto, baseProto); + var self = Object.create(proto); + // A virtual the base constructor calls reaches `self` before it has its handle. + var entry = { proto: proto, baseProto: baseProto, typeName: path, overrides: info.overrides, winrt: false, ref: self }; + var raw = globalThis.__nsDotNetCreateJsSubclass('', path, info.interfaces, info.members, _makeDispatcher(entry), args.map(_unwrap)); + if (!raw || typeof raw.__handle !== 'number') throw new Error('Could not create an instance of a subclass of ' + path); + return _adopt(entry, self, raw); + } + + // `new SomeInterface({ ... })`: an object implementing the interface with the given members. + function _implementInterface(path, impl) { + var proto = Object.create(_typePrototype('System.Object')); + Object.defineProperties(proto, Object.getOwnPropertyDescriptors(impl)); + Object.defineProperty(proto, 'interfaces', { value: [path] }); + return _constructSubclass('System.Object', [], proto); + } + + // `this.super` inside a class made by extend(): the parent class's members bound to `self`. + function _superOf(self, Cls) { + var parent = Object.getPrototypeOf(Cls.prototype); + return new Proxy(Object.create(null), { + get: function (_, prop) { + var value = Reflect.get(parent, prop, self); + return typeof value === 'function' ? value.bind(self) : value; + }, + }); + } + + // Base.extend([name,] members): an ES2015 subclass of Base with the given members; `init`, when + // present, runs after construction with the constructor arguments. + function _extendClass(Base, nameOrMembers, maybeMembers) { + var hasName = typeof nameOrMembers === 'string'; + var members = (hasName ? maybeMembers : nameOrMembers) || {}; + var Extended = class extends Base { + constructor() { + super(...arguments); + if (typeof members.init === 'function') members.init.apply(this, arguments); + } + }; + var descriptors = Object.getOwnPropertyDescriptors(members); + delete descriptors.init; + if (descriptors.interfaces) { + Object.defineProperty(Extended.prototype, 'interfaces', { value: members.interfaces }); + delete descriptors.interfaces; + } + Object.defineProperties(Extended.prototype, descriptors); + Object.defineProperty(Extended.prototype, 'super', { + get: function () { return _superOf(this, Extended); }, + }); + if (hasName) Object.defineProperty(Extended, 'name', { value: nameOrMembers.split('.').pop() }); + return Extended; + } + + // `_super.call(this, ...)` / `_super.apply(this, args)` in TypeScript's ES5 output (what the + // @NativeClass transform emits): construct the subclass instance `thisArg` was created for. + function _es5SuperCall(path, thisArg, args) { + var proto = thisArg && Object.getPrototypeOf(thisArg); + if (!proto || !Object.prototype.isPrototypeOf.call(_typePrototype(path), proto)) + throw new TypeError('Class constructor ' + path + ' cannot be invoked without \'new\''); + return _constructSubclass(path, args, proto); + } + + function _hasInstance(path, value) { + if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false; + if (Object.prototype.isPrototypeOf.call(_typePrototype(path), value)) return true; + if (typeof value.__handle !== 'number') return false; + try { + return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'IsInstanceOf', args: [{ __handle: value.__handle }, path] }) === true; + } catch (_) { + return false; + } + } + + // WinRT classes as JS base classes + // `class FixedPanel extends Microsoft.UI.Xaml.Controls.Panel { MeasureOverride(size) { ... } }`: + // the WinRT constructor hands `super(...)` here. The instance is a composed object created by + // the .NET bridge from the C#/WinRT projection of the class (the way a C# subclass is), so XAML + // calling an overridable member (MeasureOverride, ArrangeOverride, OnApplyTemplate, ...) lands in + // the JS override. The JS object is the ordinary WinRT wrapper of that object, with the subclass + // prototype spliced in front of the class's native members. + var _linkedWinRTPrototypes = new WeakSet(); + + // The WinRT class a JS class derives from: the nearest constructor with its own __typeName__. + function _winrtBaseOf(ctor) { + for (var c = ctor; c; c = Object.getPrototypeOf(c)) { + if (Object.prototype.hasOwnProperty.call(c, '__typeName__')) return c; + } + return null; + } + + // `super.X(...)` for a member a JS subclass of a WinRT class overrides: the base implementation, + // through the bridge (the native member would dispatch back into the override). + function _winrtBaseCall(handle, prop) { + return function () { + var args = Array.prototype.slice.call(arguments).map(_unwrap); + return _wrap(_invoke({ handle: handle, method: _baseCallPrefix + prop, args: args })); + }; + } + + // Puts a layer under the JS subclass prototypes that resolves the class's native members and + // sends `super.X()` for overridden members to the base implementation. On the classic engine the + // class's `prototype` is separate from its instances' native prototype, so the layer goes under the + // class's `prototype` once; on the napi engines they're the same object, so it goes under the + // subclass prototype that sits directly on it. + function _linkWinRTSubclassPrototype(base, nativeProto, proto) { + if (!base || !nativeProto) return; + var target = base.prototype; + if (target === nativeProto) { + target = proto; + while (target && Object.getPrototypeOf(target) !== nativeProto) target = Object.getPrototypeOf(target); + } + if (!target || _linkedWinRTPrototypes.has(target)) return; + _linkedWinRTPrototypes.add(target); + var layer = new Proxy(Object.create(nativeProto), { + get: function (_, prop, receiver) { + var handle = _ownHandle(receiver); + if (typeof prop !== 'string') return Reflect.get(nativeProto, prop, receiver); + if (handle >= 0) { + if (receiver.__nsOverrides && receiver.__nsOverrides.has(prop)) return _winrtBaseCall(handle, prop); + return Reflect.get(nativeProto, prop, receiver); + } + // Read off a prototype (TypeScript's ES5 `_super.prototype.X.call(this, ...)`): decide + // between the base implementation and the native member when it's called. + var native = Reflect.get(nativeProto, prop, receiver); + if (typeof native !== 'function') return native; + return function () { + var h = _ownHandle(this); + if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(prop)) + return _winrtBaseCall(h, prop).apply(this, arguments); + return native.apply(this, arguments); + }; + }, + has: function (_, prop) { return prop in nativeProto; }, + }); + Object.setPrototypeOf(target, layer); + } + + // napi engines: the class `prototype` is the native prototype itself, so TypeScript's ES5 + // `_super.prototype.X.call(this, ...)` reaches the native member directly. Wrap each native member a + // subclass overrides (once) so that, called on a JS subclass instance, it runs the base + // implementation instead of dispatching back into the override. + function _guardNativeMembers(nativeProto, overrides) { + overrides.forEach(function (name) { + for (var p = nativeProto; p; p = Object.getPrototypeOf(p)) { + var d = Object.getOwnPropertyDescriptor(p, name); + if (!d) continue; + if (typeof d.value === 'function' && !d.value.__nsGuarded && d.writable) { + var native = d.value; + var guarded = function () { + var h = _ownHandle(this); + if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(name)) + return _winrtBaseCall(h, name).apply(this, arguments); + return native.apply(this, arguments); + }; + Object.defineProperty(guarded, '__nsGuarded', { value: true }); + Object.defineProperty(p, name, { value: guarded, writable: true, configurable: true, enumerable: d.enumerable }); + } + return; + } + }); + } + + // Called by the WinRT class constructors for `super(...)` from a JS subclass (newTarget) or + // TypeScript's ES5 `_super.call(this, ...)` (es5This). Returns undefined when this isn't a subclass + // construction or the .NET bridge isn't available; the constructor then builds a plain instance. + globalThis.__nsConstructWinRTSubclass = function (typeName, newTarget, args, es5This) { + var proto; + var base; + if (newTarget) { + proto = newTarget.prototype; + base = _winrtBaseOf(Object.getPrototypeOf(newTarget)); + } else { + proto = es5This && Object.getPrototypeOf(es5This); + base = proto && proto.constructor ? _winrtBaseOf(Object.getPrototypeOf(proto.constructor)) : null; + if (!base || base.__typeName__ !== typeName || Object.prototype.hasOwnProperty.call(proto.constructor, '__typeName__')) + return undefined; + } + if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function' || typeof globalThis.__nsWrapWinRTPointer !== 'function') + return undefined; + var baseProto = base ? base.prototype : null; + var info = _subclassInfo(proto, baseProto); + var entry = { proto: proto, baseProto: baseProto, typeName: typeName, overrides: info.overrides, winrt: true, constructing: true }; + var raw = globalThis.__nsDotNetCreateJsSubclass('', typeName, info.interfaces, info.members, _makeDispatcher(entry), (args || []).map(_unwrap)); + entry.constructing = false; + if (!raw || raw.__native_ptr === undefined || raw.__native_ptr === null) + throw new Error('Could not create a WinRT object for a subclass of ' + typeName); + var self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, typeName); + var nativeProto = Object.getPrototypeOf(self); + _linkWinRTSubclassPrototype(base, nativeProto, proto); + if (base && base.prototype === nativeProto) _guardNativeMembers(nativeProto, info.overrides); + Object.setPrototypeOf(self, proto); + return _adopt(entry, self, raw); + }; + + globalThis.NSWinRT.extendClass = _extendClass; + + // System.Diagnostics.Stopwatch.StartNew() → static method call + // System.Environment.MachineName → static property get + // new System.Text.StringBuilder(64) → constructor + // sw.Stop() → instance method + // sw.Elapsed → instance property + function _makeNamespaceProxy(path) { + var cached = _nsProxyCache[path]; + if (cached) return cached; + // Static method functions and child namespace/type proxies by name, made on first access. + var members = Object.create(null); + // Handlers assigned to static events, by event name. + var assigned = null; + // Plain JS fields assigned to the namespace or type, made on first assignment. + var fields = null; + function _node() {} + var proxy = new Proxy(_node, { + get: function (_, prop) { + if (prop === Symbol.hasInstance) return function (value) { return _hasInstance(path, value); }; + if (typeof prop === 'symbol') return undefined; + // Prevent string-coercion methods from descending into sub-proxies. + // V8's console.log / JSON.stringify call toString/valueOf on unknown objects; + // returning a sub-proxy causes the apply trap to fire with a non-existent + // type name, producing a spurious "Type not found" bridge error. + if (prop === 'toString' || prop === 'valueOf') + return function() { return '[.NET ' + path + ']'; }; + var extras = _namespaceExtras[path]; + if (extras && Object.prototype.hasOwnProperty.call(extras, prop)) return extras[prop]; + if (fields !== null && prop in fields) return fields[prop]; + var member = members[prop]; + if (member !== undefined) return member; + if (prop === '__dotnetType__') return path; + if (prop === 'prototype') return _typePrototype(path); + // Not a WinRT class: TypeScript's __extends must take the standard path. + if (prop === '__nsWinRTClass__' || prop === 'then') return undefined; + var assembly = _resolveAssembly(path); + var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; + // Write-only static property: reading it is an error. + if (kind & _K_SWO) + throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); + // Readable static property: resolve value immediately. + if (kind & _K_SPROP) + return _wrap(_invoke({ assembly: assembly, typeName: path, method: _getterName(prop), args: [] })); + if (kind & _K_SEVENT) return (assigned && assigned[prop]) || null; + // Static method: a function, made once. + if (kind & _K_SMETHOD) { + return members[prop] = _withTypeArgs(function () { + return _wrap(_invokeWith(-1, path, _resolveAssembly(path), prop, arguments)); + }, function (suffix, args) { + return _wrap(_invokeWith(-1, path, _resolveAssembly(path), prop + suffix, args)); + }); + } + var ev = _eventOf(prop, _kindsOf(_getTypeInfo(assembly, path)), _K_SEVENT); + if (ev !== null) return members[prop] = _eventAccessor(prop.charCodeAt(0) === 97, -1, path, ev); + // `List.of(System.String)`: the closed generic type. + if (prop === 'of') return function () { + var args = _typeArgList(arguments); + return _makeNamespaceProxy(path + '`' + arguments.length + '[' + args + ']'); + }; + if (prop === 'extend') return function (nameOrMembers, maybeMembers) { return _extendClass(this, nameOrMembers, maybeMembers); }; + if (prop === 'call') return function (thisArg) { return _es5SuperCall(path, thisArg, Array.prototype.slice.call(arguments, 1)); }; + if (prop === 'apply') return function (thisArg, args) { return _es5SuperCall(path, thisArg, args ? Array.prototype.slice.call(args) : []); }; + // Namespace / sub-type: keep descending. + return members[prop] = _makeNamespaceProxy(path + '.' + prop); + }, + set: function (_, prop, value) { + if (typeof prop === 'symbol') return true; + var assembly = _resolveAssembly(path); + var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; + // Read-only static property: assignment is an error. + if (kind & _K_SRO) + throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); + // Writable (read-write or write-only): invoke the setter. + if (kind & (_K_SPROP | _K_SWO)) { + _invoke({ assembly: assembly, typeName: path, method: _setterName(prop), args: [_unwrap(value)] }); + return true; + } + if (kind & _K_SEVENT) { + _assignEvent(-1, path, prop, value); + (assigned || (assigned = Object.create(null)))[prop] = value || null; + return true; + } + // Not a native property: a plain JS field. + (fields || (fields = Object.create(null)))[prop] = value; + return true; + }, + apply: function (_, _this, args) { + var lastDot = path.lastIndexOf('.'); + // No dot means this is a top-level name being called directly: + // there is no type+method pair to dispatch, so bail out. + if (lastDot <= 0) return undefined; + var typeName = path.substring(0, lastDot); + var method = path.substring(lastDot + 1); + var assembly = _resolveAssembly(typeName); + return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: args.map(_unwrap) })); + }, + construct: function (_, args, newTarget) { + // `super(...)` from a JS subclass. + if (newTarget && newTarget !== proxy) return _constructSubclass(path, args, newTarget.prototype); + if (args.length === 1 && args[0] && typeof args[0] === 'object' && typeof args[0].__handle !== 'number' + && _typeKind(path) === 'interface') + return _implementInterface(path, args[0]); + return _wrap(_invokeWith(-1, path, _resolveAssembly(path), '.ctor', args)); + }, + }); + _nsProxyCache[path] = proxy; + return proxy; + } + // `System` stays the .NET namespace when app code assigns it: @nativescript/core sets + // `global.System = { import() }` (a SystemJS shim). Assigned members are kept and take + // precedence (`System.import(...)` keeps working); everything else is .NET. + (function () { + var systemProxy = _makeNamespaceProxy('System'); + Object.defineProperty(globalThis, 'System', { + configurable: true, + enumerable: true, + get: function () { return systemProxy; }, + set: function (value) { + if (value && (typeof value === 'object' || typeof value === 'function')) + Object.keys(value).forEach(function (k) { _namespaceExtras.System[k] = value[k]; }); + }, + }); + })(); + globalThis.Microsoft = _makeNamespaceProxy('Microsoft'); + globalThis.NativeScript = _makeNamespaceProxy('NativeScript'); + + // Every other root namespace of the app's assemblies (C# sources compiled into the app, + // plugin and NuGet assemblies) is a lazy global too, so `new MyCompany.Native.Foo()` works + // with no registration step, the way Java packages and Objective-C classes are globals on + // Android and iOS. WinRT owns Windows/Microsoft/NativeScript; the rest are implementation + // namespaces of the toolchain that no app reaches for. + (function () { + var reserved = { Windows: 1, Microsoft: 1, NativeScript: 1, System: 1, NativeScriptBridge: 1, NSWinRTDynamicProxies: 1, + WinRT: 1, ABI: 1, XamlGeneratedNamespace: 1, FxResources: 1, Internal: 1, Interop: 1, MS: 1, Mono: 1 }; + for (var ns in _namespaceAssemblyMap) { + if (ns.indexOf('.') !== -1 || reserved[ns] === 1 || !/^[A-Za-z_$][\w$]*$/.test(ns)) continue; + globalThis.NSWinRT.dotnet.registerNamespace(ns); + } + })(); + + // Creates a typed .NET BCL delegate (not a WinRT COM delegate). + // Use this when the API expects a managed System.Action, + // System.EventHandler, etc. rather than a WinRT delegate interface. + // For WinRT delegate types use NSWinRT.asDelegate instead. + if (typeof globalThis.__nsDotNetAwaitTask === 'function') { + globalThis.NSWinRT.dotnet.taskToPromise = function (obj) { + var h = obj && typeof obj.__handle === 'number' && obj.__isTask === true ? obj.__handle : -1; + if (h < 0) return Promise.resolve(obj); + return new Promise(function (resolve, reject) { + globalThis.__nsDotNetAwaitTask(h, function (value) { resolve(_wrap(value)); }, function (error) { reject(_errorFromException(error)); }); + }); + }; + } + + if (typeof globalThis.__nsDotNetCreateDelegate === 'function') { + globalThis.NSWinRT.dotnet.asDelegate = function(typeNameOrFn, fn) { + var typeName, callback; + if (typeof typeNameOrFn === 'function') { + typeName = ''; + callback = typeNameOrFn; + } else { + typeName = typeNameOrFn || ''; + callback = fn; + } + if (typeof callback !== 'function') + throw new TypeError('NSWinRT.dotnet.asDelegate: callback must be a function'); + var wrapped = function() { + var args = Array.prototype.slice.call(arguments).map(_wrap); + return callback.apply(null, args); + }; + return globalThis.__nsDotNetCreateDelegate(typeName, wrapped); + }; + } + + // Uncaught .NET exceptions go to __onUncaughtError; faulted Tasks nobody awaited go to + // __onDiscardedError. + try { + _bridgeCall('RegisterUnhandledExceptionHandler', [_callbackFor(function (kind, ex) { + var err = _errorFromException(ex); + var hook = kind === 'unobservedTask' ? globalThis.__onDiscardedError : globalThis.__onUncaughtError; + if (typeof hook === 'function') hook(err); + else if (typeof console !== 'undefined') console.error(err); + })]); + } catch (_) {} + + if (typeof globalThis.__nsRunOnUIThread === 'function') { + globalThis.NSWinRT.runOnUIThread = function(fn) { + if (typeof fn !== 'function') throw new TypeError('NSWinRT.runOnUIThread: expected a function'); + return globalThis.__nsRunOnUIThread(fn); + }; + } +})(); diff --git a/runtime/src/js/dotnet_subclass.js b/runtime/src/js/dotnet_subclass.js new file mode 100644 index 0000000..1ef5b2d --- /dev/null +++ b/runtime/src/js/dotnet_subclass.js @@ -0,0 +1,855 @@ +// JS subclasses of .NET and WinRT classes (extend, @NativeClass, NSWinRT.proxy). +// Shared by both engines: global_fns.rs HELPER_SOURCE (classic) and napi_engine/dotnet.rs. +// Runs inside the caller's function scope, so its vars stay private. + +var proxyExtensions = []; +var proxyInstances = new Map(); +var nextProxyId = 1; + +function ctorName(ctor) { + return (ctor && (ctor.__winrtProxyName__ || ctor.__typeName__ || ctor.name)) || 'Object'; +} + +// This IIFE runs before the NSWinRT.dotnet block below in script order, but by the +// time createManagedSubclass is actually CALLED (well after the whole bootstrap has +// finished evaluating) NSWinRT.dotnet.wrap already exists whenever +// __nsDotNetCreateJsSubclass does (both are registered together). Wraps a raw tagged +// handle value into a live DotNet instance proxy exactly like the dotnet block's own +// private `_wrap`, without needing access to that other IIFE's closure. +function _wrapDotNetHandle(value) { + var dotnet = globalThis.NSWinRT && globalThis.NSWinRT.dotnet; + return (dotnet && typeof dotnet.wrap === 'function') ? dotnet.wrap(value) : value; +} + +var typeDescriptorCache = Object.create(null); + +function describeWinRTType(typeName) { + if (!typeName || typeof globalThis.__nsDescribeWinRTType !== 'function') { + return null; + } + if (Object.prototype.hasOwnProperty.call(typeDescriptorCache, typeName)) { + return typeDescriptorCache[typeName]; + } + try { + var raw = globalThis.__nsDescribeWinRTType(typeName); + typeDescriptorCache[typeName] = raw ? JSON.parse(raw) : null; + } catch (_) { + typeDescriptorCache[typeName] = null; + } + return typeDescriptorCache[typeName]; +} + +function buildFallbackParameterMetadata(fn) { + var params = []; + var count = typeof fn === 'function' && Number.isFinite(fn.length) ? fn.length : 0; + for (var i = 0; i < count; i++) { + params.push({ name: 'arg' + i, type: 'Object' }); + } + return params; +} + +function normalizeMethodMetadata(name, value, descriptors) { + for (var i = 0; i < descriptors.length; i++) { + var descriptor = descriptors[i]; + if (!descriptor || !Array.isArray(descriptor.methods)) { + continue; + } + for (var j = 0; j < descriptor.methods.length; j++) { + var method = descriptor.methods[j]; + if (method && method.name === name) { + return { + name: method.name, + returnType: method.returnType || method.return_type || 'Void', + parameters: Array.isArray(method.parameters) ? method.parameters : [], + }; + } + } + } + + return { + name: name, + returnType: name === 'init' ? 'Void' : 'Object', + parameters: buildFallbackParameterMetadata(value), + }; +} + +function normalizePropertyMetadata(name, descriptors) { + for (var i = 0; i < descriptors.length; i++) { + var descriptor = descriptors[i]; + if (!descriptor || !Array.isArray(descriptor.properties)) { + continue; + } + for (var j = 0; j < descriptor.properties.length; j++) { + var property = descriptor.properties[j]; + if (property && property.name === name) { + return { + name: property.name, + propType: property.propType || property.prop_type || 'Object', + readable: property.readable !== false, + writable: property.writable !== false, + }; + } + } + } + + return { + name: name, + propType: 'Object', + readable: true, + writable: true, + }; +} + +function collectProxyMethods(overrides, descriptors) { + var methods = []; + for (var key in overrides) { + if (!Object.prototype.hasOwnProperty.call(overrides, key)) { + continue; + } + if (key === 'interfaces') { + continue; + } + if (typeof overrides[key] === 'function') { + methods.push(normalizeMethodMetadata(key, overrides[key], descriptors)); + } + } + methods.sort(function (left, right) { + return String(left && left.name || '').localeCompare(String(right && right.name || '')); + }); + return methods; +} + +function collectProxyProperties(overrides, descriptors) { + var props = []; + for (var key in overrides) { + if (!Object.prototype.hasOwnProperty.call(overrides, key)) { + continue; + } + if (key === 'interfaces') { + continue; + } + if (typeof overrides[key] !== 'function') { + props.push(normalizePropertyMetadata(key, descriptors)); + } + } + props.sort(function (left, right) { + return String(left && left.name || '').localeCompare(String(right && right.name || '')); + }); + return props; +} + +function safeIdentifier(name) { + return String(name || '') + .replace(/[^A-Za-z0-9_]/g, '_') + .replace(/^([^A-Za-z_])/, '_$1') || 'ProxyType'; +} + +function autoProxyTypeName(baseCtor) { + var baseType = ctorName(baseCtor) || 'Object'; + var baseShort = safeIdentifier(baseType.split('.').pop()); + return 'NativeScript.Gen.' + baseShort + '_' + (proxyExtensions.length + 1); +} + +function renderProxyCSharp(meta) { + var typeName = meta.typeName || ('GeneratedProxy' + (proxyExtensions.length + 1)); + var safeTypeName = safeIdentifier(typeName.split('.').pop()); + var baseType = meta.baseType || 'object'; + var methodStubs = ''; + for (var i = 0; i < meta.methods.length; i++) { + var methodMeta = meta.methods[i]; + var methodName = safeIdentifier((methodMeta && methodMeta.name) || methodMeta); + methodStubs += + ' public object __ns_' + methodName + '(params object[] args)\\n' + + ' {\\n' + + ' return ProxyDispatcher.Invoke(this.__proxyId, "' + methodName + '", args);\\n' + + ' }\\n\\n'; + } + + return ( + 'using System;\\n\\n' + + 'namespace NativeScriptGeneratedProxies\\n' + + '{\\n' + + ' public static class ProxyDispatcher\\n' + + ' {\\n' + + ' public static Func JsInvoke;\\n' + + ' public static object Invoke(int id, string method, object[] args)\\n' + + ' {\\n' + + ' var cb = JsInvoke;\\n' + + ' if (cb == null) throw new InvalidOperationException("JsInvoke callback is not registered.");\\n' + + ' return cb(id, method, args);\\n' + + ' }\\n' + + ' }\\n\\n' + + ' public class ' + safeTypeName + ' : ' + baseType + '\\n' + + ' {\\n' + + ' private readonly int __proxyId;\\n\\n' + + ' public ' + safeTypeName + '(int proxyId)\\n' + + ' {\\n' + + ' this.__proxyId = proxyId;\\n' + + ' }\\n\\n' + + methodStubs + + ' }\\n' + + '}\\n' + ); +} + +function renderProxyCsproj(meta) { + var asmName = safeIdentifier((meta.typeName || 'GeneratedProxy').split('.').pop()); + return ( + '\\n' + + ' \\n' + + ' net8.0-windows10.0.19041.0\\n' + + ' ' + asmName + '\\n' + + ' NativeScriptGeneratedProxies\\n' + + ' enable\\n' + + ' disable\\n' + + ' latest\\n' + + ' \\n' + + '\\n' + ); +} + +function buildProxyMetadata(baseCtor, typeName, overrides, Extended) { + var baseType = ctorName(baseCtor); + var interfaceNames = Array.isArray(overrides.interfaces) + ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) + : []; + var descriptors = [describeWinRTType(baseType)]; + for (var i = 0; i < interfaceNames.length; i++) { + descriptors.push(describeWinRTType(interfaceNames[i])); + } + var namespace = ''; + var className = typeName || ''; + if (typeName) { + var splitIndex = typeName.lastIndexOf('.'); + if (splitIndex >= 0) { + namespace = typeName.slice(0, splitIndex); + className = typeName.slice(splitIndex + 1); + } + } + var meta = { + kind: 'windows-proxy', + typeName: typeName || '', + className: className || safeIdentifier((typeName || baseType || 'GeneratedProxy').split('.').pop()), + namespace: namespace || null, + baseType: baseType, + baseClass: baseType, + interfaces: interfaceNames, + methods: collectProxyMethods(overrides, descriptors.filter(Boolean)), + properties: collectProxyProperties(overrides, descriptors.filter(Boolean)), + isAutoGeneratedName: !typeName, + registeredAt: new Date().toISOString(), + registered: false, + generated: null, + }; + try { + Object.defineProperty(Extended, '__proxyMetadata__', { + value: meta, + writable: true, + configurable: true, + enumerable: false, + }); + } catch (_) { + Extended.__proxyMetadata__ = meta; + } + proxyExtensions.push(meta); + if (typeof globalThis.__nsProxyAutoCapture === 'function') { + try { + globalThis.__nsProxyAutoCapture(JSON.stringify(proxyExtensions)); + } catch (_) { + // Capture is best-effort; runtime behavior should remain unaffected. + } + } + + if (meta.interfaces.length > 0) { + if (typeof globalThis.__nsProxyCompileProject === 'function') { + try { registerProxy(meta); } catch (_) { } + } else if (typeof globalThis.__nsProxyWriteTextFile === 'function') { + try { emitProxy(meta); } catch (_) { } + } + } + return meta; +} + +function ensureProxyInstance(instance, overrides, ctor) { + if (!instance || typeof instance !== 'object') { + return -1; + } + + var proxyId = instance.__proxyId; + if (typeof proxyId !== 'number' || !Number.isFinite(proxyId)) { + proxyId = nextProxyId++; + try { + Object.defineProperty(instance, '__proxyId', { + value: proxyId, + writable: false, + configurable: true, + enumerable: false, + }); + } catch (_) { + instance.__proxyId = proxyId; + } + } + + proxyInstances.set(proxyId, { + instance: instance, + overrides: overrides, + constructor: ctor, + }); + + return proxyId; +} + +function makeExtendedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { + var hasName = typeof nameOrOverrides === 'string'; + var explicitTypeName = hasName ? nameOrOverrides : ''; + var typeName = explicitTypeName || autoProxyTypeName(baseCtor); + var overrides = hasName ? maybeOverrides : nameOrOverrides; + if (!overrides || typeof overrides !== 'object') { + overrides = {}; + } + + function Extended() { + var instance; + var args = Array.prototype.slice.call(arguments); + try { + instance = Reflect.construct(baseCtor, args); + } catch (_) { + instance = {}; + } + + for (var key in overrides) { + if (key === 'interfaces') { + continue; + } + var value = overrides[key]; + if (typeof value === 'function') { + try { + Object.defineProperty(instance, key, { + value: value, + writable: true, + configurable: true, + enumerable: true, + }); + } catch (_) { + instance[key] = value; + } + } else { + instance[key] = value; + } + } + + if (typeof overrides.init === 'function') { + try { + var initResult = overrides.init.apply(instance, args); + if (initResult && typeof initResult === 'object') { + instance = initResult; + } + } catch (_) { + // Keep constructor resilient; init errors should not crash runtime. + } + } + + if (Array.isArray(overrides.interfaces)) { + try { + Object.defineProperty(instance, '__interfaces__', { + value: overrides.interfaces.slice(), + writable: false, + configurable: true, + enumerable: false, + }); + } catch (_) { + instance.__interfaces__ = overrides.interfaces.slice(); + } + } + + ensureProxyInstance(instance, overrides, Extended); + + return instance; + } + + Extended.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); + Extended.prototype.constructor = Extended; + + for (var protoKey in overrides) { + if (protoKey === 'interfaces') { + continue; + } + Extended.prototype[protoKey] = overrides[protoKey]; + } + + if (typeName) { + try { + Object.defineProperty(Extended, 'name', { + value: typeName, + configurable: true, + }); + } catch (_) { + // Non-critical metadata assignment. + } + } + + Extended.__typeName__ = typeName || ctorName(baseCtor); + Extended.__nsWinRTClass__ = true; + var metadata = buildProxyMetadata(baseCtor, typeName, overrides, Extended); + + Extended.extend = function (nextNameOrOverrides, nextMaybeOverrides) { + return makeExtendedConstructor(Extended, nextNameOrOverrides, nextMaybeOverrides); + }; + + try { + Object.defineProperty(Extended, 'emitProxy', { + value: function (outDir) { + return NSWinRT.proxy.emit(metadata, outDir); + }, + writable: true, + configurable: true, + enumerable: false, + }); + } catch (_) { + Extended.emitProxy = function (outDir) { + return NSWinRT.proxy.emit(metadata, outDir); + }; + } + + return Extended; +} + +if (typeof Function.prototype.extend !== 'function') { + Object.defineProperty(Function.prototype, 'extend', { + value: function (nameOrOverrides, maybeOverrides) { + // A WinRT class (or a JS class deriving from one): an ES2015 subclass, so + // instances get the composed-object construction of `class X extends Y`. + if (globalThis.NSWinRT && typeof globalThis.NSWinRT.extendClass === 'function') { + if (typeof this.__typeName__ === 'string') + return globalThis.NSWinRT.extendClass(this, nameOrOverrides, maybeOverrides); + // Object.extend({ interfaces: [...], ... }): a subclass of System.Object. + if (this === Object && globalThis.System) + return globalThis.NSWinRT.extendClass(globalThis.System.Object, nameOrOverrides, maybeOverrides); + } + return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +if (typeof Object.extend !== 'function') { + Object.defineProperty(Object, 'extend', { + value: function (nameOrOverrides, maybeOverrides) { + return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +// Create a managed-backed subclass factory. This returns a constructor +// that, when invoked, asks the managed bridge to instantiate a real +// C# subclass whose vtable forwards virtual calls back into the +// provided JS `overrides` object via the DotNet bridge. +function makeManagedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { + var hasName = typeof nameOrOverrides === 'string'; + var explicitTypeName = hasName ? nameOrOverrides : ''; + var typeName = explicitTypeName || autoProxyTypeName(baseCtor); + var overrides = hasName ? maybeOverrides : nameOrOverrides; + if (!overrides || typeof overrides !== 'object') overrides = {}; + + function Managed() { + var args = Array.prototype.slice.call(arguments); + // Pass the actual base ctor type name as the assembly param so the + // bridge can fall back to it when no static proxy exists for typeName. + var baseTypeName = ctorName(baseCtor) || ''; + var obj = NSWinRT.proxy.createManagedSubclass(baseTypeName, typeName || '', overrides); + if (typeof overrides.init === 'function') { + try { + var initResult = overrides.init.apply(obj, args); + if (initResult && typeof initResult === 'object') obj = initResult; + } catch (_) { } + } + return obj; + } + + Managed.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); + Managed.prototype.constructor = Managed; + Managed.extend = function (nextNameOrOverrides, nextMaybeOverrides) { + return makeManagedConstructor(Managed, nextNameOrOverrides, nextMaybeOverrides); + }; + try { + Object.defineProperty(Managed, 'name', { value: typeName || ctorName(baseCtor), configurable: true }); + } catch (_) { } + Managed.__typeName__ = typeName || ctorName(baseCtor); + Managed.__nsWinRTClass__ = true; + var meta = buildProxyMetadata(baseCtor, typeName, overrides, Managed); + try { + Object.defineProperty(Managed, 'emitProxy', { + value: function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }, + writable: true, + configurable: true, + enumerable: false, + }); + } catch (_) { + Managed.emitProxy = function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }; + } + return Managed; +} + +if (typeof Function.prototype.extendManaged !== 'function') { + Object.defineProperty(Function.prototype, 'extendManaged', { + value: function (nameOrOverrides, maybeOverrides) { + return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +if (typeof Object.extendManaged !== 'function') { + Object.defineProperty(Object, 'extendManaged', { + value: function (nameOrOverrides, maybeOverrides) { + return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +// Convenience alias for hosts that expect a global BaseClass helper. +globalThis.BaseClass = globalThis.BaseClass || {}; +if (typeof globalThis.BaseClass.extend !== 'function') { + globalThis.BaseClass.extend = function (baseCtor, nameOrOverrides, maybeOverrides) { + return makeManagedConstructor(baseCtor || Object, nameOrOverrides, maybeOverrides); + }; +} + +// Registry mapping each WinRT-extended prototype to its lazy factory. +// Keyed by the new Child.prototype created in __extends_winrt so that +// the Parent.apply override can look up the right factory per subclass. +var __nsExtendRegistry__ = new WeakMap(); + +function __extends_winrt(Child, Parent) { + var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; + extendStatics(Child, Parent); + + // Lazy factory: deferred until first `new Child()` so that TypeScript + // has had a chance to populate Child.prototype with override methods. + var _extended = null; + var getExtended = function () { + if (!_extended) { + var overrides = {}; + var interfaces = []; + var proto = Child.prototype; + if (proto) { + Object.getOwnPropertyNames(proto).forEach(function (key) { + if (key === 'constructor') return; + if (key === 'interfaces') { + var iv = proto[key]; + if (Array.isArray(iv)) interfaces = iv; + return; + } + overrides[key] = proto[key]; + }); + } + if (interfaces.length) overrides.interfaces = interfaces; + var name = Child.__winrtProxyName__ || Child.name || ''; + var hasBridge = typeof globalThis.__nsDotNetCreateJsSubclass === 'function'; + if (Child.__winrtProxyName__ || (interfaces.length > 0 && hasBridge)) { + _extended = makeManagedConstructor(Parent, name, overrides); + } else { + // No bridge or no interfaces: pure JS proxy via Reflect.construct. + _extended = makeExtendedConstructor(Parent, name, overrides); + } + } + return _extended; + }; + + var newProto = Object.create((Parent && Parent.prototype) || Object.prototype); + newProto.constructor = Child; + Child.prototype = newProto; + __nsExtendRegistry__.set(newProto, getExtended); + + // Patch Parent.apply/call once per parent class. The WeakMap lookup + // inside the override picks the correct factory per subclass so that + // multiple classes extending the same parent coexist correctly. + if (!Parent.__nsApplyPatched__) { + Parent.__nsApplyPatched__ = true; + + Parent.apply = function (thiz, args) { + if (thiz && thiz.__nsContainer__) return; + var factory = __nsExtendRegistry__.get(Object.getPrototypeOf(thiz)); + if (!factory) return; + thiz.__nsContainer__ = true; + try { + var Extended = factory(); + var argArr = Array.isArray(args) ? args : (args ? Array.prototype.slice.call(args) : []); + if (argArr.length > 0) { + thiz.__proto__ = new (Function.prototype.bind.apply(Extended, [null].concat(argArr)))(); + } else { + thiz.__proto__ = new Extended(); + } + return thiz.__proto__; + } finally { + delete thiz.__nsContainer__; + } + }; + + Parent.call = function (thiz) { + return Parent.apply(thiz, Array.prototype.slice.call(arguments, 1)); + }; + } +} + +// Global __extends that TypeScript compiled output resolves via +// `var __extends = (this && this.__extends) || ...`. +// Intercepts WinRT parent classes; falls back to standard TS behaviour. +if (typeof globalThis.__extends !== 'function') { + var __extends_ts_inner = function (d, b) { + if (b !== null && typeof b !== 'function') { + throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); + } + var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; + + globalThis.__extends = function (d, b) { + if (b && b.__nsWinRTClass__) { + __extends_winrt(d, b); + } else { + __extends_ts_inner(d, b); + } + }; +} + +// TypeScript decorator helpers, as the iOS/Android runtimes provide them: compiled +// (`experimentalDecorators`) code calls a bare `__decorate(...)`. Core's globals module +// binds tslib's copies too, but an ES-module bundle can evaluate decorated core modules +// before it runs. +if (typeof globalThis.__decorate !== 'function') { + globalThis.__decorate = function (decorators, target, key, desc) { + var c = arguments.length; + var r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; + if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') { + r = Reflect.decorate(decorators, target, key, desc); + } else { + for (var i = decorators.length - 1; i >= 0; i--) { + if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; + } + } + return c > 3 && r && Object.defineProperty(target, key, r), r; + }; +} +if (typeof globalThis.__param !== 'function') { + globalThis.__param = function (paramIndex, decorator) { + return function (target, key) { decorator(target, key, paramIndex); }; + }; +} +if (typeof globalThis.__metadata !== 'function') { + globalThis.__metadata = function (metadataKey, metadataValue) { + if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') { + return Reflect.metadata(metadataKey, metadataValue); + } + }; +} + +// `return global.__native(this)` ends TypeScript subclass constructors written for +// Android/iOS; here `this` already is the native-backed object. +if (typeof globalThis.__native !== 'function') { + globalThis.__native = function (obj) { return obj; }; +} + +// @NativeClass / @NativeClass(): marks a class that extends a native (WinRT/.NET) class. +// Extending works without it; it exists so code written for iOS/Android, where the build +// transforms decorated classes, runs unchanged. +if (typeof globalThis.NativeClass !== 'function') { + Object.defineProperty(globalThis, 'NativeClass', { + value: function NativeClass(target) { + if (typeof target === 'function') return target; + return function (cls) { return cls; }; + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +// @Interfaces([IFoo, IBar]): attach WinRT interface list to a class. +if (typeof globalThis.Interfaces !== 'function') { + Object.defineProperty(globalThis, 'Interfaces', { + value: function Interfaces(interfacesList) { + return function (target) { + target.prototype.interfaces = Array.isArray(interfacesList) ? interfacesList.slice() : []; + return target; + }; + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + + +if (typeof globalThis.CSharpProxy !== 'function') { + Object.defineProperty(globalThis, 'CSharpProxy', { + value: function CSharpProxy(name) { + return function (target) { + target.__winrtProxyName__ = typeof name === 'string' ? name : ''; + return target; + }; + }, + writable: true, + configurable: true, + enumerable: false, + }); +} + +function defaultProxyOutDir(meta) { + var typeName = (meta && meta.typeName) ? meta.typeName : 'GeneratedProxy'; + var safe = safeIdentifier(typeName.split('.').pop()); + return './generated-proxies/' + safe; +} + +function emitProxy(meta, outDir) { + if (!meta || typeof meta !== 'object') { + throw new Error('NSWinRT.proxy.emit(meta[, outDir]) expects a proxy metadata object'); + } + if (typeof globalThis.__nsProxyWriteTextFile !== 'function') { + throw new Error('Host proxy file emitter is not available'); + } + + var dir = outDir || defaultProxyOutDir(meta); + var csprojPath = dir + '/Proxy.csproj'; + var csPath = dir + '/Proxy.g.cs'; + var csproj = renderProxyCsproj(meta); + var source = renderProxyCSharp(meta); + + globalThis.__nsProxyWriteTextFile(csprojPath, csproj); + globalThis.__nsProxyWriteTextFile(csPath, source); + + meta.generated = { + dir: dir, + csprojPath: csprojPath, + csPath: csPath, + }; + + return meta.generated; +} + +function compileProxy(meta, outDir, configuration) { + if (typeof globalThis.__nsProxyCompileProject !== 'function') { + throw new Error('Host proxy compiler is not available'); + } + var generated = emitProxy(meta, outDir); + var result = globalThis.__nsProxyCompileProject(generated.csprojPath, configuration || 'Debug'); + generated.build = result; + return generated; +} + +function registerProxy(meta, outDir, configuration) { + var generated = compileProxy(meta, outDir, configuration); + var manifest = { + kind: 'windows-proxy', + typeName: meta.typeName, + baseType: meta.baseType, + interfaces: meta.interfaces, + methods: meta.methods, + properties: meta.properties, + generated: generated, + registration: { + hostCanLoadAssemblies: false, + note: 'Assembly build succeeded, but runtime CLR proxy activation is not wired yet. Dynamic JS fallback remains active.', + }, + }; + if (typeof globalThis.__nsProxyRegisterManifest === 'function') { + globalThis.__nsProxyRegisterManifest(JSON.stringify(manifest)); + } + meta.registered = true; + meta.registration = manifest.registration; + return manifest; +} + +function invokeProxyById(proxyId, methodName, argsArray) { + var entry = proxyInstances.get(proxyId); + if (!entry) { + throw new Error('Proxy instance not found for id ' + proxyId); + } + + var target = entry.instance; + var method = target && target[methodName]; + if (typeof method !== 'function') { + throw new Error('Proxy method "' + methodName + '" is not defined on proxy id ' + proxyId); + } + + return method.apply(target, Array.isArray(argsArray) ? argsArray : []); +} + +globalThis.__nsInvokeProxyJs = invokeProxyById; + +globalThis.NSWinRT.proxy = { + getExtensions: function () { + return proxyExtensions.slice(); + }, + emit: emitProxy, + compile: compileProxy, + register: registerProxy, + createManagedSubclass: function (assembly, typeName, overrides) { + if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') { + throw new Error('__nsDotNetCreateJsSubclass is not available in this runtime'); + } + if (!overrides || typeof overrides !== 'object') overrides = {}; + + var interfaceNames = Array.isArray(overrides.interfaces) + ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) + : []; + + // Literal CLR member names this JS object actually overrides. The bridge only + // emits an IL override for base virtuals named here (everything else falls + // through to the real base implementation) and implements exactly these + // interface members from JS (others fall back to default/no-op). Plain + // functions are method overrides; accessor descriptors become get_/set_- + // prefixed property overrides, matching the literal CLR accessor names the + // dynamic proxy already dispatches by. Read via getOwnPropertyDescriptor + // throughout so collecting names never invokes a JS getter as a side effect. + var memberNames = []; + for (var mkey in overrides) { + if (!Object.prototype.hasOwnProperty.call(overrides, mkey)) continue; + if (mkey === 'interfaces' || mkey === 'init') continue; + var mdesc = Object.getOwnPropertyDescriptor(overrides, mkey); + if (!mdesc) continue; + if (typeof mdesc.get === 'function' || typeof mdesc.set === 'function') { + if (typeof mdesc.get === 'function') memberNames.push('get_' + mkey); + if (typeof mdesc.set === 'function') memberNames.push('set_' + mkey); + } else if (typeof mdesc.value === 'function') { + memberNames.push(mkey); + } + } + + var dispatcher = function () { + var a = Array.prototype.slice.call(arguments); + try { + var target = _wrapDotNetHandle(a[0]); + var method = a[1]; + var margs = a.slice(2).map(_wrapDotNetHandle); + if (target && typeof method === 'string') { + var fn = overrides[method]; + if (typeof fn === 'function') return fn.apply(target, Array.isArray(margs) ? margs : []); + } + } catch (_) { } + }; + var handle = globalThis.__nsDotNetCreateJsSubclass(assembly || '', typeName || '', interfaceNames, memberNames, dispatcher); + var obj = _wrapDotNetHandle(handle); + ensureProxyInstance(obj, overrides, function () { }); + return obj; + }, + invokeById: invokeProxyById, + listRegisteredManifests: function () { + if (typeof globalThis.__nsProxyListManifests === 'function') { + return globalThis.__nsProxyListManifests(); + } + return []; + }, +}; diff --git a/runtime/src/napi_engine/dotnet.rs b/runtime/src/napi_engine/dotnet.rs index d213a58..2542ad7 100644 --- a/runtime/src/napi_engine/dotnet.rs +++ b/runtime/src/napi_engine/dotnet.rs @@ -5,9 +5,8 @@ //! `v8::` references and is reused completely unmodified here: see //! `[[project_engine_framework_parity]]` for how that was confirmed. What this module supplies is //! the engine-specific half: the same seven `__ns*` natives the classic engine registers in -//! `global_fns.rs`, reimplemented against Node-API instead of V8's C++ API, plus the JS half -//! mirrored verbatim (native names match exactly, so the JS needed no changes) into -//! [`DOTNET_HELPERS_JS`]. Keep the two JS copies in sync. +//! `global_fns.rs`, reimplemented against Node-API instead of V8's C++ API, plus the JS half shared with +//! the classic engine through `runtime/src/js` ([`DOTNET_HELPERS_JS`]). //! //! Includes `__nsDotNetCreateJsSubclass` and `__nsWrapWinRTPointer`, which back JS subclasses of //! .NET and WinRT classes (`class X extends Y`, `Y.extend(...)`, `new Interface({...})`; see the @@ -162,10 +161,171 @@ fn napi_json_parse(env: &Env, text: &str) -> napi::Result { parse.call(Some(&json), &[env.create_string(text)?]) } +/// A BigInt as a .NET long (tag 0x10), or ulong (0x11) when only that fits. Read through sign and +/// words: the QuickJS shim doesn't report `lossless`. +fn napi_bin_write_bigint(env: &Env, buf: &mut Vec, arg: &JsUnknown) -> bool { + let mut sign = 0i32; + let mut count = 2usize; + let mut words = [0u64; 2]; + let status = unsafe { sys::napi_get_value_bigint_words(env.raw(), arg.raw(), &mut sign, &mut count, words.as_mut_ptr()) }; + if status != sys::Status::napi_ok { + return false; + } + let magnitude = if count == 0 { 0 } else { words[0] }; + if sign != 0 { + // Values below long.MinValue wrap, as they do in a C# unchecked cast. + buf.push(0x10); + buf.extend_from_slice(&(magnitude as i64).wrapping_neg().to_le_bytes()); + } else if magnitude <= i64::MAX as u64 { + buf.push(0x10); + buf.extend_from_slice(&(magnitude as i64).to_le_bytes()); + } else { + buf.push(0x11); + buf.extend_from_slice(&magnitude.to_le_bytes()); + } + true +} + +/// A typed array, ArrayBuffer or DataView as tag 0x12 (see `global_fns::bin_write_v8_bytes`). +fn napi_bin_write_bytes(env: &Env, buf: &mut Vec, arg: &JsUnknown) -> bool { + let (env_raw, value) = (env.raw(), unsafe { arg.raw() }); + let mut is = false; + let mut data: *mut std::ffi::c_void = std::ptr::null_mut(); + let mut len = 0usize; + let mut kind = 0u8; + unsafe { + if sys::napi_is_typedarray(env_raw, value, &mut is) == sys::Status::napi_ok && is { + let mut ty: sys::napi_typedarray_type = 0; + let mut count = 0usize; + let mut buffer: sys::napi_value = std::ptr::null_mut(); + let mut offset = 0usize; + if sys::napi_get_typedarray_info(env_raw, value, &mut ty, &mut count, &mut data, &mut buffer, &mut offset) != sys::Status::napi_ok { + return false; + } + let (k, size) = match ty { + 0 => (1, 1), + 3 => (2, 2), + 4 => (3, 2), + 5 => (4, 4), + 6 => (5, 4), + 7 => (6, 4), + 8 => (7, 8), + 9 => (8, 8), + 10 => (9, 8), + _ => (0, 1), + }; + kind = k; + len = count * size; + } else if sys::napi_is_dataview(env_raw, value, &mut is) == sys::Status::napi_ok && is { + let mut buffer: sys::napi_value = std::ptr::null_mut(); + let mut offset = 0usize; + if sys::napi_get_dataview_info(env_raw, value, &mut len, &mut data, &mut buffer, &mut offset) != sys::Status::napi_ok { + return false; + } + } else if sys::napi_is_arraybuffer(env_raw, value, &mut is) == sys::Status::napi_ok && is { + if sys::napi_get_arraybuffer_info(env_raw, value, &mut data, &mut len) != sys::Status::napi_ok { + return false; + } + } else { + return false; + } + } + buf.push(0x12); + buf.push(kind); + buf.extend_from_slice(&(len as u32).to_le_bytes()); + if data.is_null() || len == 0 { + buf.resize(buf.len() + len, 0); + } else { + buf.extend_from_slice(unsafe { std::slice::from_raw_parts(data as *const u8, len) }); + } + true +} + +/// Bytes from the bridge (tag 0x12) as a typed array of `kind` over a new ArrayBuffer. +fn napi_typed_array(env: &Env, kind: u8, bytes: &[u8]) -> Result { + let env_raw = env.raw(); + let mut data: *mut std::ffi::c_void = std::ptr::null_mut(); + let mut buffer: sys::napi_value = std::ptr::null_mut(); + unsafe { + if sys::napi_create_arraybuffer(env_raw, bytes.len(), &mut data, &mut buffer) != sys::Status::napi_ok { + return Err("could not create an ArrayBuffer".into()); + } + if !data.is_null() && !bytes.is_empty() { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), data as *mut u8, bytes.len()); + } + let (ty, size): (sys::napi_typedarray_type, usize) = match kind { + 1 => (0, 1), + 2 => (3, 2), + 3 => (4, 2), + 4 => (5, 4), + 5 => (6, 4), + 6 => (7, 4), + 7 => (8, 8), + 8 => (9, 8), + 9 => (10, 8), + _ => (1, 1), + }; + let mut typed: sys::napi_value = std::ptr::null_mut(); + if sys::napi_create_typedarray(env_raw, ty, bytes.len() / size, buffer, 0, &mut typed) != sys::Status::napi_ok { + return Ok(JsUnknown::from_raw_unchecked(env_raw, buffer)); + } + Ok(JsUnknown::from_raw_unchecked(env_raw, typed)) + } +} + +/// A length-prefixed (u16 or u32, per `prefix`) UTF-8 string at `pos`, advancing past it. +fn bin_take_str<'b>(bytes: &'b [u8], pos: &mut usize, prefix: usize) -> Option<&'b str> { + let len_bytes = bytes.get(*pos..*pos + prefix)?; + let len = if prefix == 2 { + u16::from_le_bytes(len_bytes.try_into().ok()?) as usize + } else { + u32::from_le_bytes(len_bytes.try_into().ok()?) as usize + }; + let text = std::str::from_utf8(bytes.get(*pos + prefix..*pos + prefix + len)?).ok()?; + *pos += prefix + len; + Some(text) +} + +/// Throws the Error for a bridge error (0xFF), see `global_fns::bin_error_value`, and returns the +/// message; the caller's Err leaves that Error pending. +fn napi_throw_bridge_error(env: &Env, bytes: &[u8]) -> String { + let mut pos = 1usize; + let message = bin_take_str(bytes, &mut pos, 4).unwrap_or("the .NET call failed").to_string(); + let error = (|| -> napi::Result { + let mut error: JsObject = unsafe { + let msg = env.create_string(&message)?; + let mut raw: sys::napi_value = std::ptr::null_mut(); + sys::napi_create_error(env.raw(), std::ptr::null_mut(), msg.raw(), &mut raw); + JsObject::from_raw_unchecked(env.raw(), raw) + }; + let type_name = bin_take_str(bytes, &mut pos, 2); + let stack = bin_take_str(bytes, &mut pos, 4); + let handle = bytes.get(pos..pos + 4).map(|b| i32::from_le_bytes(b.try_into().unwrap())); + if let (Some(type_name), Some(stack)) = (type_name, stack) { + error.set_named_property("dotnetType", env.create_string(type_name)?)?; + error.set_named_property("dotnetStack", env.create_string(stack)?)?; + if let Some(id) = handle.filter(|id| *id >= 0) { + let mut native = env.create_object()?; + native.set_named_property("__handle", env.create_int32(id)?)?; + native.set_named_property("__type", env.create_string(type_name)?)?; + error.set_named_property("nativeException", native)?; + } + } + Ok(error) + })(); + if let Ok(error) = error { + unsafe { sys::napi_throw(env.raw(), error.raw()) }; + } + message +} + /// Encode one outgoing method-call argument (`u16` string-length prefix; AddRefs `__native_ptr`). /// Mirrors `global_fns::bin_write_v8_arg`. fn napi_bin_write_arg(env: &Env, buf: &mut Vec, arg: &JsUnknown) { let vt = arg.get_type().unwrap_or(ValueType::Undefined); + if (vt == ValueType::BigInt && napi_bin_write_bigint(env, buf, arg)) || (vt == ValueType::Object && napi_bin_write_bytes(env, buf, arg)) { + return; + } match vt { ValueType::Null | ValueType::Undefined => buf.push(0x00), ValueType::Boolean => { @@ -250,6 +410,9 @@ fn napi_bin_write_arg(env: &Env, buf: &mut Vec, arg: &JsUnknown) { /// AddRef on `__native_ptr`). Mirrors `global_fns::bin_write_v8_value`. fn napi_bin_write_value(env: &Env, buf: &mut Vec, arg: &JsUnknown) { let vt = arg.get_type().unwrap_or(ValueType::Undefined); + if (vt == ValueType::BigInt && napi_bin_write_bigint(env, buf, arg)) || (vt == ValueType::Object && napi_bin_write_bytes(env, buf, arg)) { + return; + } match vt { ValueType::Null | ValueType::Undefined => buf.push(0x00), ValueType::Boolean => { @@ -355,6 +518,27 @@ fn napi_bin_read_value(env: &Env, bytes: &[u8], pos: &mut usize) -> Result { + let v = i64::from_le_bytes(bytes.get(*pos..*pos + 8).ok_or("i64 read")?.try_into().map_err(|_| "i64 read")?); + *pos += 8; + Ok(as_unknown(env, env.create_bigint_from_i64(v).map_err(map_err)?.into_unknown().map_err(map_err)?)) + } + + 0x11 => { + let v = u64::from_le_bytes(bytes.get(*pos..*pos + 8).ok_or("u64 read")?.try_into().map_err(|_| "u64 read")?); + *pos += 8; + Ok(as_unknown(env, env.create_bigint_from_u64(v).map_err(map_err)?.into_unknown().map_err(map_err)?)) + } + + 0x12 => { + let kind = *bytes.get(*pos).ok_or("bytes kind")?; + let len = u32::from_le_bytes(bytes.get(*pos + 1..*pos + 5).ok_or("bytes len")?.try_into().map_err(|_| "bytes len")?) as usize; + *pos += 5; + let data = bytes.get(*pos..*pos + len).ok_or("bytes")?; + *pos += len; + napi_typed_array(env, kind, data) + } + 0x0D => { let len = u32::from_le_bytes(bytes[*pos..*pos + 4].try_into().map_err(|_| "json len")?) as usize; @@ -446,6 +630,25 @@ fn napi_bin_read_value(env: &Env, bytes: &[u8], pos: &mut usize) -> Result u16::from_le_bytes(b.try_into().unwrap()) as usize, + None => 0, + }; + if extra > 0 { + *pos += 2; + } + for _ in 0..extra { + let key = bin_take_str(bytes, pos, 2).ok_or("member list key")?.to_string(); + let count = u16::from_le_bytes(bytes.get(*pos..*pos + 2).ok_or("member count")?.try_into().unwrap()) as usize; + *pos += 2; + let mut arr = env.create_array_with_length(count).map_err(map_err)?; + for i in 0..count { + let s = bin_take_str(bytes, pos, 2).ok_or("member name")?; + arr.set_element(i as u32, env.create_string(s).map_err(map_err)?).map_err(map_err)?; + } + obj.set_named_property(&key, arr).map_err(map_err)?; + } Ok(as_unknown(env, obj)) } @@ -465,6 +668,9 @@ fn napi_bin_read_response(env: &Env, bytes: &[u8]) -> Result if bytes.is_empty() { return Ok(as_unknown(env, env.get_null().map_err(|e| e.to_string())?)); } + if bytes[0] == 0xFF { + return Err(napi_throw_bridge_error(env, bytes)); + } let mut pos = 0usize; napi_bin_read_value(env, bytes, &mut pos) } @@ -536,6 +742,16 @@ fn native_invoke_bin(ctx: &CallContext) -> napi::Result { } } + // A release gives back the exports its proxy took; without a count the handle goes outright. + if op == 0x04 && ctx.length > 4 { + let count_v = ctx.get::(4)?; + let count = match count_v.get_type()? { + ValueType::Number => unsafe { count_v.cast::() }.get_double()? as i32, + _ => -1, + }; + req.extend_from_slice(&count.to_le_bytes()); + } + if op == 0x01 || op == 0x02 { crate::dotnet::bin_write_str16(&mut req, method.as_bytes()); } @@ -871,6 +1087,22 @@ unsafe fn stringify_exception(env: sys::napi_env, exc: sys::napi_value) -> Optio String::from_utf8(buf).ok() } +/// The `stack` of a thrown Error, when it has one. +unsafe fn exception_stack(env: sys::napi_env, exc: sys::napi_value) -> Option { + let mut ty: sys::napi_valuetype = 0; + if sys::napi_typeof(env, exc, &mut ty) != sys::Status::napi_ok || ty != sys::ValueType::napi_object { + return None; + } + let mut stack: sys::napi_value = std::ptr::null_mut(); + if sys::napi_get_named_property(env, exc, c"stack".as_ptr(), &mut stack) != sys::Status::napi_ok { + return None; + } + if sys::napi_typeof(env, stack, &mut ty) != sys::Status::napi_ok || ty != sys::ValueType::napi_string { + return None; + } + stringify_exception(env, stack) +} + /// `[count: u8] [tagged values...]` -> raw napi_values ready for `napi_call_function`. fn parse_callback_args(env: &Env, bytes: &[u8]) -> Vec { if bytes.is_empty() { @@ -932,7 +1164,7 @@ pub(crate) unsafe extern "C" fn napi_invoke_dotnet_js_callback( napi_invoke_dotnet_js_callback(callback_id, args as *const u8, args_len, resp as *mut *mut u8, len as *mut i32) }); if delivered.is_none() { - write_error_response("the JavaScript callback could not run", resp_ptr, resp_len); + write_error_response("", resp_ptr, resp_len); } } return; @@ -981,10 +1213,12 @@ pub(crate) unsafe extern "C" fn napi_invoke_dotnet_js_callback( { if let Some(msg) = stringify_exception(env_raw, exc) { crate::store_last_js_error(msg.clone()); - // 0x0F: the callback threw. A managed caller that expects a result gets a - // JsException; a void delegate/event ignores it. + // 0x0F: the callback threw. Message, then JS stack (a JsException in .NET). let mut buf = vec![0x0Fu8]; crate::dotnet::bin_write_str32(&mut buf, msg.as_bytes()); + if let Some(stack) = exception_stack(env_raw, exc) { + crate::dotnet::bin_write_str32(&mut buf, stack.as_bytes()); + } return buf; } } @@ -1107,1877 +1341,11 @@ pub fn install_dotnet(env: &Env) -> napi::Result<()> { Ok(()) } -/// The JS half of `NSWinRT.dotnet`, copied verbatim from the classic runtime's -/// `global_fns.rs` `HELPER_SOURCE` (the native names match exactly, so nothing needed adapting): -/// keep the two in sync. Requires the dotnet-bridge project to be published into -/// `/dotnet-bridge/publish/DotNetBridge.dll`; a no-op if `__nsDotNetInvokeBin` is absent. -const DOTNET_HELPERS_JS: &str = r#" -'use strict'; -(function () { - // Managed-subclass / proxy system, ported verbatim from the classic engine's - // `HELPER_SOURCE` (the giant bootstrap IIFE spanning `var proxyExtensions = []` through the - // `NSWinRT.proxy = {...}` object literal): see the module doc comment. Runs unconditionally, - // same as classic: `Function.prototype.extend`/`Object.extend`/pure-JS `makeExtendedConstructor` - // fallback are useful even without a .NET bridge (e.g. WinRT-only class extension). Only - // `createManagedSubclass` itself requires `__nsDotNetCreateJsSubclass`, which by the time this - // whole script runs is guaranteed to already be registered (both are installed together, see - // `install_dotnet` below): so no extra guard is needed here. - globalThis.NSWinRT = globalThis.NSWinRT || {}; - - var proxyExtensions = []; - var proxyInstances = new Map(); - var nextProxyId = 1; - - function ctorName(ctor) { - return (ctor && (ctor.__winrtProxyName__ || ctor.__typeName__ || ctor.name)) || 'Object'; - } - - // Wraps a raw tagged handle value into a live DotNet instance proxy exactly like the - // NSWinRT.dotnet block below's own private `_wrap`: reused here via the public `wrap` alias - // that block exposes on `NSWinRT.dotnet` (see below), since this code has no direct access to - // that other IIFE's closure. Both IIFEs run synchronously during the same `install_dotnet` - // call, so by the time createManagedSubclass is actually CALLED, NSWinRT.dotnet.wrap exists. - function _wrapDotNetHandle(value) { - var dotnet = globalThis.NSWinRT && globalThis.NSWinRT.dotnet; - return (dotnet && typeof dotnet.wrap === 'function') ? dotnet.wrap(value) : value; - } - - var typeDescriptorCache = Object.create(null); - - function describeWinRTType(typeName) { - if (!typeName || typeof globalThis.__nsDescribeWinRTType !== 'function') { - return null; - } - if (Object.prototype.hasOwnProperty.call(typeDescriptorCache, typeName)) { - return typeDescriptorCache[typeName]; - } - try { - var raw = globalThis.__nsDescribeWinRTType(typeName); - typeDescriptorCache[typeName] = raw ? JSON.parse(raw) : null; - } catch (_) { - typeDescriptorCache[typeName] = null; - } - return typeDescriptorCache[typeName]; - } - - function buildFallbackParameterMetadata(fn) { - var params = []; - var count = typeof fn === 'function' && Number.isFinite(fn.length) ? fn.length : 0; - for (var i = 0; i < count; i++) { - params.push({ name: 'arg' + i, type: 'Object' }); - } - return params; - } - - function normalizeMethodMetadata(name, value, descriptors) { - for (var i = 0; i < descriptors.length; i++) { - var descriptor = descriptors[i]; - if (!descriptor || !Array.isArray(descriptor.methods)) { - continue; - } - for (var j = 0; j < descriptor.methods.length; j++) { - var method = descriptor.methods[j]; - if (method && method.name === name) { - return { - name: method.name, - returnType: method.returnType || method.return_type || 'Void', - parameters: Array.isArray(method.parameters) ? method.parameters : [], - }; - } - } - } - - return { - name: name, - returnType: name === 'init' ? 'Void' : 'Object', - parameters: buildFallbackParameterMetadata(value), - }; - } - - function normalizePropertyMetadata(name, descriptors) { - for (var i = 0; i < descriptors.length; i++) { - var descriptor = descriptors[i]; - if (!descriptor || !Array.isArray(descriptor.properties)) { - continue; - } - for (var j = 0; j < descriptor.properties.length; j++) { - var property = descriptor.properties[j]; - if (property && property.name === name) { - return { - name: property.name, - propType: property.propType || property.prop_type || 'Object', - readable: property.readable !== false, - writable: property.writable !== false, - }; - } - } - } - - return { - name: name, - propType: 'Object', - readable: true, - writable: true, - }; - } - - function collectProxyMethods(overrides, descriptors) { - var methods = []; - for (var key in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, key)) { - continue; - } - if (key === 'interfaces') { - continue; - } - if (typeof overrides[key] === 'function') { - methods.push(normalizeMethodMetadata(key, overrides[key], descriptors)); - } - } - methods.sort(function (left, right) { - return String(left && left.name || '').localeCompare(String(right && right.name || '')); - }); - return methods; - } - - function collectProxyProperties(overrides, descriptors) { - var props = []; - for (var key in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, key)) { - continue; - } - if (key === 'interfaces') { - continue; - } - if (typeof overrides[key] !== 'function') { - props.push(normalizePropertyMetadata(key, descriptors)); - } - } - props.sort(function (left, right) { - return String(left && left.name || '').localeCompare(String(right && right.name || '')); - }); - return props; - } - - function safeIdentifier(name) { - return String(name || '') - .replace(/[^A-Za-z0-9_]/g, '_') - .replace(/^([^A-Za-z_])/, '_$1') || 'ProxyType'; - } - - function autoProxyTypeName(baseCtor) { - var baseType = ctorName(baseCtor) || 'Object'; - var baseShort = safeIdentifier(baseType.split('.').pop()); - return 'NativeScript.Gen.' + baseShort + '_' + (proxyExtensions.length + 1); - } - - function renderProxyCSharp(meta) { - var typeName = meta.typeName || ('GeneratedProxy' + (proxyExtensions.length + 1)); - var safeTypeName = safeIdentifier(typeName.split('.').pop()); - var baseType = meta.baseType || 'object'; - var methodStubs = ''; - for (var i = 0; i < meta.methods.length; i++) { - var methodMeta = meta.methods[i]; - var methodName = safeIdentifier((methodMeta && methodMeta.name) || methodMeta); - methodStubs += - ' public object __ns_' + methodName + '(params object[] args)\\n' + - ' {\\n' + - ' return ProxyDispatcher.Invoke(this.__proxyId, "' + methodName + '", args);\\n' + - ' }\\n\\n'; - } - - return ( - 'using System;\\n\\n' + - 'namespace NativeScriptGeneratedProxies\\n' + - '{\\n' + - ' public static class ProxyDispatcher\\n' + - ' {\\n' + - ' public static Func JsInvoke;\\n' + - ' public static object Invoke(int id, string method, object[] args)\\n' + - ' {\\n' + - ' var cb = JsInvoke;\\n' + - ' if (cb == null) throw new InvalidOperationException("JsInvoke callback is not registered.");\\n' + - ' return cb(id, method, args);\\n' + - ' }\\n' + - ' }\\n\\n' + - ' public class ' + safeTypeName + ' : ' + baseType + '\\n' + - ' {\\n' + - ' private readonly int __proxyId;\\n\\n' + - ' public ' + safeTypeName + '(int proxyId)\\n' + - ' {\\n' + - ' this.__proxyId = proxyId;\\n' + - ' }\\n\\n' + - methodStubs + - ' }\\n' + - '}\\n' - ); - } - - function renderProxyCsproj(meta) { - var asmName = safeIdentifier((meta.typeName || 'GeneratedProxy').split('.').pop()); - return ( - '\\n' + - ' \\n' + - ' net8.0-windows10.0.19041.0\\n' + - ' ' + asmName + '\\n' + - ' NativeScriptGeneratedProxies\\n' + - ' enable\\n' + - ' disable\\n' + - ' latest\\n' + - ' \\n' + - '\\n' - ); - } - - function buildProxyMetadata(baseCtor, typeName, overrides, Extended) { - var baseType = ctorName(baseCtor); - var interfaceNames = Array.isArray(overrides.interfaces) - ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) - : []; - var descriptors = [describeWinRTType(baseType)]; - for (var i = 0; i < interfaceNames.length; i++) { - descriptors.push(describeWinRTType(interfaceNames[i])); - } - var namespace = ''; - var className = typeName || ''; - if (typeName) { - var splitIndex = typeName.lastIndexOf('.'); - if (splitIndex >= 0) { - namespace = typeName.slice(0, splitIndex); - className = typeName.slice(splitIndex + 1); - } - } - var meta = { - kind: 'windows-proxy', - typeName: typeName || '', - className: className || safeIdentifier((typeName || baseType || 'GeneratedProxy').split('.').pop()), - namespace: namespace || null, - baseType: baseType, - baseClass: baseType, - interfaces: interfaceNames, - methods: collectProxyMethods(overrides, descriptors.filter(Boolean)), - properties: collectProxyProperties(overrides, descriptors.filter(Boolean)), - isAutoGeneratedName: !typeName, - registeredAt: new Date().toISOString(), - registered: false, - generated: null, - }; - try { - Object.defineProperty(Extended, '__proxyMetadata__', { - value: meta, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Extended.__proxyMetadata__ = meta; - } - proxyExtensions.push(meta); - if (typeof globalThis.__nsProxyAutoCapture === 'function') { - try { - globalThis.__nsProxyAutoCapture(JSON.stringify(proxyExtensions)); - } catch (_) { - // Capture is best-effort; runtime behavior should remain unaffected. - } - } - - if (meta.interfaces.length > 0) { - if (typeof globalThis.__nsProxyCompileProject === 'function') { - try { registerProxy(meta); } catch (_) { } - } else if (typeof globalThis.__nsProxyWriteTextFile === 'function') { - try { emitProxy(meta); } catch (_) { } - } - } - return meta; - } - - function ensureProxyInstance(instance, overrides, ctor) { - if (!instance || typeof instance !== 'object') { - return -1; - } - - var proxyId = instance.__proxyId; - if (typeof proxyId !== 'number' || !Number.isFinite(proxyId)) { - proxyId = nextProxyId++; - try { - Object.defineProperty(instance, '__proxyId', { - value: proxyId, - writable: false, - configurable: true, - enumerable: false, - }); - } catch (_) { - instance.__proxyId = proxyId; - } - } - - proxyInstances.set(proxyId, { - instance: instance, - overrides: overrides, - constructor: ctor, - }); - - return proxyId; - } - - function makeExtendedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { - var hasName = typeof nameOrOverrides === 'string'; - var explicitTypeName = hasName ? nameOrOverrides : ''; - var typeName = explicitTypeName || autoProxyTypeName(baseCtor); - var overrides = hasName ? maybeOverrides : nameOrOverrides; - if (!overrides || typeof overrides !== 'object') { - overrides = {}; - } - - function Extended() { - var instance; - var args = Array.prototype.slice.call(arguments); - try { - instance = Reflect.construct(baseCtor, args); - } catch (_) { - instance = {}; - } - - for (var key in overrides) { - if (key === 'interfaces') { - continue; - } - var value = overrides[key]; - if (typeof value === 'function') { - try { - Object.defineProperty(instance, key, { - value: value, - writable: true, - configurable: true, - enumerable: true, - }); - } catch (_) { - instance[key] = value; - } - } else { - instance[key] = value; - } - } - - if (typeof overrides.init === 'function') { - try { - var initResult = overrides.init.apply(instance, args); - if (initResult && typeof initResult === 'object') { - instance = initResult; - } - } catch (_) { - // Keep constructor resilient; init errors should not crash runtime. - } - } - - if (Array.isArray(overrides.interfaces)) { - try { - Object.defineProperty(instance, '__interfaces__', { - value: overrides.interfaces.slice(), - writable: false, - configurable: true, - enumerable: false, - }); - } catch (_) { - instance.__interfaces__ = overrides.interfaces.slice(); - } - } - - ensureProxyInstance(instance, overrides, Extended); - - return instance; - } - - Extended.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); - Extended.prototype.constructor = Extended; - - for (var protoKey in overrides) { - if (protoKey === 'interfaces') { - continue; - } - Extended.prototype[protoKey] = overrides[protoKey]; - } - - if (typeName) { - try { - Object.defineProperty(Extended, 'name', { - value: typeName, - configurable: true, - }); - } catch (_) { - // Non-critical metadata assignment. - } - } - - Extended.__typeName__ = typeName || ctorName(baseCtor); - Extended.__nsWinRTClass__ = true; - var metadata = buildProxyMetadata(baseCtor, typeName, overrides, Extended); - - Extended.extend = function (nextNameOrOverrides, nextMaybeOverrides) { - return makeExtendedConstructor(Extended, nextNameOrOverrides, nextMaybeOverrides); - }; - - try { - Object.defineProperty(Extended, 'emitProxy', { - value: function (outDir) { - return NSWinRT.proxy.emit(metadata, outDir); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Extended.emitProxy = function (outDir) { - return NSWinRT.proxy.emit(metadata, outDir); - }; - } - - return Extended; - } - - if (typeof Function.prototype.extend !== 'function') { - Object.defineProperty(Function.prototype, 'extend', { - value: function (nameOrOverrides, maybeOverrides) { - // A WinRT class (or a JS class deriving from one): an ES2015 subclass, so - // instances get the composed-object construction of `class X extends Y`. - if (globalThis.NSWinRT && typeof globalThis.NSWinRT.extendClass === 'function') { - if (typeof this.__typeName__ === 'string') - return globalThis.NSWinRT.extendClass(this, nameOrOverrides, maybeOverrides); - // Object.extend({ interfaces: [...], ... }): a subclass of System.Object. - if (this === Object && globalThis.System) - return globalThis.NSWinRT.extendClass(globalThis.System.Object, nameOrOverrides, maybeOverrides); - } - return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - if (typeof Object.extend !== 'function') { - Object.defineProperty(Object, 'extend', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // Create a managed-backed subclass factory. This returns a constructor - // that, when invoked, asks the managed bridge to instantiate a real - // C# subclass whose vtable forwards virtual calls back into the - // provided JS `overrides` object via the DotNet bridge. - function makeManagedConstructor(baseCtor, nameOrOverrides, maybeOverrides) { - var hasName = typeof nameOrOverrides === 'string'; - var explicitTypeName = hasName ? nameOrOverrides : ''; - var typeName = explicitTypeName || autoProxyTypeName(baseCtor); - var overrides = hasName ? maybeOverrides : nameOrOverrides; - if (!overrides || typeof overrides !== 'object') overrides = {}; - - function Managed() { - var args = Array.prototype.slice.call(arguments); - // Pass the actual base ctor type name as the assembly param so the - // bridge can fall back to it when no static proxy exists for typeName. - var baseTypeName = ctorName(baseCtor) || ''; - var obj = NSWinRT.proxy.createManagedSubclass(baseTypeName, typeName || '', overrides); - if (typeof overrides.init === 'function') { - try { - var initResult = overrides.init.apply(obj, args); - if (initResult && typeof initResult === 'object') obj = initResult; - } catch (_) { } - } - return obj; - } - - Managed.prototype = Object.create((baseCtor && baseCtor.prototype) || Object.prototype); - Managed.prototype.constructor = Managed; - Managed.extend = function (nextNameOrOverrides, nextMaybeOverrides) { - return makeManagedConstructor(Managed, nextNameOrOverrides, nextMaybeOverrides); - }; - try { - Object.defineProperty(Managed, 'name', { value: typeName || ctorName(baseCtor), configurable: true }); - } catch (_) { } - Managed.__typeName__ = typeName || ctorName(baseCtor); - Managed.__nsWinRTClass__ = true; - var meta = buildProxyMetadata(baseCtor, typeName, overrides, Managed); - try { - Object.defineProperty(Managed, 'emitProxy', { - value: function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }, - writable: true, - configurable: true, - enumerable: false, - }); - } catch (_) { - Managed.emitProxy = function (outDir) { return NSWinRT.proxy.emit(meta, outDir); }; - } - return Managed; - } - - if (typeof Function.prototype.extendManaged !== 'function') { - Object.defineProperty(Function.prototype, 'extendManaged', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(this, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - if (typeof Object.extendManaged !== 'function') { - Object.defineProperty(Object, 'extendManaged', { - value: function (nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(Object, nameOrOverrides, maybeOverrides); - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // Convenience alias for hosts that expect a global BaseClass helper. - globalThis.BaseClass = globalThis.BaseClass || {}; - if (typeof globalThis.BaseClass.extend !== 'function') { - globalThis.BaseClass.extend = function (baseCtor, nameOrOverrides, maybeOverrides) { - return makeManagedConstructor(baseCtor || Object, nameOrOverrides, maybeOverrides); - }; - } - - // Registry mapping each WinRT-extended prototype to its lazy factory. - // Keyed by the new Child.prototype created in __extends_winrt so that - // the Parent.apply override can look up the right factory per subclass. - var __nsExtendRegistry__ = new WeakMap(); - - function __extends_winrt(Child, Parent) { - var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; - extendStatics(Child, Parent); - - // Lazy factory: deferred until first `new Child()` so that TypeScript - // has had a chance to populate Child.prototype with override methods. - var _extended = null; - var getExtended = function () { - if (!_extended) { - var overrides = {}; - var interfaces = []; - var proto = Child.prototype; - if (proto) { - Object.getOwnPropertyNames(proto).forEach(function (key) { - if (key === 'constructor') return; - if (key === 'interfaces') { - var iv = proto[key]; - if (Array.isArray(iv)) interfaces = iv; - return; - } - overrides[key] = proto[key]; - }); - } - if (interfaces.length) overrides.interfaces = interfaces; - var name = Child.__winrtProxyName__ || Child.name || ''; - var hasBridge = typeof globalThis.__nsDotNetCreateJsSubclass === 'function'; - if (Child.__winrtProxyName__ || (interfaces.length > 0 && hasBridge)) { - _extended = makeManagedConstructor(Parent, name, overrides); - } else { - // No bridge or no interfaces: pure JS proxy via Reflect.construct. - _extended = makeExtendedConstructor(Parent, name, overrides); - } - } - return _extended; - }; - - var newProto = Object.create((Parent && Parent.prototype) || Object.prototype); - newProto.constructor = Child; - Child.prototype = newProto; - __nsExtendRegistry__.set(newProto, getExtended); - - // Patch Parent.apply/call once per parent class. The WeakMap lookup - // inside the override picks the correct factory per subclass so that - // multiple classes extending the same parent coexist correctly. - if (!Parent.__nsApplyPatched__) { - Parent.__nsApplyPatched__ = true; - - Parent.apply = function (thiz, args) { - if (thiz && thiz.__nsContainer__) return; - var factory = __nsExtendRegistry__.get(Object.getPrototypeOf(thiz)); - if (!factory) return; - thiz.__nsContainer__ = true; - try { - var Extended = factory(); - var argArr = Array.isArray(args) ? args : (args ? Array.prototype.slice.call(args) : []); - if (argArr.length > 0) { - thiz.__proto__ = new (Function.prototype.bind.apply(Extended, [null].concat(argArr)))(); - } else { - thiz.__proto__ = new Extended(); - } - return thiz.__proto__; - } finally { - delete thiz.__nsContainer__; - } - }; - - Parent.call = function (thiz) { - return Parent.apply(thiz, Array.prototype.slice.call(arguments, 1)); - }; - } - } - - // Global __extends that TypeScript compiled output resolves via - // `var __extends = (this && this.__extends) || ...`. - // Intercepts WinRT parent classes; falls back to standard TS behaviour. - if (typeof globalThis.__extends !== 'function') { - var __extends_ts_inner = function (d, b) { - if (b !== null && typeof b !== 'function') { - throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null'); - } - var extendStatics = Object.setPrototypeOf || function (d, b) { d.__proto__ = b; }; - extendStatics(d, b); - function __() { this.constructor = d; } - d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); - }; - - globalThis.__extends = function (d, b) { - if (b && b.__nsWinRTClass__) { - __extends_winrt(d, b); - } else { - __extends_ts_inner(d, b); - } - }; - } - - // TypeScript decorator helpers, as the iOS/Android runtimes provide them: compiled - // (`experimentalDecorators`) code calls a bare `__decorate(...)`. Core's globals module - // binds tslib's copies too, but an ES-module bundle can evaluate decorated core modules - // before it runs. - if (typeof globalThis.__decorate !== 'function') { - globalThis.__decorate = function (decorators, target, key, desc) { - var c = arguments.length; - var r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc, d; - if (typeof Reflect === 'object' && typeof Reflect.decorate === 'function') { - r = Reflect.decorate(decorators, target, key, desc); - } else { - for (var i = decorators.length - 1; i >= 0; i--) { - if ((d = decorators[i])) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - } - } - return c > 3 && r && Object.defineProperty(target, key, r), r; - }; - } - if (typeof globalThis.__param !== 'function') { - globalThis.__param = function (paramIndex, decorator) { - return function (target, key) { decorator(target, key, paramIndex); }; - }; - } - if (typeof globalThis.__metadata !== 'function') { - globalThis.__metadata = function (metadataKey, metadataValue) { - if (typeof Reflect === 'object' && typeof Reflect.metadata === 'function') { - return Reflect.metadata(metadataKey, metadataValue); - } - }; - } - - // `return global.__native(this)` ends TypeScript subclass constructors written for - // Android/iOS; here `this` already is the native-backed object. - if (typeof globalThis.__native !== 'function') { - globalThis.__native = function (obj) { return obj; }; - } - - // @NativeClass / @NativeClass(): marks a class that extends a native (WinRT/.NET) class. - // Extending works without it; it exists so code written for iOS/Android, where the build - // transforms decorated classes, runs unchanged. - if (typeof globalThis.NativeClass !== 'function') { - Object.defineProperty(globalThis, 'NativeClass', { - value: function NativeClass(target) { - if (typeof target === 'function') return target; - return function (cls) { return cls; }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - // @Interfaces([IFoo, IBar]): attach WinRT interface list to a class. - if (typeof globalThis.Interfaces !== 'function') { - Object.defineProperty(globalThis, 'Interfaces', { - value: function Interfaces(interfacesList) { - return function (target) { - target.prototype.interfaces = Array.isArray(interfacesList) ? interfacesList.slice() : []; - return target; - }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - if (typeof globalThis.CSharpProxy !== 'function') { - Object.defineProperty(globalThis, 'CSharpProxy', { - value: function CSharpProxy(name) { - return function (target) { - target.__winrtProxyName__ = typeof name === 'string' ? name : ''; - return target; - }; - }, - writable: true, - configurable: true, - enumerable: false, - }); - } - - function defaultProxyOutDir(meta) { - var typeName = (meta && meta.typeName) ? meta.typeName : 'GeneratedProxy'; - var safe = safeIdentifier(typeName.split('.').pop()); - return './generated-proxies/' + safe; - } - - function emitProxy(meta, outDir) { - if (!meta || typeof meta !== 'object') { - throw new Error('NSWinRT.proxy.emit(meta[, outDir]) expects a proxy metadata object'); - } - if (typeof globalThis.__nsProxyWriteTextFile !== 'function') { - throw new Error('Host proxy file emitter is not available'); - } - - var dir = outDir || defaultProxyOutDir(meta); - var csprojPath = dir + '/Proxy.csproj'; - var csPath = dir + '/Proxy.g.cs'; - var csproj = renderProxyCsproj(meta); - var source = renderProxyCSharp(meta); - - globalThis.__nsProxyWriteTextFile(csprojPath, csproj); - globalThis.__nsProxyWriteTextFile(csPath, source); - - meta.generated = { - dir: dir, - csprojPath: csprojPath, - csPath: csPath, - }; - - return meta.generated; - } - - function compileProxy(meta, outDir, configuration) { - if (typeof globalThis.__nsProxyCompileProject !== 'function') { - throw new Error('Host proxy compiler is not available'); - } - var generated = emitProxy(meta, outDir); - var result = globalThis.__nsProxyCompileProject(generated.csprojPath, configuration || 'Debug'); - generated.build = result; - return generated; - } - - function registerProxy(meta, outDir, configuration) { - var generated = compileProxy(meta, outDir, configuration); - var manifest = { - kind: 'windows-proxy', - typeName: meta.typeName, - baseType: meta.baseType, - interfaces: meta.interfaces, - methods: meta.methods, - properties: meta.properties, - generated: generated, - registration: { - hostCanLoadAssemblies: false, - note: 'Assembly build succeeded, but runtime CLR proxy activation is not wired yet. Dynamic JS fallback remains active.', - }, - }; - if (typeof globalThis.__nsProxyRegisterManifest === 'function') { - globalThis.__nsProxyRegisterManifest(JSON.stringify(manifest)); - } - meta.registered = true; - meta.registration = manifest.registration; - return manifest; - } - - function invokeProxyById(proxyId, methodName, argsArray) { - var entry = proxyInstances.get(proxyId); - if (!entry) { - throw new Error('Proxy instance not found for id ' + proxyId); - } - - var target = entry.instance; - var method = target && target[methodName]; - if (typeof method !== 'function') { - throw new Error('Proxy method "' + methodName + '" is not defined on proxy id ' + proxyId); - } - - return method.apply(target, Array.isArray(argsArray) ? argsArray : []); - } - - globalThis.__nsInvokeProxyJs = invokeProxyById; - - globalThis.NSWinRT.proxy = { - getExtensions: function () { - return proxyExtensions.slice(); - }, - emit: emitProxy, - compile: compileProxy, - register: registerProxy, - createManagedSubclass: function (assembly, typeName, overrides) { - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') { - throw new Error('__nsDotNetCreateJsSubclass is not available in this runtime'); - } - if (!overrides || typeof overrides !== 'object') overrides = {}; - - var interfaceNames = Array.isArray(overrides.interfaces) - ? overrides.interfaces.map(function (iface) { return ctorName(iface); }) - : []; - - // Literal CLR member names this JS object actually overrides. The bridge only - // emits an IL override for base virtuals named here (everything else falls - // through to the real base implementation) and implements exactly these - // interface members from JS (others fall back to default/no-op). Plain - // functions are method overrides; accessor descriptors become get_/set_- - // prefixed property overrides: matching the literal CLR accessor names the - // dynamic proxy already dispatches by. Read via getOwnPropertyDescriptor - // throughout so collecting names never invokes a JS getter as a side effect. - var memberNames = []; - for (var mkey in overrides) { - if (!Object.prototype.hasOwnProperty.call(overrides, mkey)) continue; - if (mkey === 'interfaces' || mkey === 'init') continue; - var mdesc = Object.getOwnPropertyDescriptor(overrides, mkey); - if (!mdesc) continue; - if (typeof mdesc.get === 'function' || typeof mdesc.set === 'function') { - if (typeof mdesc.get === 'function') memberNames.push('get_' + mkey); - if (typeof mdesc.set === 'function') memberNames.push('set_' + mkey); - } else if (typeof mdesc.value === 'function') { - memberNames.push(mkey); - } - } - - var dispatcher = function () { - var a = Array.prototype.slice.call(arguments); - try { - var target = _wrapDotNetHandle(a[0]); - var method = a[1]; - var margs = a.slice(2).map(_wrapDotNetHandle); - if (target && typeof method === 'string') { - var fn = overrides[method]; - if (typeof fn === 'function') return fn.apply(target, Array.isArray(margs) ? margs : []); - } - } catch (_) { } - }; - var handle = globalThis.__nsDotNetCreateJsSubclass(assembly || '', typeName || '', interfaceNames, memberNames, dispatcher); - var obj = _wrapDotNetHandle(handle); - ensureProxyInstance(obj, overrides, function () { }); - return obj; - }, - invokeById: invokeProxyById, - listRegisteredManifests: function () { - if (typeof globalThis.__nsProxyListManifests === 'function') { - return globalThis.__nsProxyListManifests(); - } - return []; - }, - }; -})(); - -(function () { - if (typeof globalThis.__nsDotNetInvokeBin !== 'function') return; - // installDotnet() is independently callable (unlike the classic engine's single monolithic - // HELPER_SOURCE, where some earlier block always creates NSWinRT first): don't assume - // install_interop ran first. - globalThis.NSWinRT = globalThis.NSWinRT || {}; - - // Binary protocol: no JSON.stringify / JSON.parse. - // __nsDotNetInvokeBin(handle, typeName, assembly, method, ...args) - // throws on error, returns the result value directly. - function _invoke(req) { - var handle = (req.handle !== undefined && req.handle !== null) ? req.handle : -1; - var args = req.args; - var typeName = req.typeName || '', assembly = req.assembly || '', method = req.method || ''; - if (!args || args.length === 0) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method); - if (args.length === 1) return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, args[0]); - return globalThis.__nsDotNetInvokeBin(handle, typeName, assembly, method, ...args); - } - - // _invoke with a function's own `arguments`, unwrapped straight into the native call (no - // request object or intermediate arrays). - function _invokeWith(handle, typeName, assembly, method, args) { - var invoke = globalThis.__nsDotNetInvokeBin; - switch (args.length) { - case 0: return invoke(handle, typeName, assembly, method); - case 1: return invoke(handle, typeName, assembly, method, _unwrap(args[0])); - case 2: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1])); - case 3: return invoke(handle, typeName, assembly, method, _unwrap(args[0]), _unwrap(args[1]), _unwrap(args[2])); - } - // Built by push so the array stays packed: apply on a holey array takes V8's slow path. - var call = [handle, typeName, assembly, method]; - for (var i = 0; i < args.length; i++) call.push(_unwrap(args[i])); - return invoke.apply(null, call); - } - - // Populated lazily on first access; avoids repeated bridge round-trips. - var _typeInfoCache = {}; - // A type's member lists get `kinds` (member name -> _K_* flags), so the proxy traps look a name up - // instead of scanning the lists on every access. - var _K_PROP = 1, _K_WO = 2, _K_RO = 4, _K_SPROP = 8, _K_SMETHOD = 16, _K_SWO = 32, _K_SRO = 64; - function _withKinds(info) { - var kinds = Object.create(null); - function add(list, flag) { - if (!list) return; - for (var i = 0; i < list.length; i++) kinds[list[i]] = (kinds[list[i]] | 0) | flag; - } - add(info.properties, _K_PROP); - add(info.writeonlyProperties, _K_WO); - add(info.readonlyProperties, _K_RO); - add(info.staticProperties, _K_SPROP); - add(info.staticMethods, _K_SMETHOD); - add(info.writeonlyStaticProperties, _K_SWO); - add(info.readonlyStaticProperties, _K_SRO); - info.kinds = kinds; - return info; - } - function _kindsOf(info) { return info.kinds || _withKinds(info).kinds; } - - // "get_X" / "set_X" accessor names, made once per property name. - var _getterNames = Object.create(null), _setterNames = Object.create(null); - function _getterName(prop) { return _getterNames[prop] || (_getterNames[prop] = 'get_' + prop); } - function _setterName(prop) { return _setterNames[prop] || (_setterNames[prop] = 'set_' + prop); } - - // The native COM pointer of a handle's object as the BigInt the runtime reads from `__native_ptr`, - // or undefined when it has none. The bridge makes it for a plain .NET object only on request. - function _nativePtrOf(handle) { - try { - var p = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNativePtrForHandle', args: [handle] }); - if (typeof p !== 'number' || p === 0) return undefined; - return typeof BigInt === 'function' ? BigInt(p) : p; - } catch (_) { - return undefined; - } - } - var _emptyInfo = _withKinds({ methods: [], properties: [], staticMethods: [], staticProperties: [], readonlyProperties: [], readonlyStaticProperties: [], writeonlyProperties: [], writeonlyStaticProperties: [] }); - // Optional mapping for namespace prefixes -> assembly simple-name. - // Exact namespaces are preferred first, then progressively shorter - // prefixes are tried as a fallback. - var _namespaceAssemblyMap = Object.create(null); - - // _resolveAssembly results by type name, cleared whenever the namespace map changes. - var _assemblyCache = Object.create(null); - - function _resolveAssembly(typeName) { - if (!typeName || typeof typeName !== 'string') return ''; - var cached = _assemblyCache[typeName]; - if (cached !== undefined) return cached; - var resolved = ''; - var probe = typeName; - while (probe) { - var assembly = _namespaceAssemblyMap[probe]; - if (typeof assembly === 'string' && assembly) { resolved = assembly; break; } - var lastDot = probe.lastIndexOf('.'); - if (lastDot < 0) break; - probe = probe.substring(0, lastDot); - } - _assemblyCache[typeName] = resolved; - return resolved; - } - - // When the JS GC collects a DotNet proxy the registry fires the - // callback with the managed handle id, releasing the CLR reference. - // Explicit sw.release() still works for deterministic teardown. - var _dotNetFinalizers = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(function (handle) { - try { _invoke({ handle: handle, method: '__release', args: [] }); } catch (e) {} - }) - : null; - - // Auto-populate namespace→assembly map from the managed side. - // GetNamespaceAssemblyMapJson scans the app directory for assemblies and - // returns a JSON map of { rootNamespace: assemblySimpleName }. - // Runs once at IIFE init; any failure is silently ignored. - try { - var _autoNsJson = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'GetNamespaceAssemblyMapJson', args: [] }); - if (typeof _autoNsJson === 'string' && _autoNsJson) { - var _parsed = JSON.parse(_autoNsJson); - if (_parsed && typeof _parsed === 'object') { - for (var _k in _parsed) { - if (Object.prototype.hasOwnProperty.call(_parsed, _k) && typeof _parsed[_k] === 'string') { - _namespaceAssemblyMap[_k] = _parsed[_k]; - } - } - } - } - } catch (_e) {} - - function _getTypeInfo(assembly, typeName) { - if (!typeName) return _emptyInfo; - var cached = _typeInfoCache[typeName]; - if (cached !== undefined) return cached; - var info = _emptyInfo; - try { - // Respect an explicitly-provided assembly name. When empty, - // let the managed side attempt resolution (BCL types via Type.GetType). - var asm = (typeof assembly === 'string') ? assembly : ''; - var members = _invoke({ assembly: asm, typeName: typeName, method: '__members__', args: [] }); - if (members && typeof members === 'object') info = _withKinds(members); - } catch (e) {} - _typeInfoCache[typeName] = info; - return info; - } - - function _unwrap(v) { - if (v && typeof v === 'object' && typeof v.__handle === 'number') return { __handle: v.__handle }; - if (Array.isArray(v)) return v.map(_unwrap); - // A JS Date reaches DateTime / DateTimeOffset parameters as its ISO 8601 string. - if (v instanceof Date) return isNaN(v.getTime()) ? null : v.toISOString(); - // A JS function passed where .NET expects a delegate gets .NET objects as arguments, the same - // proxies a method result would be, rather than raw handles. - if (typeof v === 'function' && typeof v.__dotnetType__ !== 'string' && typeof v.__handle !== 'number') - return _callbackFor(v); - return v; - } - - // The wrapper sent for a JS callback: one per function, so passing it again sends the same one. - var _callbacks = typeof WeakMap === 'function' ? new WeakMap() : null; - function _callbackFor(fn) { - var wrapped = _callbacks && _callbacks.get(fn); - if (wrapped) return wrapped; - wrapped = function () { return fn.apply(this, Array.prototype.map.call(arguments, _wrap)); }; - if (_callbacks) _callbacks.set(fn, wrapped); - return wrapped; - } - - // .NET collections (List, Dictionary, ObservableCollection) act like JS arrays: `length`, - // `list[i]`, for...of, and the array methods (map, filter, ...) on a copy of their items. - // _collectionKind is the bridge's CollectionKind flags (1 enumerable, 2 count, 4 indexed) by type. - var _collectionKinds = Object.create(null); - var _arrayMethods = Object.create(null); - ['forEach', 'map', 'filter', 'reduce', 'reduceRight', 'some', 'every', 'find', 'findIndex', 'findLast', - 'findLastIndex', 'indexOf', 'lastIndexOf', 'includes', 'join', 'slice', 'concat', 'flat', 'flatMap', - 'entries', 'keys', 'values', 'at', 'toJSON'].forEach(function (name) { _arrayMethods[name] = 1; }); - - function _bridgeCall(method, args) { - return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: method, args: args }); - } - - function _isIndex(prop) { - var c = prop.charCodeAt(0); - return c >= 48 && c <= 57 && /^(0|[1-9]\d*)$/.test(prop); - } - - function _collectionKind(handle, typeName) { - var kind = typeName ? _collectionKinds[typeName] : undefined; - if (kind !== undefined) return kind; - try { kind = _bridgeCall('CollectionKind', [{ __handle: handle }]) | 0; } catch (_) { kind = 0; } - if (typeName) _collectionKinds[typeName] = kind; - return kind; - } - - function _collectionItems(handle) { - return _wrap(_bridgeCall('CollectionItems', [{ __handle: handle }])) || []; - } - - // `prop` of a collection proxy when it is one of the array-like members, else undefined. - function _collectionMember(handle, typeName, prop) { - var kind = _collectionKind(handle, typeName); - if (prop === 'length') return (kind & 2) ? _bridgeCall('CollectionCount', [{ __handle: handle }]) : undefined; - if (_arrayMethods[prop] === 1) { - if (!(kind & 1)) return undefined; - return function () { - var items = _collectionItems(handle); - return prop === 'toJSON' ? items : Array.prototype[prop].apply(items, arguments); - }; - } - return (kind & 4) ? _wrap(_bridgeCall('ItemAt', [{ __handle: handle }, +prop])) : undefined; - } - - // Value types whose JS string is their .NET ToString(), so `${DateTime.Now}` reads as a date. - var _netToString = { 'System.DateTime': 1, 'System.DateTimeOffset': 1, 'System.TimeSpan': 1 }; - - // Makes sw.Stop() and sw.Elapsed both work naturally. - // The proxy is registered with _dotNetFinalizers so the CLR reference - // is released automatically when JS GC collects the proxy. - function _makeDotNetInstance(handle, assembly, typeName, isTask, nativePtr) { - _getTypeInfo(assembly, typeName); - // Method functions by name, made on first access. - var methods = null; - var ptrKnown = nativePtr !== undefined; - // The proxy target holds the plain JS fields set on the object. - var proxy = new Proxy({}, { - get: function (target, prop) { - if (typeof prop === 'symbol') { - if (prop === Symbol.iterator && (_collectionKind(handle, typeName) & 1)) - return function () { return _collectionItems(handle)[Symbol.iterator](); }; - return target[prop]; - } - if (prop === '__handle') return handle; - if (prop === '__type') return typeName; - if (prop === '__isTask') return isTask === true; - if (prop === '__native_ptr') { - if (!ptrKnown) { ptrKnown = true; nativePtr = _nativePtrOf(handle); } - return nativePtr; - } - if (prop === 'release') return function () { - _invoke({ handle: handle, method: '__release', args: [] }); - }; - // Like a plain object, so `'' + obj` and comparisons fall through to toString. - if (prop === 'valueOf') return function () { return proxy; }; - if (prop === 'toString') return function () { - if (_netToString[typeName] === 1) return String(_invoke({ handle: handle, method: 'ToString', args: [] })); - return '[DotNetObject ' + typeName + ' #' + handle + ']'; - }; - // A Task/ValueTask (awaitable) result is thenable, so `await obj.SomethingAsync()` works; - // any other .NET object must not look like a promise. - if (prop === 'then') { - if (isTask !== true) return undefined; - return function (onFulfilled, onRejected) { - return globalThis.NSWinRT.dotnet.taskToPromise({ __handle: handle, __isTask: true }).then(onFulfilled, onRejected); - }; - } - if (Object.prototype.hasOwnProperty.call(target, prop)) return target[prop]; - // Re-read info in case it was populated after construction. - var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; - // Write-only (setter, no getter): reading it is an error. - if (kind & _K_WO) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); - if (kind & _K_PROP) - return _wrap(_invoke({ handle: handle, method: _getterName(prop), args: [] })); - if (prop === 'length' || _arrayMethods[prop] === 1 || _isIndex(prop)) { - var member = _collectionMember(handle, typeName, prop); - if (member !== undefined) return member; - } - // Not a native property: a function for method dispatch. - if (methods === null) methods = Object.create(null); - return methods[prop] || (methods[prop] = function () { - return _wrap(_invokeWith(handle, '', '', prop, arguments)); - }); - }, - set: function (target, prop, value) { - if (typeof prop !== 'symbol') { - var kind = _kindsOf(_typeInfoCache[typeName] || _emptyInfo)[prop] | 0; - // Read-only (getter, no setter): assigning it is an error. - if (kind & _K_RO) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + typeName + '\''); - // Writable (read-write or write-only): invoke the setter. - if (kind & (_K_PROP | _K_WO)) { - _invoke({ handle: handle, method: _setterName(prop), args: [_unwrap(value)] }); - return true; - } - if (_isIndex(prop) && (_collectionKind(handle, typeName) & 4)) { - _bridgeCall('SetItemAt', [{ __handle: handle }, +prop, _unwrap(value)]); - return true; - } - } - // Not a native property: a plain JS field of this object. - target[prop] = value; - return true; - }, - }); - if (_dotNetFinalizers) _dotNetFinalizers.register(proxy, handle); - return proxy; - } - - function _wrap(value) { - if (value == null) return null; - if (Array.isArray(value)) return value.map(_wrap); - if (typeof value === 'object' && typeof value.__handle === 'number') { - var typeName = value.__type || ''; - var jsSelf = _jsSelfByHandle && _deref(_jsSelfByHandle.get(value.__handle)); - if (jsSelf) return jsSelf; - // A JS subclass instance whose JS object was collected: its dispatcher revives one. - if (typeName.indexOf('NSWinRTDynamicProxies.') === 0) { - try { _invoke({ handle: value.__handle, method: '__ns_js_self', args: [] }); } catch (_) {} - jsSelf = _deref(_jsSelfByHandle.get(value.__handle)); - if (jsSelf) return jsSelf; - } - var assembly = _resolveAssembly(typeName); - return _makeDotNetInstance(value.__handle, assembly, typeName, value.__isTask === true, value.__native_ptr); - } - return value; - } - - globalThis.NSWinRT.dotnet = { - invoke: function (assembly, typeName, method, args) { - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: (args || []).map(_unwrap) })); - }, - get: function (assembly, typeName, prop) { - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: 'get_' + prop, args: [] })); - }, - fromHandle: function (handle, typeName) { - var assembly = _resolveAssembly(typeName || ''); - return _makeDotNetInstance(handle, assembly, typeName || ''); - }, - // Exposed so other IIFEs in this bootstrap (e.g. the managed-subclass/proxy block, - // which runs in its own closure earlier in the script and has no direct access to - // this IIFE's private `_wrap`) can wrap a raw tagged handle value the same way. - wrap: _wrap, - // Lazily register a namespace prefix and optional assembly mapping. - // Example: NSWinRT.dotnet.registerNamespace('com', 'NativeScript'); - registerNamespace: function(rootName, assemblyName) { - if (!rootName || typeof rootName !== 'string') return; - var name = String(rootName); - var root = name.split('.')[0]; - if (assemblyName && typeof assemblyName === 'string') { - _assemblyCache = Object.create(null); - _namespaceAssemblyMap[name] = assemblyName; - if (!_namespaceAssemblyMap[root]) { - _namespaceAssemblyMap[root] = assemblyName; - } - } - if (root in globalThis) return; - try { - Object.defineProperty(globalThis, root, { - configurable: true, - enumerable: true, - get: function() { - var proxy = _makeNamespaceProxy(root); - try { Object.defineProperty(globalThis, root, { value: proxy, writable: true, configurable: true, enumerable: true }); } catch (_) {} - return proxy; - }, - // A namespace root never blocks app code from using the name for its own value - // (`var App = ...` in a classic script assigns through this accessor). - set: function(value) { - try { Object.defineProperty(globalThis, root, { value: value, writable: true, configurable: true, enumerable: true }); } catch (_) {} - } - }); - } catch (_) { - try { globalThis[root] = _makeNamespaceProxy(root); } catch (_) {} - } - }, - registerNamespaces: function(arr) { - if (!Array.isArray(arr)) return; - for (var i = 0; i < arr.length; i++) this.registerNamespace(arr[i]); - }, - }; - - // ── .NET types as JS classes ──────────────────────────────────────────────── - // A .NET type proxy (System.Text.StringBuilder, MyApp.Native.Greeter) is a constructor with a - // stable `prototype`, so it can be extended the way Java and Objective-C classes are on Android - // and iOS: - // class Cat extends MyApp.Animal { constructor(n) { super(n); } Speak() { return super.Speak(); } } - // const Dog = MyApp.Animal.extend('Dog', { Speak() { return this.super.Speak(); } }); - // const calc = new MyApp.ICalculator({ Compute(a, b) { return a * b; } }); - // Constructing a subclass creates the JS object first, then a managed subclass of the .NET type - // whose overridden virtuals (and implemented interface members) call back into that object. - // `super.X()` reaches the base implementation through the non-virtual `__ns_base_X` entry point - // the bridge emits for every overridden member. Native code handing the instance back to JS gets - // the same JS object. - var _nsProxyCache = Object.create(null); - var _namespaceExtras = { System: Object.create(null) }; - var _typeKindCache = Object.create(null); - var _typeProtoCache = Object.create(null); - var _subclassInfoCache = new WeakMap(); - var _jsSelfByHandle = new Map(); - - // ── Lifetime of JS subclass instances ─────────────────────────────────────── - // An instance is a JS object plus the managed object it drives. The JS object holds the managed - // one through its handle; the managed object reaches the JS object only through its dispatcher, - // which holds it weakly. So once JS drops the JS object, its handle is released and the managed - // object goes as soon as .NET/native code drops it too (the bridge then unpins the dispatcher). - // When native code calls into an instance whose JS object was collected, a new JS object of the - // same class is made for it: fields its constructor set are gone, as on Android when a JS object - // used only from native code is collected. Subclasses of WinRT UI classes are kept strongly while - // they are in a live XAML tree. - var _canWeakRef = typeof WeakRef === 'function'; - var _selfFinalizers = typeof FinalizationRegistry === 'function' - ? new FinalizationRegistry(function (handle) { - if (!_deref(_jsSelfByHandle.get(handle))) _jsSelfByHandle.delete(handle); - try { _invoke({ handle: handle, method: '__release', args: [] }); } catch (_) {} - }) - : null; - - function _weak(obj) { return _canWeakRef ? new WeakRef(obj) : obj; } - function _deref(ref) { return ref && _canWeakRef && ref instanceof WeakRef ? ref.deref() : ref; } - - // Gives `self` the bridge identity in `raw` and registers it as the JS object of `entry`'s instance. - function _adopt(entry, self, raw) { - Object.defineProperty(self, '__handle', { value: raw.__handle }); - Object.defineProperty(self, '__nsOverrides', { value: entry.overrides }); - if (!entry.winrt) { - Object.defineProperty(self, '__type', { value: raw.__type || entry.typeName }); - if (raw.__native_ptr !== undefined) Object.defineProperty(self, '__native_ptr', { value: raw.__native_ptr }); - } - entry.ref = _weak(self); - _jsSelfByHandle.set(raw.__handle, _weak(self)); - if (_selfFinalizers) _selfFinalizers.register(self, raw.__handle); - if (entry.winrt) { - entry.strong = self; - _attachedEntries.add(entry); - } - _sweepAttached(); - return self; - } - - // The JS object of `entry`'s instance, made anew when the previous one was collected. - function _selfFor(entry, raw) { - var self = _deref(entry.ref); - if (self) return self; - if (entry.constructing) throw new Error(entry.typeName + ' called into its JS subclass before the instance existed'); - if (!raw || typeof raw.__handle !== 'number') throw new Error('The ' + entry.typeName + ' subclass instance is gone'); - if (entry.winrt) { - self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, entry.typeName); - Object.setPrototypeOf(self, entry.proto); - } else { - self = Object.create(entry.proto); - } - return _adopt(entry, self, raw); - } - - // The dispatcher native code calls the instance's overridden and interface members through. - function _makeDispatcher(entry) { - return function (selfHandle, member) { - var self = _selfFor(entry, selfHandle); - if (member === '__ns_js_self') return undefined; - _sweepAttached(); - var margs = Array.prototype.slice.call(arguments, 2).map(_wrap); - return _dispatchToJs(self, entry.baseProto, entry.typeName, member, margs); - }; - } - - // Subclasses of WinRT classes held strongly (entry.strong) while in a live XAML tree, re-checked at - // most every two seconds when subclass instances are created or called into. - var _attachedEntries = new Set(); - var _lastAttachedSweep = 0; - function _sweepAttached() { - var now = Date.now(); - if (now - _lastAttachedSweep < 2000) return; - _lastAttachedSweep = now; - _attachedEntries.forEach(function (entry) { - var self = _deref(entry.ref); - if (!self) { - _attachedEntries.delete(entry); - return; - } - var attached = false; - try { attached = self.XamlRoot != null; } catch (_) {} - entry.strong = attached ? self : undefined; - }); - } - var _baseCallPrefix = '__ns_base_'; - - function _has(list, name) { - return !!list && list.indexOf(name) >= 0; - } - - function _typeKind(path) { - var kind = _typeKindCache[path]; - if (kind !== undefined) return kind; - try { - kind = _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'TypeKind', args: [path] }) || ''; - } catch (_) { - kind = ''; - } - _typeKindCache[path] = kind; - return kind; - } - - // Full name of a .NET type proxy, or of the .NET base of a JS class deriving from one. - function _dotnetTypeName(type) { - if (typeof type === 'string') return type; - if (!type || (typeof type !== 'function' && typeof type !== 'object')) return ''; - var name = type.__dotnetType__; - return typeof name === 'string' ? name : ''; - } - - // The handle of an instance of a JS subclass (an own property); -1 for anything else, such as - // a class prototype further down the chain, without running any property trap. - function _ownHandle(o) { - if (!o || (typeof o !== 'object' && typeof o !== 'function')) return -1; - var d = Object.getOwnPropertyDescriptor(o, '__handle'); - return d && typeof d.value === 'number' ? d.value : -1; - } - - function _baseCallName(self, member) { - var overrides = self.__nsOverrides; - return overrides && overrides.has(member) ? _baseCallPrefix + member : member; - } - - // A native member of `self` (an instance of a JS subclass), reached through the .NET type's - // prototype: properties are read through their getter, methods become functions. Members the JS - // class overrides resolve to the base implementation — only `super.X` gets this far for them. - function _baseMember(path, self, prop) { - if (prop === 'toString') return function () { return String(_invoke({ handle: self.__handle, method: 'ToString', args: [] })); }; - if (prop === 'valueOf') return function () { return this; }; - var info = _getTypeInfo(_resolveAssembly(path), path); - var overrides = self.__nsOverrides; - if (_has(info.properties, prop) || (overrides && overrides.has('get_' + prop))) { - return _wrap(_invoke({ handle: self.__handle, method: _baseCallName(self, 'get_' + prop), args: [] })); - } - if (_has(info.writeonlyProperties, prop)) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); - if (_has(info.methods, prop) || (overrides && overrides.has(prop))) { - var method = _baseCallName(self, prop); - return function () { - var args = Array.prototype.slice.call(arguments).map(_unwrap); - return _wrap(_invoke({ handle: self.__handle, method: method, args: args })); - }; - } - return undefined; - } - - // The `prototype` of a .NET type: the end of every JS subclass's prototype chain. Reads resolve - // native members against the receiving instance; writes go to native properties, and anything - // else becomes a plain JS field of the instance. - function _typePrototype(path) { - var cached = _typeProtoCache[path]; - if (cached) return cached; - var proto = new Proxy(Object.create(null), { - get: function (_, prop, receiver) { - if (typeof prop === 'symbol') return undefined; - if (_ownHandle(receiver) < 0) { - // Read off the prototype itself — TypeScript's ES5 super call - // `_super.prototype.Method.call(this, ...)`: a function that runs the base - // implementation on whichever instance it is called with. - var typeInfo = _getTypeInfo(_resolveAssembly(path), path); - if (!_has(typeInfo.methods, prop)) return undefined; - return function () { - if (_ownHandle(this) < 0) throw new TypeError(path + '.prototype.' + prop + ' called on an object that is not a ' + path); - return _baseMember(path, this, prop).apply(this, arguments); - }; - } - return _baseMember(path, receiver, prop); - }, - set: function (_, prop, value, receiver) { - if (typeof prop !== 'symbol' && _ownHandle(receiver) >= 0) { - var info = _getTypeInfo(_resolveAssembly(path), path); - if (_has(info.readonlyProperties, prop)) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); - if (_has(info.properties, prop) || _has(info.writeonlyProperties, prop)) { - _invoke({ handle: receiver.__handle, method: _baseCallName(receiver, 'set_' + prop), args: [_unwrap(value)] }); - return true; - } - } - Object.defineProperty(receiver, prop, { value: value, writable: true, enumerable: true, configurable: true }); - return true; - }, - }); - _typeProtoCache[path] = proto; - return proto; - } - - // Members (literal CLR names: methods, get_X/set_X accessors) and interfaces a JS class adds on - // top of its .NET base, from every class between it and the base. - function _subclassInfo(proto, baseProto) { - var cached = _subclassInfoCache.get(proto); - if (cached) return cached; - var members = []; - var overrides = new Set(); - var interfaces = []; - var seen = Object.create(null); - function addInterfaces(list) { - if (!Array.isArray(list)) return; - for (var i = 0; i < list.length; i++) { - var name = _dotnetTypeName(list[i]) || (list[i] && (list[i].__typeName__ || list[i].name)) || ''; - if (name && interfaces.indexOf(name) < 0) interfaces.push(name); - } - } - for (var p = proto; p && p !== baseProto; p = Object.getPrototypeOf(p)) { - var keys = Object.getOwnPropertyNames(p); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - if (key === 'constructor' || key === 'interfaces' || key === 'super' || key === 'init' || seen[key]) continue; - seen[key] = true; - var d = Object.getOwnPropertyDescriptor(p, key); - if (typeof d.get === 'function') { members.push('get_' + key); overrides.add('get_' + key); } - if (typeof d.set === 'function') { members.push('set_' + key); overrides.add('set_' + key); } - if (typeof d.value === 'function') { members.push(key); overrides.add(key); } - } - if (Object.prototype.hasOwnProperty.call(p, 'interfaces')) addInterfaces(p.interfaces); - var ctor = Object.prototype.hasOwnProperty.call(p, 'constructor') ? p.constructor : null; - if (ctor && Object.prototype.hasOwnProperty.call(ctor, 'interfaces')) addInterfaces(ctor.interfaces); - } - var info = { members: members, overrides: overrides, interfaces: interfaces }; - _subclassInfoCache.set(proto, info); - return info; - } - - // The JS-defined member `key` of `self` (an own field, or a member of one of its JS classes). - function _jsMember(self, key, baseProto) { - for (var o = self; o && o !== baseProto; o = Object.getPrototypeOf(o)) { - var d = Object.getOwnPropertyDescriptor(o, key); - if (d) return d; - } - return null; - } - - // Native code calling an overridden member (or an implemented interface member) of `self`. - function _dispatchToJs(self, baseProto, typeName, member, args) { - var accessor = /^(get|set)[_:](.+)$/.exec(member); - if (accessor) { - var ad = _jsMember(self, accessor[2], baseProto); - if (ad) { - if (accessor[1] === 'get') { - if (typeof ad.get === 'function') return ad.get.call(self); - if ('value' in ad && typeof ad.value !== 'function') return ad.value; - } else if (typeof ad.set === 'function') { - ad.set.call(self, args[0]); - return undefined; - } else if ('value' in ad && ad.writable) { - self[accessor[2]] = args[0]; - return undefined; - } - } - } - var md = _jsMember(self, member, baseProto); - if (md && typeof md.value === 'function') return md.value.apply(self, args); - throw new Error((self.constructor && self.constructor.name || typeName) + ' does not implement ' + member); - } - - function _constructSubclass(path, args, proto) { - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function') - throw new Error('Extending .NET types is not available in this runtime'); - var baseProto = _typePrototype(path); - var info = _subclassInfo(proto, baseProto); - var self = Object.create(proto); - // A virtual the base constructor calls reaches `self` before it has its handle. - var entry = { proto: proto, baseProto: baseProto, typeName: path, overrides: info.overrides, winrt: false, ref: self }; - var raw = globalThis.__nsDotNetCreateJsSubclass('', path, info.interfaces, info.members, _makeDispatcher(entry), args.map(_unwrap)); - if (!raw || typeof raw.__handle !== 'number') throw new Error('Could not create an instance of a subclass of ' + path); - return _adopt(entry, self, raw); - } - - // `new SomeInterface({ ... })`: an object implementing the interface with the given members. - function _implementInterface(path, impl) { - var proto = Object.create(_typePrototype('System.Object')); - Object.defineProperties(proto, Object.getOwnPropertyDescriptors(impl)); - Object.defineProperty(proto, 'interfaces', { value: [path] }); - return _constructSubclass('System.Object', [], proto); - } - - // `this.super` inside a class made by extend(): the parent class's members bound to `self`. - function _superOf(self, Cls) { - var parent = Object.getPrototypeOf(Cls.prototype); - return new Proxy(Object.create(null), { - get: function (_, prop) { - var value = Reflect.get(parent, prop, self); - return typeof value === 'function' ? value.bind(self) : value; - }, - }); - } - - // Base.extend([name,] members): an ES2015 subclass of Base with the given members; `init`, when - // present, runs after construction with the constructor arguments. - function _extendClass(Base, nameOrMembers, maybeMembers) { - var hasName = typeof nameOrMembers === 'string'; - var members = (hasName ? maybeMembers : nameOrMembers) || {}; - var Extended = class extends Base { - constructor() { - super(...arguments); - if (typeof members.init === 'function') members.init.apply(this, arguments); - } - }; - var descriptors = Object.getOwnPropertyDescriptors(members); - delete descriptors.init; - if (descriptors.interfaces) { - Object.defineProperty(Extended.prototype, 'interfaces', { value: members.interfaces }); - delete descriptors.interfaces; - } - Object.defineProperties(Extended.prototype, descriptors); - Object.defineProperty(Extended.prototype, 'super', { - get: function () { return _superOf(this, Extended); }, - }); - if (hasName) Object.defineProperty(Extended, 'name', { value: nameOrMembers.split('.').pop() }); - return Extended; - } - - // `_super.call(this, ...)` / `_super.apply(this, args)` in TypeScript's ES5 output (what the - // @NativeClass transform emits): construct the subclass instance `thisArg` was created for. - function _es5SuperCall(path, thisArg, args) { - var proto = thisArg && Object.getPrototypeOf(thisArg); - if (!proto || !Object.prototype.isPrototypeOf.call(_typePrototype(path), proto)) - throw new TypeError('Class constructor ' + path + ' cannot be invoked without \'new\''); - return _constructSubclass(path, args, proto); - } - - function _hasInstance(path, value) { - if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false; - if (Object.prototype.isPrototypeOf.call(_typePrototype(path), value)) return true; - if (typeof value.__handle !== 'number') return false; - try { - return _invoke({ assembly: '', typeName: 'NativeScriptBridge.Bridge', method: 'IsInstanceOf', args: [{ __handle: value.__handle }, path] }) === true; - } catch (_) { - return false; - } - } - - // ── WinRT classes as JS base classes ──────────────────────────────────────── - // `class FixedPanel extends Microsoft.UI.Xaml.Controls.Panel { MeasureOverride(size) { ... } }`: - // the WinRT constructor hands `super(...)` here. The instance is a composed object created by - // the .NET bridge from the C#/WinRT projection of the class (the way a C# subclass is), so XAML - // calling an overridable member (MeasureOverride, ArrangeOverride, OnApplyTemplate, ...) lands in - // the JS override. The JS object is the ordinary WinRT wrapper of that object, with the subclass - // prototype spliced in front of the class's native members. - var _linkedWinRTPrototypes = new WeakSet(); - - // The WinRT class a JS class derives from: the nearest constructor with its own __typeName__. - function _winrtBaseOf(ctor) { - for (var c = ctor; c; c = Object.getPrototypeOf(c)) { - if (Object.prototype.hasOwnProperty.call(c, '__typeName__')) return c; - } - return null; - } - - // `super.X(...)` for a member a JS subclass of a WinRT class overrides: the base implementation, - // through the bridge (the native member would dispatch back into the override). - function _winrtBaseCall(handle, prop) { - return function () { - var args = Array.prototype.slice.call(arguments).map(_unwrap); - return _wrap(_invoke({ handle: handle, method: _baseCallPrefix + prop, args: args })); - }; - } - - // Puts a layer under the JS subclass prototypes that resolves the class's native members and - // sends `super.X()` for overridden members to the base implementation. On the classic engine the - // class's `prototype` is separate from its instances' native prototype, so the layer goes under the - // class's `prototype` once; on the napi engines they're the same object, so it goes under the - // subclass prototype that sits directly on it. - function _linkWinRTSubclassPrototype(base, nativeProto, proto) { - if (!base || !nativeProto) return; - var target = base.prototype; - if (target === nativeProto) { - target = proto; - while (target && Object.getPrototypeOf(target) !== nativeProto) target = Object.getPrototypeOf(target); - } - if (!target || _linkedWinRTPrototypes.has(target)) return; - _linkedWinRTPrototypes.add(target); - var layer = new Proxy(Object.create(nativeProto), { - get: function (_, prop, receiver) { - var handle = _ownHandle(receiver); - if (typeof prop !== 'string') return Reflect.get(nativeProto, prop, receiver); - if (handle >= 0) { - if (receiver.__nsOverrides && receiver.__nsOverrides.has(prop)) return _winrtBaseCall(handle, prop); - return Reflect.get(nativeProto, prop, receiver); - } - // Read off a prototype — TypeScript's ES5 `_super.prototype.X.call(this, ...)`: decide - // between the base implementation and the native member when it's called. - var native = Reflect.get(nativeProto, prop, receiver); - if (typeof native !== 'function') return native; - return function () { - var h = _ownHandle(this); - if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(prop)) - return _winrtBaseCall(h, prop).apply(this, arguments); - return native.apply(this, arguments); - }; - }, - has: function (_, prop) { return prop in nativeProto; }, - }); - Object.setPrototypeOf(target, layer); - } - - // napi engines: the class `prototype` is the native prototype itself, so TypeScript's ES5 - // `_super.prototype.X.call(this, ...)` reaches the native member directly. Wrap each native member a - // subclass overrides (once) so that, called on a JS subclass instance, it runs the base - // implementation instead of dispatching back into the override. - function _guardNativeMembers(nativeProto, overrides) { - overrides.forEach(function (name) { - for (var p = nativeProto; p; p = Object.getPrototypeOf(p)) { - var d = Object.getOwnPropertyDescriptor(p, name); - if (!d) continue; - if (typeof d.value === 'function' && !d.value.__nsGuarded && d.writable) { - var native = d.value; - var guarded = function () { - var h = _ownHandle(this); - if (h >= 0 && this.__nsOverrides && this.__nsOverrides.has(name)) - return _winrtBaseCall(h, name).apply(this, arguments); - return native.apply(this, arguments); - }; - Object.defineProperty(guarded, '__nsGuarded', { value: true }); - Object.defineProperty(p, name, { value: guarded, writable: true, configurable: true, enumerable: d.enumerable }); - } - return; - } - }); - } - - // Called by the WinRT class constructors for `super(...)` from a JS subclass (newTarget) or - // TypeScript's ES5 `_super.call(this, ...)` (es5This). Returns undefined when this isn't a subclass - // construction or the .NET bridge isn't available; the constructor then builds a plain instance. - globalThis.__nsConstructWinRTSubclass = function (typeName, newTarget, args, es5This) { - var proto; - var base; - if (newTarget) { - proto = newTarget.prototype; - base = _winrtBaseOf(Object.getPrototypeOf(newTarget)); - } else { - proto = es5This && Object.getPrototypeOf(es5This); - base = proto && proto.constructor ? _winrtBaseOf(Object.getPrototypeOf(proto.constructor)) : null; - if (!base || base.__typeName__ !== typeName || Object.prototype.hasOwnProperty.call(proto.constructor, '__typeName__')) - return undefined; - } - if (typeof globalThis.__nsDotNetCreateJsSubclass !== 'function' || typeof globalThis.__nsWrapWinRTPointer !== 'function') - return undefined; - var baseProto = base ? base.prototype : null; - var info = _subclassInfo(proto, baseProto); - var entry = { proto: proto, baseProto: baseProto, typeName: typeName, overrides: info.overrides, winrt: true, constructing: true }; - var raw = globalThis.__nsDotNetCreateJsSubclass('', typeName, info.interfaces, info.members, _makeDispatcher(entry), (args || []).map(_unwrap)); - entry.constructing = false; - if (!raw || raw.__native_ptr === undefined || raw.__native_ptr === null) - throw new Error('Could not create a WinRT object for a subclass of ' + typeName); - var self = globalThis.__nsWrapWinRTPointer(raw.__native_ptr, typeName); - var nativeProto = Object.getPrototypeOf(self); - _linkWinRTSubclassPrototype(base, nativeProto, proto); - if (base && base.prototype === nativeProto) _guardNativeMembers(nativeProto, info.overrides); - Object.setPrototypeOf(self, proto); - return _adopt(entry, self, raw); - }; - - globalThis.NSWinRT.extendClass = _extendClass; - - // System.Diagnostics.Stopwatch.StartNew() → static method call - // System.Environment.MachineName → static property get - // new System.Text.StringBuilder(64) → constructor - // sw.Stop() → instance method - // sw.Elapsed → instance property - function _makeNamespaceProxy(path) { - var cached = _nsProxyCache[path]; - if (cached) return cached; - // Static method functions and child namespace/type proxies by name, made on first access. - var members = Object.create(null); - // Plain JS fields assigned to the namespace or type, made on first assignment. - var fields = null; - function _node() {} - var proxy = new Proxy(_node, { - get: function (_, prop) { - if (prop === Symbol.hasInstance) return function (value) { return _hasInstance(path, value); }; - if (typeof prop === 'symbol') return undefined; - // Prevent string-coercion methods from descending into sub-proxies. - // V8's console.log / JSON.stringify call toString/valueOf on unknown objects; - // returning a sub-proxy causes the apply trap to fire with a non-existent - // type name, producing a spurious "Type not found" bridge error. - if (prop === 'toString' || prop === 'valueOf') - return function() { return '[.NET ' + path + ']'; }; - var extras = _namespaceExtras[path]; - if (extras && Object.prototype.hasOwnProperty.call(extras, prop)) return extras[prop]; - if (fields !== null && prop in fields) return fields[prop]; - var member = members[prop]; - if (member !== undefined) return member; - if (prop === '__dotnetType__') return path; - if (prop === 'prototype') return _typePrototype(path); - // Not a WinRT class: TypeScript's __extends must take the standard path. - if (prop === '__nsWinRTClass__' || prop === 'then') return undefined; - var assembly = _resolveAssembly(path); - var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; - // Write-only static property: reading it is an error. - if (kind & _K_SWO) - throw new TypeError('Cannot read write-only property \'' + prop + '\' of .NET type \'' + path + '\''); - // Readable static property: resolve value immediately. - if (kind & _K_SPROP) - return _wrap(_invoke({ assembly: assembly, typeName: path, method: _getterName(prop), args: [] })); - // Static method: a function, made once. - if (kind & _K_SMETHOD) { - return members[prop] = function () { - return _wrap(_invokeWith(-1, path, _resolveAssembly(path), prop, arguments)); - }; - } - if (prop === 'extend') return function (nameOrMembers, maybeMembers) { return _extendClass(this, nameOrMembers, maybeMembers); }; - if (prop === 'call') return function (thisArg) { return _es5SuperCall(path, thisArg, Array.prototype.slice.call(arguments, 1)); }; - if (prop === 'apply') return function (thisArg, args) { return _es5SuperCall(path, thisArg, args ? Array.prototype.slice.call(args) : []); }; - // Namespace / sub-type: keep descending. - return members[prop] = _makeNamespaceProxy(path + '.' + prop); - }, - set: function (_, prop, value) { - if (typeof prop === 'symbol') return true; - var assembly = _resolveAssembly(path); - var kind = _kindsOf(_getTypeInfo(assembly, path))[prop] | 0; - // Read-only static property: assignment is an error. - if (kind & _K_SRO) - throw new TypeError('Cannot assign to read-only property \'' + prop + '\' of .NET type \'' + path + '\''); - // Writable (read-write or write-only): invoke the setter. - if (kind & (_K_SPROP | _K_SWO)) { - _invoke({ assembly: assembly, typeName: path, method: _setterName(prop), args: [_unwrap(value)] }); - return true; - } - // Not a native property: a plain JS field. - (fields || (fields = Object.create(null)))[prop] = value; - return true; - }, - apply: function (_, _this, args) { - var lastDot = path.lastIndexOf('.'); - // No dot means this is a top-level name being called directly — - // there is no type+method pair to dispatch, so bail out. - if (lastDot <= 0) return undefined; - var typeName = path.substring(0, lastDot); - var method = path.substring(lastDot + 1); - var assembly = _resolveAssembly(typeName); - return _wrap(_invoke({ assembly: assembly, typeName: typeName, method: method, args: args.map(_unwrap) })); - }, - construct: function (_, args, newTarget) { - // `super(...)` from a JS subclass. - if (newTarget && newTarget !== proxy) return _constructSubclass(path, args, newTarget.prototype); - if (args.length === 1 && args[0] && typeof args[0] === 'object' && typeof args[0].__handle !== 'number' - && _typeKind(path) === 'interface') - return _implementInterface(path, args[0]); - var assembly = _resolveAssembly(path); - return _wrap(_invoke({ assembly: assembly, typeName: path, method: '.ctor', args: args.map(_unwrap) })); - }, - }); - _nsProxyCache[path] = proxy; - return proxy; - } - // `System` stays the .NET namespace when app code assigns it: @nativescript/core sets - // `global.System = { import() }` (a SystemJS shim). Assigned members are kept and take - // precedence (`System.import(...)` keeps working); everything else is .NET. - (function () { - var systemProxy = _makeNamespaceProxy('System'); - Object.defineProperty(globalThis, 'System', { - configurable: true, - enumerable: true, - get: function () { return systemProxy; }, - set: function (value) { - if (value && (typeof value === 'object' || typeof value === 'function')) - Object.keys(value).forEach(function (k) { _namespaceExtras.System[k] = value[k]; }); - }, - }); - })(); - globalThis.Microsoft = _makeNamespaceProxy('Microsoft'); - globalThis.NativeScript = _makeNamespaceProxy('NativeScript'); - - // Every other root namespace of the app's assemblies (C# sources compiled into the app, - // plugin and NuGet assemblies) is a lazy global too, so `new MyCompany.Native.Foo()` works - // with no registration step, the way Java packages and Objective-C classes are globals on - // Android and iOS. WinRT owns Windows/Microsoft/NativeScript; the rest are implementation - // namespaces of the toolchain that no app reaches for. - (function () { - var reserved = { Windows: 1, Microsoft: 1, NativeScript: 1, System: 1, NativeScriptBridge: 1, NSWinRTDynamicProxies: 1, - WinRT: 1, ABI: 1, XamlGeneratedNamespace: 1, FxResources: 1, Internal: 1, Interop: 1, MS: 1, Mono: 1 }; - for (var ns in _namespaceAssemblyMap) { - if (ns.indexOf('.') !== -1 || reserved[ns] === 1 || !/^[A-Za-z_$][\w$]*$/.test(ns)) continue; - globalThis.NSWinRT.dotnet.registerNamespace(ns); - } - })(); - - // Creates a typed .NET BCL delegate (not a WinRT COM delegate). - // Use this when the API expects a managed System.Action, - // System.EventHandler, etc. rather than a WinRT delegate interface. - // For WinRT delegate types use NSWinRT.asDelegate instead. - if (typeof globalThis.__nsDotNetAwaitTask === 'function') { - globalThis.NSWinRT.dotnet.taskToPromise = function (obj) { - var h = obj && typeof obj.__handle === 'number' && obj.__isTask === true ? obj.__handle : -1; - if (h < 0) return Promise.resolve(obj); - return new Promise(function (resolve, reject) { - globalThis.__nsDotNetAwaitTask(h, function (value) { resolve(_wrap(value)); }, reject); - }); - }; - } - - if (typeof globalThis.__nsDotNetCreateDelegate === 'function') { - globalThis.NSWinRT.dotnet.asDelegate = function(typeNameOrFn, fn) { - var typeName, callback; - if (typeof typeNameOrFn === 'function') { - typeName = ''; - callback = typeNameOrFn; - } else { - typeName = typeNameOrFn || ''; - callback = fn; - } - if (typeof callback !== 'function') - throw new TypeError('NSWinRT.dotnet.asDelegate: callback must be a function'); - var wrapped = function() { - var args = Array.prototype.slice.call(arguments).map(_wrap); - return callback.apply(null, args); - }; - return globalThis.__nsDotNetCreateDelegate(typeName, wrapped); - }; - } - - if (typeof globalThis.__nsRunOnUIThread === 'function') { - globalThis.NSWinRT.runOnUIThread = function(fn) { - if (typeof fn !== 'function') throw new TypeError('NSWinRT.runOnUIThread: expected a function'); - return globalThis.__nsRunOnUIThread(fn); - }; - } -})(); -"#; +/// The JS half of `NSWinRT.dotnet` and the managed-subclass system, shared with the classic +/// engine (`runtime/src/js`). A no-op if `__nsDotNetInvokeBin` is absent. +const DOTNET_HELPERS_JS: &str = concat!( + "'use strict';\n(function () {\nglobalThis.NSWinRT = globalThis.NSWinRT || {};\n", + include_str!("../js/dotnet_subclass.js"), + "})();\n", + include_str!("../js/dotnet.js"), +); diff --git a/test-app/App_Resources/Windows/src/TestFixtures/Interop.cs b/test-app/App_Resources/Windows/src/TestFixtures/Interop.cs new file mode 100644 index 0000000..a23068c --- /dev/null +++ b/test-app/App_Resources/Windows/src/TestFixtures/Interop.cs @@ -0,0 +1,247 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +// Fixtures for tests/csharp-interop.js. +namespace TestFixtures.Interop +{ + public enum Tint + { + Red = 1, + Green = 2, + Blue = 4, + } + + [Flags] + public enum Access + { + None = 0, + Read = 1, + Write = 2, + } + + public class Fields + { + public const string Label = "fields"; + public static readonly int Answer = 42; + public static int Counter; + public int Value = 7; + public readonly string Id = "id-1"; + public Tint Tint = Tint.Green; + + public int Twice() => Value * 2; + } + + public class Outer + { + public class Inner + { + public static string Where() => "inner"; + + public string Hello() => "hi"; + } + + public enum Kind + { + First, + Second, + } + } + + public static class Numbers + { + public static long Big() => 9007199254740993L; + public static long Small() => 42L; + public static ulong Huge() => ulong.MaxValue; + public static long Echo(long value) => value; + public static ulong EchoUnsigned(ulong value) => value; + public static string Describe(int value) => "int:" + value; + public static string Describe(long value) => "long:" + value; + public static string Text(long value) => value.ToString(); + + public static byte[] Bytes() => new byte[] { 1, 2, 255 }; + public static int Sum(byte[] data) => data.Sum(b => (int)b); + public static int SumInts(int[] data) => data.Sum(); + public static double SumDoubles(double[] data) => data.Sum(); + public static int Length(ReadOnlyMemory data) => data.Length; + public static long StreamLength(Stream stream) => stream.Length; + public static int[] Ints() => new[] { 1, 2, 3 }; + } + + public static class Params + { + public static string Optional(string a, string b = "B", int c = 3) => $"{a}{b}{c}"; + + public static string Tinted(Tint tint = Tint.Blue) => tint.ToString(); + + public static bool TryDouble(string text, out int result) + { + if (int.TryParse(text, out var value)) + { + result = value * 2; + return true; + } + result = 0; + return false; + } + + public static void Swap(ref int a, ref int b) => (a, b) = (b, a); + + public static void Split(string full, out string first, out string last) + { + var parts = full.Split(' '); + first = parts[0]; + last = parts.Length > 1 ? parts[1] : ""; + } + + public static string Point(int x = 0, int y = 0, string label = "p") => $"{label}({x},{y})"; + } + + public class Sized + { + public Sized(string name, int size = 10) + { + Name = name; + Size = size; + } + + public string Name { get; } + public int Size { get; } + } + + public static class Generics + { + public static T Identity(T value) => value; + public static string NameOf() => typeof(T).Name; + public static int CountOf(IEnumerable items) => items.Count(); + public static List Repeat(T value, int times) => Enumerable.Repeat(value, times).ToList(); + public static string Describe(List list) => typeof(T).Name + ":" + list.Count; + } + + public class Box + { + public Box(T value) => Value = value; + + public T Value { get; set; } + + public string TypeArgument => typeof(T).Name; + } + + public class Registry + { + public static Registry Shared { get; } = new Registry(); + + public Registry Self() => this; + + public List Names { get; } = new List { "a" }; + + public Dictionary Scores { get; } = new Dictionary { ["alice"] = 3 }; + + public Dictionary Bag() => new Dictionary { ["n"] = 1, ["s"] = "two" }; + } + + public class Talker + { + public event EventHandler? Said; + public static event Action? Ticked; + + public int Say(string message) + { + Said?.Invoke(this, message); + return Handlers; + } + + public int Handlers => Said?.GetInvocationList().Length ?? 0; + + public static int Tick(int n) + { + Ticked?.Invoke(n); + return Ticked?.GetInvocationList().Length ?? 0; + } + } + + public static class Failing + { + public static void Throw(string message) => throw new InvalidOperationException(message); + + public static void ThrowWithInner() => throw new InvalidOperationException("outer", new ArgumentException("inner")); + + public static async Task FailAsync(string message) + { + await Task.Delay(5).ConfigureAwait(false); + throw new FormatException(message); + } + + // What a JS exception looks like to the C# code that called the callback. + public static string Catch(Func callback) + { + try + { + callback(); + return "none"; + } + catch (Exception e) + { + return e.Message + "|" + (e.StackTrace ?? ""); + } + } + + public static string CatchVoid(Action callback) + { + try + { + callback(); + return "none"; + } + catch (Exception e) + { + return e.Message; + } + } + } + + public static class Extensions + { + public static string Shout(this string text) => text.ToUpperInvariant() + "!"; + + public static int Thrice(this Fields fields) => fields.Value * 3; + } + + public abstract class Shape + { + protected virtual string Kind => "shape"; + + protected int Sides { get; set; } + + protected string Describe(int n) => "n" + n; + + protected string Describe(string s) => "s" + s; + + public string Report() => Kind + ":" + Sides; + + public virtual T Convert(object value) => (T)value; + + public string UseConvert() => Convert("x"); + + public abstract string Draw(); + } + + public static class Values + { + public static (int, string) Pair() => (1, "one"); + + public static Dimensions Make(double width, double height) => new Dimensions { Width = width, Height = height }; + + public static double Area(Dimensions d) => d.Width * d.Height; + } + + public struct Dimensions + { + public double Width; + public double Height; + + public double Area => Width * Height; + } +} diff --git a/test-app/app/main.js b/test-app/app/main.js index e56d8cd..5b4b42b 100644 --- a/test-app/app/main.js +++ b/test-app/app/main.js @@ -14,6 +14,7 @@ Object.assign(globalThis, api); const specs = [ "./tests/csharp-sources.js", + "./tests/csharp-interop.js", "./tests/extend-dotnet.js", "./tests/extend-winrt.js", "./tests/threading.js", diff --git a/test-app/app/tests/csharp-interop.js b/test-app/app/tests/csharp-interop.js new file mode 100644 index 0000000..a955ab8 --- /dev/null +++ b/test-app/app/tests/csharp-interop.js @@ -0,0 +1,354 @@ +// C# interop: fields, nested types, longs and binary data, parameters, generics, extension methods, +// collections, events, exceptions, object identity and JS classes created by .NET code. +// Fixtures: App_Resources/Windows/src/TestFixtures/Interop.cs. + +const I = () => TestFixtures.Interop; + +describe("C# fields, constants and enums", () => { + it("reads constants and static readonly fields", () => { + expect(I().Fields.Label).toBe("fields"); + expect(I().Fields.Answer).toBe(42); + }); + + it("reads and writes static fields", () => { + I().Fields.Counter = 5; + expect(I().Fields.Counter).toBe(5); + }); + + it("reads and writes instance fields", () => { + const f = new (I().Fields)(); + expect(f.Value).toBe(7); + f.Value = 21; + expect(f.Value).toBe(21); + expect(f.Twice()).toBe(42); + }); + + it("refuses to assign read-only fields", () => { + const f = new (I().Fields)(); + expect(f.Id).toBe("id-1"); + expect(() => (f.Id = "x")).toThrow(); + expect(() => (I().Fields.Answer = 1)).toThrow(); + }); + + it("exposes enum members as their values", () => { + expect(I().Tint.Red).toBe(1); + expect(I().Tint.Blue).toBe(4); + expect(I().Access.Read | I().Access.Write).toBe(3); + expect(new (I().Fields)().Tint).toBe(I().Tint.Green); + }); + + it("reads value tuple items and struct fields", () => { + const pair = I().Values.Pair(); + expect(pair.Item1).toBe(1); + expect(pair.Item2).toBe("one"); + const d = I().Values.Make(3, 4); + expect(d.Width).toBe(3); + expect(d.Area).toBe(12); + }); +}); + +describe("C# nested types", () => { + it("reaches nested classes and enums through their outer type", () => { + expect(I().Outer.Inner.Where()).toBe("inner"); + expect(new (I().Outer.Inner)().Hello()).toBe("hi"); + expect(I().Outer.Kind.Second).toBe(1); + }); +}); + +describe("C# long values and binary data", () => { + it("returns longs that fit a double as numbers", () => { + expect(I().Numbers.Small()).toBe(42); + }); + + it("returns larger longs as BigInt, without losing digits", () => { + expect(I().Numbers.Big()).toBe(9007199254740993n); + expect(I().Numbers.Huge()).toBe(18446744073709551615n); + }); + + it("passes BigInt arguments as longs", () => { + expect(I().Numbers.Echo(9007199254740993n)).toBe(9007199254740993n); + expect(I().Numbers.Text(-9007199254740993n)).toBe("-9007199254740993"); + expect(I().Numbers.EchoUnsigned(18446744073709551615n)).toBe(18446744073709551615n); + expect(I().Numbers.Describe(5n)).toBe("long:5"); + expect(I().Numbers.Describe(5)).toBe("int:5"); + }); + + it("returns byte[] as a Uint8Array", () => { + const bytes = I().Numbers.Bytes(); + expect(bytes instanceof Uint8Array).toBe(true); + expect(Array.from(bytes)).toEqual([1, 2, 255]); + }); + + it("keeps other arrays as JS arrays", () => { + expect(I().Numbers.Ints()).toEqual([1, 2, 3]); + }); + + it("passes typed arrays, ArrayBuffers and DataViews as arrays", () => { + expect(I().Numbers.Sum(new Uint8Array([1, 2, 3]))).toBe(6); + expect(I().Numbers.Sum(new Uint8Array([4, 5]).buffer)).toBe(9); + expect(I().Numbers.Sum(new DataView(new Uint8Array([7]).buffer))).toBe(7); + expect(I().Numbers.SumInts(new Int32Array([100000, 200000]))).toBe(300000); + expect(I().Numbers.SumDoubles(new Float64Array([0.5, 0.25]))).toBe(0.75); + }); + + it("passes bytes as Memory and Stream", () => { + expect(I().Numbers.Length(new Uint8Array(5))).toBe(5); + expect(I().Numbers.StreamLength(new Uint8Array(3))).toBe(3); + }); +}); + +describe("C# parameters", () => { + it("fills in optional parameters", () => { + expect(I().Params.Optional("a")).toBe("aB3"); + expect(I().Params.Optional("a", "x")).toBe("ax3"); + expect(I().Params.Tinted()).toBe("Blue"); + }); + + it("passes arguments by name", () => { + expect(I().Params.Point(NSWinRT.dotnet.named({ y: 2, label: "q" }))).toBe("q(0,2)"); + expect(I().Params.Point(1, NSWinRT.dotnet.named({ label: "r" }))).toBe("r(1,0)"); + }); + + it("fills in optional constructor parameters", () => { + const s = new (I().Sized)("n"); + expect(s.Size).toBe(10); + expect(new (I().Sized)("n", NSWinRT.dotnet.named({ size: 2 })).Size).toBe(2); + }); + + it("returns out parameters that are left off with the result", () => { + expect(I().Params.TryDouble("21")).toEqual([true, 42]); + expect(I().Params.TryDouble("x")).toEqual([false, 0]); + expect(I().Params.Split("Ada Lovelace")).toEqual(["Ada", "Lovelace"]); + }); + + it("fills in out and ref references", () => { + const result = NSWinRT.interop.out(); + expect(I().Params.TryDouble("5", result)).toBe(true); + expect(result.value).toBe(10); + + const a = NSWinRT.interop.out(1); + const b = NSWinRT.interop.out(2); + I().Params.Swap(a, b); + expect([a.value, b.value]).toEqual([2, 1]); + }); + + it("reaches BCL methods with out parameters", () => { + const parsed = NSWinRT.interop.out(); + expect(System.Int32.TryParse("123", parsed)).toBe(true); + expect(parsed.value).toBe(123); + }); +}); + +describe("C# generics", () => { + it("constructs closed generic types with .of()", () => { + const List = System.Collections.Generic.List.of(System.String); + const list = new List(); + list.Add("a"); + list.Add("b"); + expect(list.Count).toBe(2); + expect(I().Generics.Describe(list)).toBe("String:2"); + }); + + it("takes JS constructors as type arguments", () => { + const Dict = System.Collections.Generic.Dictionary.of(String, Number); + const d = new Dict(); + d.Add("x", 1.5); + expect(d.get_Item("x")).toBe(1.5); + }); + + it("constructs app generic types", () => { + const box = new (I().Box.of(System.Int32))(5); + expect(box.Value).toBe(5); + expect(box.TypeArgument).toBe("Int32"); + }); + + it("infers generic method type arguments from the arguments", () => { + expect(I().Generics.Identity("same")).toBe("same"); + const list = I().Generics.Repeat("z", 3); + expect(list.Count).toBe(3); + expect(I().Generics.CountOf(list)).toBe(3); + expect(I().Generics.CountOf([1, 2, 3, 4])).toBe(4); + }); + + it("calls generic methods with explicit type arguments", () => { + expect(I().Generics.NameOf.of(System.DateTime)()).toBe("DateTime"); + expect(System.Linq.Enumerable.Empty.of(System.String)().length).toBe(0); + }); +}); + +describe("C# extension methods", () => { + it("calls extension methods on instances", () => { + const f = new (I().Fields)(); + expect(f.Thrice()).toBe(21); + }); + + it("calls LINQ on .NET collections", () => { + const list = I().Generics.Repeat(2, 3); + expect(list.Sum()).toBe(6); + expect(list.Select((x) => x * 10).ToList().get_Item(1)).toBe(20); + expect(list.Where((x) => x > 1).Count()).toBe(3); + }); +}); + +describe("C# collections", () => { + it("reads and writes string-keyed dictionaries by key", () => { + const scores = I().Registry.Shared.Scores; + expect(scores.alice).toBe(3); + scores.bob = 5; + expect(scores["bob"]).toBe(5); + expect(scores.Count).toBe(2); + expect(scores.ContainsKey("bob")).toBe(true); + }); +}); + +describe("C# events", () => { + it("subscribes and unsubscribes JS functions with add_/remove_", () => { + const t = new (I().Talker)(); + const seen = []; + const handler = (sender, message) => seen.push(message); + t.add_Said(handler); + expect(t.Say("one")).toBe(1); + t.remove_Said(handler); + expect(t.Say("two")).toBe(0); + expect(seen).toEqual(["one"]); + }); + + it("replaces the handler assigned to an event", () => { + const t = new (I().Talker)(); + const seen = []; + t.Said = (sender, m) => seen.push("a:" + m); + t.Said = (sender, m) => seen.push("b:" + m); + expect(t.Say("x")).toBe(1); + expect(typeof t.Said).toBe("function"); + t.Said = null; + expect(t.Handlers).toBe(0); + expect(seen).toEqual(["b:x"]); + }); + + it("handles static events", () => { + const seen = []; + const handler = (n) => seen.push(n); + I().Talker.add_Ticked(handler); + expect(I().Talker.Tick(3)).toBe(1); + I().Talker.remove_Ticked(handler); + expect(I().Talker.Tick(4)).toBe(0); + I().Talker.Ticked = (n) => seen.push(-n); + I().Talker.Tick(5); + I().Talker.Ticked = null; + expect(seen).toEqual([3, -5]); + }); + + it("passes the sender as the same JS object", () => { + const t = new (I().Talker)(); + let sender = null; + t.Said = (s) => (sender = s); + t.Say("x"); + expect(sender).toBe(t); + t.Said = null; + }); +}); + +describe("C# exceptions", () => { + it("throws a JS Error with the .NET exception", () => { + let error = null; + try { + I().Failing.Throw("bad state"); + } catch (e) { + error = e; + } + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("bad state"); + expect(error.dotnetType).toBe("System.InvalidOperationException"); + expect(error.nativeException.Message).toBe("bad state"); + expect(error.stack).toContain("Failing.Throw"); + }); + + it("exposes inner exceptions", () => { + let error = null; + try { + I().Failing.ThrowWithInner(); + } catch (e) { + error = e; + } + expect(error.nativeException.InnerException.Message).toBe("inner"); + }); + + it("rejects faulted Tasks with an Error", async () => { + let error = null; + try { + await I().Failing.FailAsync("late"); + } catch (e) { + error = e; + } + expect(error instanceof Error).toBe(true); + expect(error.message).toBe("late"); + expect(error.dotnetType).toBe("System.FormatException"); + }); + + it("hands JS errors to the C# caller with their stack", () => { + const outcome = I().Failing.Catch(function failingCallback() { + throw new Error("from js"); + }); + expect(outcome).toContain("from js"); + expect(outcome).toContain("failingCallback"); + }); + + it("throws JS errors into C# from void callbacks too", () => { + expect(I().Failing.CatchVoid(() => { + throw new Error("void js"); + })).toContain("void js"); + }); +}); + +describe("C# objects in JS", () => { + it("returns the same JS object for the same .NET object", () => { + const r = I().Registry.Shared; + expect(r.Self()).toBe(r); + expect(I().Registry.Shared).toBe(r); + expect(r.Names).toBe(r.Names); + }); + + it("converts to strings with ToString()", () => { + const b = new System.Text.StringBuilder("abc"); + expect(String(b)).toBe("abc"); + expect(`${new (I().Fields)()}`).toBe("TestFixtures.Interop.Fields"); + }); +}); + +describe("C# subclasses", () => { + it("reach protected members of the base class", () => { + class Square extends I().Shape { + get Kind() { + return "square"; + } + Draw() { + this.Sides = 4; + return this.Describe(2) + "," + this.Describe("x"); + } + } + const s = new Square(); + expect(s.Draw()).toBe("n2,sx"); + expect(s.Report()).toBe("square:4"); + }); + + it("override generic virtual methods", () => { + class Loud extends I().Shape { + Draw() { + return ""; + } + Convert(value) { + return value + "!"; + } + } + expect(new Loud().UseConvert()).toBe("x!"); + }); +}); + +describe("Loading assemblies", () => { + it("loads an assembly at runtime and makes its types reachable", () => { + const dir = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory(); + const namespaces = NSWinRT.dotnet.loadAssembly(System.IO.Path.Combine(dir, "System.Web.HttpUtility.dll")); + expect(namespaces).toContain("System.Web"); + expect(System.Web.HttpUtility.UrlEncode("a b")).toBe("a+b"); + }); +}); From 3d5c35f9d7e502e144b05a171bc82c84d89858f1 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 23 Oct 2026 10:56:13 -0400 Subject: [PATCH 04/10] perf(dotnet): faster bridge startup, first calls and calls into JS - The namespace map JS asks for at startup is read from assembly metadata instead of loading every DLL of the app and listing its types: the first bridge call of the test app went from ~505 ms to ~124 ms (CLR start included). Paths the map lists as namespaces no longer ask the bridge for type members (each miss scanned every assembly). - Methods start on MethodInvoker and get a compiled delegate after 32 calls: a method's first call went from ~80 us (Expression.Compile) to ~0.3 us; warm calls cost 1-2 ns more. - Calls from .NET into JS reuse a per-thread request buffer and write the instance and member of an overridden member directly: 200/176/424 B -> 80/56/80 B per delegate / void delegate / override call, overrides 233 -> 167 ns (dotnet-bridge-benchmarks CallbackBenchmarks). - The app build republishes the bridge when any of its sources changes, not only Bridge.cs. --- dotnet-bridge-benchmarks/BridgeBenchmarks.cs | 2 +- .../CallbackBenchmarks.cs | 69 ++++++++++++++ dotnet-bridge-benchmarks/EntryBenchmarks.cs | 36 +++++++ dotnet-bridge/Bridge.Dispatch.cs | 42 ++++++-- dotnet-bridge/Bridge.JsDelegate.cs | 95 +++++++++++-------- dotnet-bridge/Bridge.Proxy.cs | 19 +--- dotnet-bridge/Bridge.cs | 71 +++++++------- runtime/src/js/dotnet.js | 5 + .../__PROJECT_NAME__/__PROJECT_NAME__.csproj | 7 +- 9 files changed, 249 insertions(+), 97 deletions(-) create mode 100644 dotnet-bridge-benchmarks/CallbackBenchmarks.cs create mode 100644 dotnet-bridge-benchmarks/EntryBenchmarks.cs diff --git a/dotnet-bridge-benchmarks/BridgeBenchmarks.cs b/dotnet-bridge-benchmarks/BridgeBenchmarks.cs index fcb6dac..4623ca3 100644 --- a/dotnet-bridge-benchmarks/BridgeBenchmarks.cs +++ b/dotnet-bridge-benchmarks/BridgeBenchmarks.cs @@ -7,7 +7,7 @@ using BenchmarkDotNet.Running; using NativeScriptBridge; -BenchmarkSwitcher.FromTypes([typeof(BridgeBenchmarks), typeof(AbiBenchmarks)]).Run(args); +BenchmarkSwitcher.FromTypes([typeof(BridgeBenchmarks), typeof(AbiBenchmarks), typeof(CallbackBenchmarks), typeof(EntryBenchmarks)]).Run(args); /// /// Measures the hot-path dispatch cost for the JSON and binary protocols. diff --git a/dotnet-bridge-benchmarks/CallbackBenchmarks.cs b/dotnet-bridge-benchmarks/CallbackBenchmarks.cs new file mode 100644 index 0000000..1402acc --- /dev/null +++ b/dotnet-bridge-benchmarks/CallbackBenchmarks.cs @@ -0,0 +1,69 @@ +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using BenchmarkDotNet.Attributes; +using NativeScriptBridge; + +public class CallbackBase +{ + public virtual int Compute(int value) => value; + + public int Run(int value) => Compute(value); +} + +/// +/// Managed cost of .NET calling into JS: a JS function passed as a delegate, and a JS subclass +/// override called from C#. The "JS side" is an unmanaged function that answers at once, so +/// what's measured is the bridge's own work (argument encoding, response handling). +/// +/// dotnet run -c Release -- --filter *CallbackBenchmarks* --job short +/// +[MemoryDiagnoser] +public unsafe class CallbackBenchmarks +{ + private Func _func = null!; + private Action _action = null!; + private CallbackBase _override = null!; + + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void FakeJs(int id, byte* args, int len, byte** resp, int* respLen) + { + // Every callback answers with the int 42, allocated the way the runtime allocates it. + var p = (byte*)Marshal.AllocHGlobal(5); + p[0] = 0x03; + *(int*)(p + 1) = 42; + *resp = p; + *respLen = 5; + } + + [GlobalSetup] + public void Setup() + { + Bridge.ClearCaches(); + Bridge.s_jsInvoker = &FakeJs; + _func = (Func)Bridge.MakeJsDelegate(typeof(Func), 1); + _action = (Action)Bridge.MakeJsDelegate(typeof(Action), 2); + + var buf = new ArrayBufferWriter(64); + var w = new BinWriter(buf); + w.WriteByte(0x0A); + w.WriteString16(""); + w.WriteString16(typeof(CallbackBase).FullName!); + w.WriteI32(0); + w.WriteI32(1); + w.WriteString16("Compute"); + w.WriteI32(3); + w.WriteByte(0); + var r = new BinReader(buf.WrittenSpan); + var handle = Bridge.DispatchBin(ref r).HandleId(); + _override = (CallbackBase)Bridge.s_handles[handle]!; + if (_override.Run(1) != 42) throw new InvalidOperationException("override did not reach the fake JS side"); + } + + [Benchmark] public int DelegateWithResult() => _func(5); + + [Benchmark] public void VoidDelegate() => _action("hello"); + + [Benchmark] public int OverrideFromCSharp() => _override.Run(5); +} diff --git a/dotnet-bridge-benchmarks/EntryBenchmarks.cs b/dotnet-bridge-benchmarks/EntryBenchmarks.cs new file mode 100644 index 0000000..25a9494 --- /dev/null +++ b/dotnet-bridge-benchmarks/EntryBenchmarks.cs @@ -0,0 +1,36 @@ +using System; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using NativeScriptBridge; + +/// +/// What a method costs the first time JS calls it (building its invoker) and after that. +/// +/// dotnet run -c Release -- --filter *EntryBenchmarks* --job short +/// +[MemoryDiagnoser] +public class EntryBenchmarks +{ + private static readonly MethodInfo s_max = typeof(Math).GetMethod(nameof(Math.Max), [typeof(int), typeof(int)])!; + private static readonly MethodInfo s_append = typeof(System.Text.StringBuilder).GetMethod(nameof(System.Text.StringBuilder.Append), [typeof(string)])!; + private readonly object?[] _maxArgs = [3, 7]; + private readonly System.Text.StringBuilder _sb = new(); + private readonly object?[] _appendArgs = [""]; + private DispatchEntry _warmMax; + private DispatchEntry _warmAppend; + + [GlobalSetup] + public void Setup() + { + _warmMax = Bridge.BuildDispatchEntry(typeof(Math), s_max); + _warmAppend = Bridge.BuildDispatchEntry(typeof(System.Text.StringBuilder), s_append); + } + + [Benchmark] public object? FirstCallStatic() => Bridge.BuildDispatchEntry(typeof(Math), s_max).Invoke!(null, _maxArgs); + + [Benchmark] public object? FirstCallInstance() => Bridge.BuildDispatchEntry(typeof(System.Text.StringBuilder), s_append).Invoke!(_sb, _appendArgs); + + [Benchmark] public object? WarmStatic() => _warmMax.Invoke!(null, _maxArgs); + + [Benchmark] public object? WarmInstance() => _warmAppend.Invoke!(_sb, _appendArgs); +} diff --git a/dotnet-bridge/Bridge.Dispatch.cs b/dotnet-bridge/Bridge.Dispatch.cs index 14ba54f..042a1ee 100644 --- a/dotnet-bridge/Bridge.Dispatch.cs +++ b/dotnet-bridge/Bridge.Dispatch.cs @@ -171,7 +171,41 @@ private static DispatchEntry BuildDispatchEntry(Type type, string name, int argC internal static DispatchEntry BuildDispatchEntry(Type type, MethodInfo mi) { var parameters = mi.GetParameters(); + if (parameters.Any(p => p.ParameterType.IsByRef || p.ParameterType.IsByRefLike) || mi.ReturnType.IsByRefLike) + return new DispatchEntry(CompileInvoker(type, mi, parameters), parameters); + return new DispatchEntry(new TieredInvoker(type, mi, parameters).Invoke, parameters); + } + + // MethodInvoker (cheap to create) until the method is called often enough to pay for a compiled + // delegate (~80 us to build, a few ns per call). + private sealed class TieredInvoker(Type type, MethodInfo mi, ParameterInfo[] parameters) + { + private const int CompileAfter = 32; + private readonly MethodInvoker _invoker = MethodInvoker.Create(mi); + private Func? _compiled; + private int _calls; + + public object? Invoke(object? target, object?[] args) + { + var compiled = _compiled; + if (compiled is not null) return compiled(target, args); + if (Interlocked.Increment(ref _calls) == CompileAfter) + _compiled = CompileInvoker(type, mi, parameters); + try + { + return _invoker.Invoke(target, new Span(args, 0, parameters.Length)); + } + catch (TargetInvocationException tie) when (tie.InnerException is not null) + { + // Throw what the method threw, as the compiled delegate does. + System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(tie.InnerException).Throw(); + throw; + } + } + } + private static Func CompileInvoker(Type type, MethodInfo mi, ParameterInfo[] parameters) + { try { var targetParam = Expression.Parameter(typeof(object), "t"); @@ -191,18 +225,14 @@ internal static DispatchEntry BuildDispatchEntry(Type type, MethodInfo mi) else if (body.Type != typeof(object)) body = Expression.Convert(body, typeof(object)); - var fn = Expression.Lambda>( + return Expression.Lambda>( body, targetParam, argsParam).Compile(); - - return new DispatchEntry(fn, parameters); } catch { // mi.Invoke validates args.Length == paramCount; slice the rented buffer if needed. int pc = parameters.Length; - return new DispatchEntry( - (t, a) => mi.Invoke(t, a.Length == pc ? a : a[..pc]), - parameters); + return (t, a) => mi.Invoke(t, a.Length == pc ? a : a[..pc]); } } diff --git a/dotnet-bridge/Bridge.JsDelegate.cs b/dotnet-bridge/Bridge.JsDelegate.cs index 009ef01..838b296 100644 --- a/dotnet-bridge/Bridge.JsDelegate.cs +++ b/dotnet-bridge/Bridge.JsDelegate.cs @@ -133,21 +133,49 @@ private static DynamicMethod BuildJsDelegateInvoker(Type delegateType) // Calls the JS function `id`. A JS throw becomes a JsException with the JS stack; a callback // that couldn't run (its worker is gone) only throws when a result is expected. - internal static unsafe object? CallJsCallback(int id, object?[] args, bool expectsResult) + internal static object? CallJsCallback(int id, object?[] args, bool expectsResult) => + CallJsCallback(id, -1, null, args, expectsResult); + + // A request buffer per thread; a nested call (JS calling back into .NET) gets its own. + [ThreadStatic] private static ArrayBufferWriter? t_callbackRequest; + [ThreadStatic] private static bool t_callbackRequestInUse; + + // `selfHandle` and `member`, when given, are written first: an override's instance and name. + internal static unsafe object? CallJsCallback(int id, int selfHandle, string? member, object?[] args, bool expectsResult) { if (s_jsInvoker == null) return null; - var buf = new ArrayBufferWriter(64); - var w = new BinWriter(buf); - w.WriteByte((byte)Math.Min(args.Length, 255)); - foreach (var arg in args) - WriteCallbackArg(ref w, arg); - - var bytes = buf.WrittenSpan; + var reuse = !t_callbackRequestInUse; + var buf = reuse ? (t_callbackRequest ??= new ArrayBufferWriter(128)) : new ArrayBufferWriter(64); + if (reuse) + { + buf.ResetWrittenCount(); + t_callbackRequestInUse = true; + } byte* respPtr = null; int respLen = 0; - fixed (byte* p = bytes) - s_jsInvoker(id, p, bytes.Length, &respPtr, &respLen); + try + { + var w = new BinWriter(buf); + var head = member is null ? 0 : 2; + w.WriteByte((byte)Math.Min(args.Length + head, 255)); + if (member is not null) + { + WriteHandleArg(ref w, selfHandle); + w.WriteByte(0x05); + w.WriteString32(member); + } + foreach (var arg in args) + WriteCallbackArg(ref w, arg); + + var bytes = buf.WrittenSpan; + fixed (byte* p = bytes) + s_jsInvoker(id, p, bytes.Length, &respPtr, &respLen); + } + finally + { + if (reuse) t_callbackRequestInUse = false; + } object? result = null; // Response is only set for non-void delegates; parse and free when present. @@ -186,38 +214,29 @@ private static DynamicMethod BuildJsDelegateInvoker(Type delegateType) // Serialises a single delegate argument in response-binary tag format so // the Rust side can reuse its existing bin_read_value parser. + // An existing handle, forwarded as is (tag 0x06, its object's type and native pointer). + private static void WriteHandleArg(ref BinWriter w, int id) + { + w.WriteByte(0x06); + w.WriteI32(id); + var obj = s_handles.TryGetValue(id, out var o) ? o : null; + w.WriteString16(obj is null ? "" : GetBoxInfo(obj.GetType()).TypeName); + if (s_nativePtrs.TryGetValue(id, out var nativePtr)) + { + w.WriteByte(1); + w.WriteI64(nativePtr.ToInt64()); + } + else + { + w.WriteByte(0); + } + } + private static void WriteCallbackArg(ref BinWriter w, object? arg) { - // Allow existing handle references to be forwarded without re-boxing. if (arg is HandleRef hr) { - w.WriteByte(0x06); - w.WriteI32(hr.Id); - try - { - if (s_handles.TryGetValue(hr.Id, out var obj) && obj != null) - { - var objTypeName = obj.GetType().FullName ?? obj.GetType().Name; - w.WriteString16(objTypeName); - } - else - { - w.WriteString16(""); - } - } - catch - { - w.WriteString16(""); - } - if (Bridge.s_nativePtrs.TryGetValue(hr.Id, out var nativePtr)) - { - w.WriteByte(1); - w.WriteI64(nativePtr.ToInt64()); - } - else - { - w.WriteByte(0); - } + WriteHandleArg(ref w, hr.Id); return; } diff --git a/dotnet-bridge/Bridge.Proxy.cs b/dotnet-bridge/Bridge.Proxy.cs index 409af98..3f402bf 100644 --- a/dotnet-bridge/Bridge.Proxy.cs +++ b/dotnet-bridge/Bridge.Proxy.cs @@ -550,39 +550,30 @@ private static ProxyCallbackHolder JsHolderFor(object instance) throw new InvalidOperationException("No JS callback registered for proxy instance."); } - // Payload for the JS dispatcher: (self handle, member name, ...args). The arguments are sent + // The JS dispatcher gets (self handle, member name, ...args). The arguments are sent // individually so each arrives as its JS value (numbers, strings, structs as plain objects, // objects as handles). - private static object?[] DispatchPayload(object instance, ProxyCallbackHolder holder, string memberName, object?[] args) - { - var payload = new object?[args.Length + 2]; - payload[0] = new HandleRef(EnsureJsHandle(instance, holder)); - payload[1] = memberName; - Array.Copy(args, 0, payload, 2, args.Length); - return payload; - } - private static object? ProxyInvokeMethod(object instance, string methodName, object[] args) { var holder = JsHolderFor(instance); - return CallJsCallback(holder.CallbackId, DispatchPayload(instance, holder, methodName, args), expectsResult: true); + return CallJsCallback(holder.CallbackId, EnsureJsHandle(instance, holder), methodName, args, expectsResult: true); } private static void ProxyInvokeVoid(object instance, string methodName, object[] args) { var holder = JsHolderFor(instance); - CallJsCallback(holder.CallbackId, DispatchPayload(instance, holder, methodName, args), expectsResult: false); + CallJsCallback(holder.CallbackId, EnsureJsHandle(instance, holder), methodName, args, expectsResult: false); } private static object? ProxyGetProperty(object instance, string propertyName) { var holder = JsHolderFor(instance); - return CallJsCallback(holder.CallbackId, DispatchPayload(instance, holder, "get_" + propertyName, []), expectsResult: true); + return CallJsCallback(holder.CallbackId, EnsureJsHandle(instance, holder), "get_" + propertyName, [], expectsResult: true); } private static void ProxySetProperty(object instance, string propertyName, object? value) { var holder = JsHolderFor(instance); - CallJsCallback(holder.CallbackId, DispatchPayload(instance, holder, "set_" + propertyName, [value]), expectsResult: false); + CallJsCallback(holder.CallbackId, EnsureJsHandle(instance, holder), "set_" + propertyName, [value], expectsResult: false); } } diff --git a/dotnet-bridge/Bridge.cs b/dotnet-bridge/Bridge.cs index 9505a3d..c300100 100644 --- a/dotnet-bridge/Bridge.cs +++ b/dotnet-bridge/Bridge.cs @@ -107,57 +107,56 @@ internal static void RemoveNativePtr(int handleId, IntPtr ptr) // on a coarse top-level root such as Microsoft. public static string GetNamespaceAssemblyMapJson() { + // Metadata only: loading each DLL to list its types is slow. var map = new Dictionary(StringComparer.OrdinalIgnoreCase); + var entryPath = Assembly.GetEntryAssembly()?.Location; try { foreach (var dir in s_assemblySearchDirs.Where(Directory.Exists)) { - try + IEnumerable files; + try { files = Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly); } + catch { continue; } + foreach (var file in files) { - foreach (var file in Directory.EnumerateFiles(dir, "*.dll", SearchOption.TopDirectoryOnly)) + try { - try - { - var simple = Path.GetFileNameWithoutExtension(file); - if (string.IsNullOrEmpty(simple)) continue; - - Assembly? asm = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => string.Equals(a.GetName().Name, simple, StringComparison.OrdinalIgnoreCase)); - if (asm is null) - { - try { asm = AssemblyLoadContext.Default.LoadFromAssemblyPath(file); } - catch { continue; } - } - - // The app's own assembly (C# sources compiled into the app) contributes - // its internal types too: they are reachable from JS (type resolution - // finds non-public types), so their namespaces must be as well. - Type[] types; - try { types = asm == Assembly.GetEntryAssembly() ? asm.GetTypes() : asm.GetExportedTypes(); } - catch (ReflectionTypeLoadException e) { types = e.Types.Where(t => t != null).ToArray()!; } - catch { types = Array.Empty(); } - - var assemblyName = asm.GetName().Name; - if (string.IsNullOrEmpty(assemblyName)) continue; - - foreach (var t in types) - { - if (string.IsNullOrEmpty(t.Namespace) || t.Name.StartsWith('<')) continue; - map[t.Namespace] = assemblyName; - var root = t.Namespace.Split('.')[0]; - if (!map.ContainsKey(root)) map[root] = assemblyName; - } - } - catch { } + // The app's own assembly counts its internal types too: JS reaches them. + var includeInternal = entryPath is not null && string.Equals(Path.GetFullPath(file), entryPath, StringComparison.OrdinalIgnoreCase); + AddNamespaces(file, includeInternal, map); } + catch { } } - catch { } } } catch { } try { return JsonSerializer.Serialize(map); } catch { return "{}"; } } + // Maps the namespaces of an assembly's top-level types (and their roots) to its name. + private static void AddNamespaces(string file, bool includeInternal, Dictionary map) + { + using var stream = File.OpenRead(file); + using var pe = new System.Reflection.PortableExecutable.PEReader(stream); + if (!pe.HasMetadata) return; + var md = System.Reflection.Metadata.PEReaderExtensions.GetMetadataReader(pe); + if (!md.IsAssembly) return; + var assemblyName = md.GetString(md.GetAssemblyDefinition().Name); + if (string.IsNullOrEmpty(assemblyName)) return; + var seen = new HashSet(); + foreach (var h in md.TypeDefinitions) + { + var td = md.GetTypeDefinition(h); + var visibility = td.Attributes & TypeAttributes.VisibilityMask; + if (visibility != TypeAttributes.Public && !(includeInternal && visibility == TypeAttributes.NotPublic)) continue; + if (td.Namespace.IsNil || !seen.Add(td.Namespace)) continue; + if (md.GetString(td.Name).StartsWith('<')) continue; + var ns = md.GetString(td.Namespace); + map[ns] = assemblyName; + map.TryAdd(ns.Split('.')[0], assemblyName); + } + } + /// Loads the assembly at `path` (relative to the app directory unless absolute) and returns its /// namespace map, as GetNamespaceAssemblyMapJson does. public static string LoadAssembly(string path) diff --git a/runtime/src/js/dotnet.js b/runtime/src/js/dotnet.js index 708fe6f..213f795 100644 --- a/runtime/src/js/dotnet.js +++ b/runtime/src/js/dotnet.js @@ -231,6 +231,11 @@ if (!typeName) return _emptyInfo; var cached = _typeInfoCache[typeName]; if (cached !== undefined) return cached; + // A namespace of the app's assemblies isn't a type; asking the bridge would scan them all. + if (typeof _namespaceAssemblyMap[typeName] === 'string' && handle === undefined) { + _typeInfoCache[typeName] = _emptyInfo; + return _emptyInfo; + } var info = _emptyInfo; try { // Respect an explicitly-provided assembly name. When empty, diff --git a/template/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj b/template/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj index 31b0b51..8ea3612 100644 --- a/template/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj +++ b/template/framework/__PROJECT_NAME__/__PROJECT_NAME__.csproj @@ -154,12 +154,15 @@ + + <_DotNetBridgeSources Include="$(NSWindowsRoot)\dotnet-bridge\*.cs" /> + From 792cb8f997aa038bd76bd9551349bae40ea6e336 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 23 Oct 2026 11:15:35 -0400 Subject: [PATCH 05/10] perf(napi): compiled-script caches for the napi engines The classic engine keeps V8's code cache for app scripts; the napi engines compiled every script from source on every launch. Each now keeps compiled code in the app's local folder, keyed by script name and source (and engine build), so a later launch skips compiling the bundle: - V8: ScriptCompiler code cache, made after the first run (covers the functions run at startup); V8 rejects a stale one and it is rewritten. - QuickJS: bytecode from a compile-only eval (JS_WriteObject/JS_ReadObject), checksummed, keyed to the engine build. - Hermes: its own jsr script cache. Hermes compiles large sources lazily and doesn't store those, so it helps module-sized scripts; big bundles need hermesc at build time. CommonJS modules compile through a new __nsCompileScript(code, filename) instead of new Function, so required modules (vendor.js) are cached too. require() of a 9 MB script in the test app, first launch -> later launches: V8 232 -> ~64 ms, QuickJS 871 -> ~350 ms. --- packages/common/src/prelude.rs | 5 +- packages/windows-hermes/src/lib.rs | 133 ++++++++++++++++++ packages/windows-quickjs/src/lib.rs | 26 +++- .../vendor/napi/quickjs/quickjs-api.c | 126 +++++++++++++++++ packages/windows-v8/csrc/win_jsr.cpp | 76 ++++++++++ packages/windows-v8/src/abi.rs | 3 + packages/windows-v8/src/lib.rs | 35 ++++- runtime/src/lib.rs | 6 + runtime/src/napi_engine/module_natives.rs | 27 +++- runtime/src/napi_engine/module_runner.rs | 2 + 10 files changed, 430 insertions(+), 9 deletions(-) diff --git a/packages/common/src/prelude.rs b/packages/common/src/prelude.rs index 6f25388..b97192b 100644 --- a/packages/common/src/prelude.rs +++ b/packages/common/src/prelude.rs @@ -248,7 +248,10 @@ pub const PRELUDE: &str = r#" var childRequire = makeRequire(resolved); try { - var factory = new Function('module', 'exports', 'require', '__filename', '__dirname', content); + // A named script, so engines cache its compiled code. The module starts on line 1. + var factory = typeof g.__nsCompileScript === 'function' + ? g.__nsCompileScript('(function (module, exports, require, __filename, __dirname) {' + content + '\n})', resolved) + : new Function('module', 'exports', 'require', '__filename', '__dirname', content); factory(mod, mod.exports, childRequire, resolved, dirName); } catch (e) { cjsCache.delete(key); diff --git a/packages/windows-hermes/src/lib.rs b/packages/windows-hermes/src/lib.rs index df4b9e8..3bdc858 100644 --- a/packages/windows-hermes/src/lib.rs +++ b/packages/windows-hermes/src/lib.rs @@ -34,6 +34,131 @@ pub mod ffi { result: *mut napi_value, ) -> i32; pub fn jsr_drain_microtasks(env: napi_env, max_count_hint: i32, result: *mut bool) -> i32; + pub fn jsr_config_set_script_cache( + config: *mut c_void, + script_cache_data: *mut c_void, + load: Option, + store: Option, + data_delete: Option, + deleter_data: *mut c_void, + ) -> i32; + } + + pub type DataDelete = unsafe extern "C" fn(data: *mut c_void, deleter_data: *mut c_void); + + pub type ScriptCacheLoad = unsafe extern "C" fn( + script_cache_data: *mut c_void, + source_url: *const c_char, + source_hash: u64, + runtime_name: *const c_char, + runtime_version: u64, + cache_tag: *const c_char, + buffer: *mut *const u8, + buffer_size: *mut usize, + buffer_delete_cb: *mut Option, + deleter_data: *mut *mut c_void, + ); + + pub type ScriptCacheStore = unsafe extern "C" fn( + script_cache_data: *mut c_void, + source_url: *const c_char, + source_hash: u64, + runtime_name: *const c_char, + runtime_version: u64, + cache_tag: *const c_char, + buffer: *const u8, + buffer_size: usize, + buffer_delete_cb: Option, + deleter_data: *mut c_void, + ); +} + +/// Hermes's script cache, in the app's local folder. Files are keyed by script name, source hash, +/// Hermes version and cache tag, so an edited script or a new Hermes compiles again. +mod script_cache { + use std::ffi::{c_char, c_void, CStr}; + use std::path::PathBuf; + + use super::ffi::DataDelete; + + unsafe fn text(s: *const c_char) -> String { + if s.is_null() { String::new() } else { CStr::from_ptr(s).to_string_lossy().into_owned() } + } + + unsafe fn cache_path(url: *const c_char, hash: u64, version: u64, tag: *const c_char) -> Option { + let dir = runtime::local_folder()?; + let url = text(url); + let name: String = url + .rsplit(['/', '\\']) + .next() + .unwrap_or("script") + .chars() + .map(|c| if c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_' { c } else { '_' }) + .take(64) + .collect(); + let tag: String = text(tag).chars().filter(|c| c.is_ascii_alphanumeric()).take(16).collect(); + Some(PathBuf::from(dir).join("hermes-cache").join(format!("{name}.{hash:016x}.{version:x}.{tag}.hbc"))) + } + + unsafe extern "C" fn free_bytes(data: *mut c_void, deleter_data: *mut c_void) { + let _ = data; + drop(Box::from_raw(deleter_data as *mut Vec)); + } + + pub unsafe extern "C" fn load( + _data: *mut c_void, + source_url: *const c_char, + source_hash: u64, + _runtime_name: *const c_char, + runtime_version: u64, + cache_tag: *const c_char, + buffer: *mut *const u8, + buffer_size: *mut usize, + buffer_delete_cb: *mut Option, + deleter_data: *mut *mut c_void, + ) { + *buffer = std::ptr::null(); + *buffer_size = 0; + let Some(path) = cache_path(source_url, source_hash, runtime_version, cache_tag) else { return }; + let Ok(bytes) = std::fs::read(&path) else { return }; + if bytes.is_empty() { + return; + } + let boxed = Box::new(bytes); + *buffer = boxed.as_ptr(); + *buffer_size = boxed.len(); + *buffer_delete_cb = Some(free_bytes); + *deleter_data = Box::into_raw(boxed) as *mut c_void; + } + + pub unsafe extern "C" fn store( + _data: *mut c_void, + source_url: *const c_char, + source_hash: u64, + _runtime_name: *const c_char, + runtime_version: u64, + cache_tag: *const c_char, + buffer: *const u8, + buffer_size: usize, + buffer_delete_cb: Option, + deleter_data: *mut c_void, + ) { + if let Some(path) = cache_path(source_url, source_hash, runtime_version, cache_tag) { + if !buffer.is_null() && buffer_size > 0 { + let bytes = std::slice::from_raw_parts(buffer, buffer_size); + // Written beside the final name first, so a crash never leaves a partial cache file. + let tmp = path.with_extension("tmp"); + let written = path.parent().map_or(Ok(()), std::fs::create_dir_all) + .and_then(|_| std::fs::write(&tmp, bytes)) + .and_then(|_| std::fs::rename(&tmp, &path)); + if written.is_err() { + let _ = std::fs::remove_file(&tmp); + } + } + } + if let Some(delete) = buffer_delete_cb { + delete(buffer as *mut c_void, deleter_data); + } } } @@ -54,6 +179,14 @@ pub unsafe fn create_runtime_env() -> Option<(*mut c_void, napi_env, *mut c_void let mut env_raw: napi_env = std::ptr::null_mut(); if ffi::jsr_create_config(&mut config) != 0 || ffi::jsr_config_set_explicit_microtasks(config, true) != 0 + || ffi::jsr_config_set_script_cache( + config, + std::ptr::null_mut(), + Some(script_cache::load), + Some(script_cache::store), + None, + std::ptr::null_mut(), + ) != 0 || ffi::jsr_create_runtime(config, &mut runtime) != 0 || ffi::jsr_runtime_get_node_api_env(runtime, &mut env_raw) != 0 || env_raw.is_null() diff --git a/packages/windows-quickjs/src/lib.rs b/packages/windows-quickjs/src/lib.rs index 53d88df..74d4dbb 100644 --- a/packages/windows-quickjs/src/lib.rs +++ b/packages/windows-quickjs/src/lib.rs @@ -65,6 +65,13 @@ pub mod shim { file: *const c_char, result: *mut NapiValue, ) -> NapiStatus; + fn qjs_execute_script_cached( + env: NapiEnv, + script: NapiValue, + file: *const c_char, + cache_dir: *const c_char, + result: *mut NapiValue, + ) -> NapiStatus; fn qjs_free_napi_env(env: NapiEnv) -> NapiStatus; fn qjs_free_runtime(rt: NapiRuntime) -> NapiStatus; fn qjs_execute_pending_jobs(env: NapiEnv) -> NapiStatus; @@ -117,7 +124,11 @@ pub mod shim { napi_create_string_utf8(env, c.as_ptr(), NAPI_AUTO_LENGTH, &mut script); let file = CString::new("