From 17002de08487ce91cfa5f0ebb37fb89d9f39d809 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 8 Jun 2026 11:28:16 +0300 Subject: [PATCH 1/2] implement --- docs/guide/specialization.md | 8 +++ .../Attributes/SpecializeExportAttribute.cs | 11 ++-- .../Attributes/SpecializeImportAttribute.cs | 42 +++++++------ .../GenerateCS/CSInstanceTest.cs | 33 ++++++++++ .../GenerateJS/DeclarationTest.cs | 63 +++++++++++++++++++ .../Common/Global/GlobalType.cs | 32 ++++++---- .../Common/Inspection/TypeInspector.cs | 14 ++--- .../Common/Preferences/Preferences.cs | 7 ++- .../Common/Preferences/Specialization.cs | 15 +++-- .../GenerateCS/CSInstanceGenerator.cs | 2 +- .../Declarations/DeclarationGenerator.cs | 49 ++++++++------- .../Declarations/DocumentationBuilder.cs | 4 +- .../Declarations/TypeSyntaxBuilder.cs | 11 ++-- src/cs/Directory.Build.props | 2 +- .../Test.Library/Modules/BidirectionalCS.cs | 2 +- .../cs/Test.Library/Modules/IBidirectional.cs | 21 ++++++- .../test/cs/Test.Library/Modules/Modules.cs | 2 +- src/js/test/cs/Test.Library/Specialization.cs | 59 ++++++++++------- src/js/test/cs/Test/Static.cs | 4 +- 19 files changed, 272 insertions(+), 109 deletions(-) diff --git a/docs/guide/specialization.md b/docs/guide/specialization.md index 4c8d77fd..2fd17ffe 100644 --- a/docs/guide/specialization.md +++ b/docs/guide/specialization.md @@ -46,6 +46,10 @@ const comparer = Program.getComparer(); comparer.compare("a", "b"); // -1 ``` +::: tip +When the specialized `Clr` type is a class, it will also affect (specialize) any subclasses discovered on the interop surfaces. +::: + ## Injecting Code The `[SpecializeImport]` attribute accepts optional `CS`, `JS`, `JSCtor` and `Decl` snippets that are spliced verbatim into the generated C# or JavaScript proxies and its TypeScript declaration. This lets the imported proxy satisfy JS-side contracts that aren't expressible through the C# abstract members alone — for example, injecting an iterator: @@ -56,6 +60,10 @@ The `[SpecializeImport]` attribute accepts optional `CS`, `JS`, `JSCtor` and `De Decl: "[Symbol.iterator](): IterableIterator;")] ``` +The `CS` snippet can contain `$full` markers — they will be replaced with the fully-qualified type name of the specialized instance. This allows referencing the concrete specialized instances in the proxy when the specialization is applied to a base class. + +The `Decl` snippet can contain `$full`, `$name` and `$T{I}` markers — first is the same as CS one, but in TypeScript context, name is the short type name and `T` is the fully-qualified name of the generic type argument with the `{I}` inde (if any), for example `$T{0}` is replaced with the first generic argument. + When `Decl` value starts with `export ` — the content will replace the entire TypeScript declaration of the type, instead of splicing it into the bottom of the default type declaration. ::: tip EXAMPLE diff --git a/src/cs/Bootsharp.Common/Attributes/SpecializeExportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/SpecializeExportAttribute.cs index f09258fc..c235ee02 100644 --- a/src/cs/Bootsharp.Common/Attributes/SpecializeExportAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/SpecializeExportAttribute.cs @@ -7,11 +7,12 @@ namespace Bootsharp; /// The exported specialization is expected to be paired with the /// counterpart and contain implementations for all the abstract members defined on the imported specialization. /// +/// +/// The CLR type to specialize the export for. +/// When the type is a class, will specialize subclasses as well. +/// [AttributeUsage(AttributeTargets.Class)] -public sealed class SpecializeExportAttribute (Type clr) : Attribute +public sealed class SpecializeExportAttribute (Type Clr) : Attribute { - /// - /// The CLR type to specialize the export for. - /// - public Type Clr { get; } = clr; + public Type Clr { get; } = Clr; } diff --git a/src/cs/Bootsharp.Common/Attributes/SpecializeImportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/SpecializeImportAttribute.cs index bc83c91b..e7ff06e8 100644 --- a/src/cs/Bootsharp.Common/Attributes/SpecializeImportAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/SpecializeImportAttribute.cs @@ -7,29 +7,35 @@ namespace Bootsharp; /// the actual interop surface. All the abstract members are expected to be implemented on the paired export /// specialization of the class annotated with . /// +/// +/// The CLR type to specialize the import for. +/// When the type is a class, will specialize subclasses as well. +/// +/// +/// Raw snippet spliced into the generated C# import proxy class body. +/// All occurrences of '$full' are replaced with the fully-qualified type name of the specialized instance type. +/// +/// +/// Raw snippet spliced into the generated JavaScript export proxy class body. +/// +/// +/// Raw snippet spliced into the generated JavaScript export proxy constructor. +/// +/// +/// Raw snippet spliced into the generated TypeScript declaration. +/// When starts with 'export ' will instead replace the whole declaration. +/// Occurrences of '$name' are replaced with the name of the specialized instance type. +/// Occurrences of '$full' are replaced with the fully-qualified name of the specialized instance type. +/// When the instance type is generic, occurrences of '$T{I}' are replaced with the fully-qualified names +/// of the generic type arguments with the {I} index, starting with 0 (eg, '$T0' is the first generic arg). +/// [AttributeUsage(AttributeTargets.Class)] -public sealed class SpecializeImportAttribute (Type clr, string? CS = null, string? JS = null, +public sealed class SpecializeImportAttribute (Type Clr, string? CS = null, string? JS = null, string? JSCtor = null, string? Decl = null) : Attribute { - /// - /// The CLR type to specialize the import for. - /// - public Type Clr { get; } = clr; - /// - /// Raw snippet spliced into the generated C# import proxy class body. - /// + public Type Clr { get; } = Clr; public string? CS { get; } = CS; - /// - /// Raw snippet spliced into the generated JavaScript export proxy class body. - /// public string? JS { get; } = JS; - /// - /// Raw snippet spliced into the generated JavaScript export proxy constructor. - /// public string? JSCtor { get; } = JSCtor; - /// - /// Raw snippet spliced into the generated TypeScript declaration. - /// When starts with 'export ' will instead replace the whole declaration. - /// public string? Decl { get; } = Decl; } diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs index f3b14c16..2f5cd13a 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs @@ -279,6 +279,39 @@ internal static int Export (global::Custom it) => Export(new global::CustomExpor """); } + [Fact] + public void GeneratesForCustomSpecializationOfBaseClass () + { + AddAssembly(With( + """ + public abstract class Event; + public sealed class IntEvent : Event; + + [SpecializeImport(typeof(Event<>), CS: "protected override object Unwrap () => new $full();")] + public abstract class EventImport (int id) : SpecializedImport(id); + + [SpecializeExport(typeof(Event<>))] + public sealed class EventExport (Event it) : SpecializedExport(it); + + public class Class + { + [Export] public static IntEvent Foo (IntEvent it) => default!; + } + """)); + Execute(); + Contains("internal static int Export (global::IntEvent it) => Export(new global::EventExport(it));"); + Contains("Instances.RegisterImport(typeof(global::IntEvent), static id => new global::Bootsharp.Generated.JS_Import_IntEvent(id));"); + Contains( + """ + public sealed class JS_Import_IntEvent (int id) : global::EventImport(id) + { + ~JS_Import_IntEvent() => Instances.DisposeImported(_id); + + protected override object Unwrap () => new global::IntEvent(); + } + """); + } + [Fact] public void GeneratesForBuiltInSpecializations () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index bf26ef88..2656f36d 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -71,6 +71,25 @@ export namespace Foo { """); } + [Fact] + public void TypeNestedUnderInstancedDeclaredUnderNamespace () + { + AddAssembly( + With("public class Foo { public record Bar; public int Value { get; } }"), + WithClass("[Export] public static Foo Get (Foo.Bar b) => default;")); + Execute(); + Contains( + """ + export interface Foo { + readonly value: number; + } + export namespace Foo { + export type Bar = Readonly<{ + }>; + } + """); + } + [Fact] public void CrawledTypeDoesNotOverrideSpecialized () { @@ -358,6 +377,22 @@ export interface Interface { """); } + [Fact] + public void ExtendsClosedGenericWithClosedArguments () + { + AddAssembly( + With("public interface IBar { T Get (); }"), + With("public class Foo : IBar { public int Get () => 0; }"), + WithClass("[Export] public static Foo UseFoo (IBar bar) => default;")); + Execute(); + Contains( + """ + export interface Foo extends IBar { + get(): number; + } + """); + } + [Fact] public void GeneratedForSpecializedTypes () { @@ -400,6 +435,34 @@ public class Class { [Export] public static Foo Get () => default; } DoesNotContain("export interface Foo"); } + [Fact] + public void SpecializedDeclarationSubstitutesTemplates () + { + AddAssembly(With( + """ + public delegate void Handler (string arg); + public abstract class Event where T : System.Delegate; + public class Host { public sealed class HandlerEvent : Event; } + [SpecializeImport(typeof(Event<>), Decl: "export interface $name {\n broadcast: $T0;\n}")] + public abstract class EventImport (int id) : SpecializedImport(id) where T : System.Delegate + { + public abstract void Subscribe (T handler); + } + [SpecializeExport(typeof(Event<>))] + public sealed class EventExport (Event it) : SpecializedExport(it) where T : System.Delegate; + public class Class { [Export] public static Host.HandlerEvent Get () => default; } + """)); + Execute(); + Contains( + """ + export namespace Host { + export interface HandlerEvent { + broadcast: Handler; + } + } + """); + } + [Fact] public void GeneratedForCollections () { diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index d6a6c8a9..30c6df68 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -28,8 +28,8 @@ public static bool IsTaskLike (Type type) public static bool IsDelegate (Type type) { - for (var bs = type.BaseType; bs != null; bs = bs.BaseType) - if (bs.FullName == "System.MulticastDelegate") + for (var t = type.BaseType; t != null; t = t.BaseType) + if (t.FullName == "System.MulticastDelegate") return true; return false; } @@ -54,11 +54,11 @@ public static bool IsList (Type type, [NotNullWhen(true)] out Type? element) static bool IsList (Type type) => type.IsGenericType && - (type.GetGenericTypeDefinition().FullName == typeof(List<>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IList<>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyList<>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(ICollection<>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyCollection<>).FullName); + (OpenGeneric(type).FullName == typeof(List<>).FullName || + OpenGeneric(type).FullName == typeof(IList<>).FullName || + OpenGeneric(type).FullName == typeof(IReadOnlyList<>).FullName || + OpenGeneric(type).FullName == typeof(ICollection<>).FullName || + OpenGeneric(type).FullName == typeof(IReadOnlyCollection<>).FullName); } public static bool IsDictionary (Type type, [NotNullWhen(true)] out Type? key, [NotNullWhen(true)] out Type? value) @@ -73,14 +73,15 @@ public static bool IsDictionary (Type type, [NotNullWhen(true)] out Type? key, [ static bool IsDictionary (Type type) => type.IsGenericType && - (type.GetGenericTypeDefinition().FullName == typeof(Dictionary<,>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IDictionary<,>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); + (OpenGeneric(type).FullName == typeof(Dictionary<,>).FullName || + OpenGeneric(type).FullName == typeof(IDictionary<,>).FullName || + OpenGeneric(type).FullName == typeof(IReadOnlyDictionary<,>).FullName); } - public static bool IsSameType (Type a, Type b) => - (a.IsGenericType ? a.GetGenericTypeDefinition() : a) == - (b.IsGenericType ? b.GetGenericTypeDefinition() : b); + public static bool IsSameType (Type a, Type b) + { + return OpenGeneric(a) == OpenGeneric(b); + } public static Nullity GetNullity (EventInfo evt) => new NullabilityInfoContext().Create(evt); public static Nullity GetNullity (EventInfo evt, ParameterInfo param) => @@ -160,6 +161,11 @@ public static string TrimGeneric (string typeName) return Regex.Replace(typeName, @"`\d+(\[\[.*\]\])?", ""); } + public static Type OpenGeneric (Type type) + { + return type.IsGenericType ? type.GetGenericTypeDefinition() : type; + } + public static string ExportCS (ArgumentMeta arg) => ExportCS(arg.Value, arg.Name); public static string ExportCS (ValueMeta value, string exp) => ExportCS(value.Type, exp); public static string ExportCS (TypeMeta type, string exp) diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index e2725330..82940a5a 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -61,7 +61,7 @@ public IReadOnlyCollection Collect () { if (!inspectedModuleTypes.Add(type) || IsStatic(type) || (ik == InteropKind.Import && !type.IsInterface)) return null; - var proxy = BuildProxy(type, BuildId(type), ik); + var proxy = BuildProxy(BuildId(type), ik); var md = new ModuleMeta(type) { IK = ik, Proxy = proxy, Members = new List() }; return InspectMembers(md, ik, null); } @@ -78,7 +78,7 @@ public IReadOnlyCollection Collect () return InspectInstance(type, InteropKind.Export, nul)!; // likely passing back an exported instance return InspectMembers(its[key] = new(type) { IK = ik, - Proxy = BuildProxy(type, id, ik, sp), + Proxy = BuildProxy(id, ik, sp), Members = new List(), Exporter = ResolveExporter(), Importer = ResolveImporter(), @@ -103,7 +103,7 @@ static bool IsInstanced (Type type) string? ResolveImporter () { if (ik != InteropKind.Import) return null; - if ((sp?.For(type, ik) ?? type).GetEvents().Length > 0) return $"import_{id}"; + if ((sp?.Import ?? type).GetEvents().Length > 0) return $"import_{id}"; return null; } } @@ -111,7 +111,7 @@ static bool IsInstanced (Type type) private DelegateMeta InspectDelegate (Type type, InteropKind ik, Nullity? nul) { var members = new List(); - var proxy = BuildProxy(type, BuildId(type), ik); + var proxy = BuildProxy(BuildId(type), ik); var del = new DelegateMeta(type) { IK = ik, Proxy = proxy, Members = members }; members.Add(InspectMethod(type.GetMethod("Invoke")!, ik, del, nul)); return del; @@ -200,7 +200,7 @@ private TypeMeta InspectType (Type type, InteropKind ik, Nullity? nul) return InspectInstance(type, ik, nul) ?? srd.Inspect(type, ik, nul) ?? new TypeMeta(type); } - private SurfaceProxy BuildProxy (Type type, string typeId, InteropKind ik, Specialization? sp = null) + private SurfaceProxy BuildProxy (string typeId, InteropKind ik, Specialization? sp = null) { var id = "JS_" + (ik == InteropKind.Export ? "Export_" : "Import_") + typeId; var stx = $"global::Bootsharp.Generated.{id}"; @@ -208,8 +208,8 @@ private SurfaceProxy BuildProxy (Type type, string typeId, InteropKind ik, Speci return new SpecializedProxy { Id = id, Syntax = stx, - Import = new(sp.For(type, InteropKind.Import)), - Export = new(sp.For(type, InteropKind.Export)), + Import = new(sp.Import), + Export = new(sp.Export), CS = sp.CS, JS = sp.JS, JSCtor = sp.JSCtor, diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs index 9d2881de..2d383a3d 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs @@ -48,8 +48,11 @@ public static IReadOnlyCollection Rename (IReadOnlyCollection IsSpecialized(type, out _); public static bool IsSpecialized (Type type, [NotNullWhen(true)] out Specialization? sp) { - var key = type.IsGenericType ? type.GetGenericTypeDefinition() : type; - return (sp = prefs.Specs.GetValueOrDefault(key)) != null; + for (var t = type; t != null; t = t.BaseType) + if ((sp = prefs.Specs.GetValueOrDefault(OpenGeneric(t))?.For(t)) != null) + return true; + sp = null; + return false; } private static void ResolveRenames (Assembly ass) diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/Specialization.cs b/src/cs/Bootsharp.Publish/Common/Preferences/Specialization.cs index e5f0d96c..8a87981d 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/Specialization.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/Specialization.cs @@ -9,10 +9,13 @@ internal sealed record Specialization public string? JSCtor { get; init; } public string? Decl { get; init; } - public Type For (Type specialized, InteropKind ik) - { - var specializer = ik == InteropKind.Import ? Import : Export; - if (!specializer.IsGenericTypeDefinition) return specializer; - return specializer.MakeGenericType(specialized.GenericTypeArguments); - } + public Specialization For (Type specialized) => this with { + Import = Close(Import, specialized), + Export = Close(Export, specialized) + }; + + private static Type Close (Type specializer, Type specialized) => + specializer.IsGenericTypeDefinition && specialized.IsConstructedGenericType + ? specializer.MakeGenericType(specialized.GenericTypeArguments) + : specializer; } diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index 19505df0..c94f69fe 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs @@ -98,7 +98,7 @@ public sealed class {{it.Proxy.Id}} (int id) : {{sp.Import.Syntax}}(id) { ~{{it.Proxy.Id}}() => Instances.DisposeImported(_id); - {{Fmt([..it.Members.Select(EmitMemberImport), sp.CS])}} + {{Fmt([..it.Members.Select(EmitMemberImport), sp.CS?.Replace("$full", it.Syntax)])}} } """; diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs index 748f9655..98a64061 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -33,32 +33,31 @@ public string Generate (JSModule module) } private string EmitImports (JSModule md) => Fmt([ - $$"""import type * as $bcl from "{{md.To("bcl/index")}}";""", + $"""import type * as $bcl from "{md.To("bcl/index")}";""", ..mds.GetImported(md).Select(imp => $"""import type * as {imp.Alias} from "{md.ToMd(imp.Path)}";""") ], 0); private void DeclareNode (JSNode node) { - var surf = node.Types.FirstOrDefault(s => s is SurfaceMeta and not InstanceMeta); - var wrap = surf != null || node.Children.Count > 0; - if (wrap) - { - if (surf != null) doc.Type(surf); - bld.Enter($"export namespace {node.Name} {{"); - } + var surfaces = new List(); foreach (var type in node.Types) // Dedup by CLR to discard the other side of a bidirectional (export+import) // instance surface and closed generic variants (all produce same open type). - if (declared.Add(type.Clr.IsGenericType ? type.Clr.GetGenericTypeDefinition() : type.Clr)) + if (declared.Add(OpenGeneric(type.Clr))) if (type is SerializedEnumMeta enu) DeclareEnum(enu); - else if (type is SerializedObjectMeta o) DeclareSerialized(o); - else if (type is DelegateMeta d) DeclareDelegate(d); + else if (type is SerializedObjectMeta ser) DeclareSerialized(ser); + else if (type is DelegateMeta del) DeclareDelegate(del); else if (type is InstanceMeta it) DeclareInstance(it); - else if (type is SurfaceMeta srf) DeclareSurface(srf); + else if (type is SurfaceMeta srf) surfaces.Add(srf); + if (surfaces.Count == 0 && node.Children.Count == 0) return; + if (surfaces.Count > 0) doc.Type(surfaces[0]); + bld.Enter($"export namespace {node.Name} {{"); + foreach (var surface in surfaces) + DeclareSurface(surface); foreach (var child in node.Children) DeclareNode(child); - if (wrap) bld.Exit("}"); + bld.Exit("}"); } private void DeclareEnum (SerializedEnumMeta enu) @@ -77,7 +76,7 @@ private void DeclareEnum (SerializedEnumMeta enu) private void DeclareSerialized (SerializedObjectMeta obj) { doc.Type(obj); - var ext = spec.Types.HasBase(obj.Clr, out var bs) ? $"{ts.BuildFullName(bs.Clr)} & " : ""; + var ext = spec.Types.HasBase(obj.Clr, out var bs) ? $"{ts.BuildRef(bs.Clr)} & " : ""; bld.Enter($$"""export type {{ts.BuildName(obj.Clr)}} = {{ext}}Readonly<{"""); foreach (var prop in obj.Properties.Where(p => ShouldDeclareOn(obj.Clr, p.Info))) { @@ -98,25 +97,33 @@ private void DeclareDelegate (DelegateMeta del) private void DeclareInstance (InstanceMeta it) { doc.Type(it); - if (it.Proxy is SpecializedProxy { Decl: { } sp } && sp.StartsWith("export ")) - { - bld.Line(sp); - return; - } + if (DeclareSpecialized(out var specialized)) return; bld.Enter($$"""export interface {{ts.BuildName(it.Clr)}}{{BuildExtensions()}} {"""); foreach (var member in it.Members.Where(m => ShouldDeclareOn(it.Clr, m.Info))) if (member is EventMeta evt) DeclareEvent(evt); else if (member is PropertyMeta prop) DeclareProperty(prop); else if (member is MethodMeta method) DeclareMethod(method); - if (it.Proxy is SpecializedProxy { Decl: { } decl }) bld.Line(decl); + if (specialized != null) bld.Line(specialized); bld.Exit("}"); + bool DeclareSpecialized (out string? decl) + { + if (string.IsNullOrEmpty(decl = (it.Proxy as SpecializedProxy)?.Decl)) return false; + decl = Fmt(decl).Replace("$name", ts.BuildName(it.Clr)).Replace("$full", ts.BuildRef(it.Clr)); + var args = (it.Proxy as SpecializedProxy)!.Import.Clr.GetGenericArguments(); + for (var i = 0; i < args.Length; i++) + decl = decl.Replace($"$T{i}", ts.BuildRef(args[i])); + var replaces = decl.StartsWith("export "); + if (replaces) bld.Line(decl); + return replaces; + } + string BuildExtensions () { if (it.Proxy is SpecializedProxy) return ""; // specialized surfaces are self-contained var ext = it.Clr.GetInterfaces().Where(i => IsUserType(i) && spec.Types.Has(i)).ToList(); if (spec.Types.HasBase(it.Clr, out var bs)) ext.Insert(0, bs.Clr); - return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildFullName))}"; + return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildRef))}"; } void DeclareEvent (EventMeta evt) diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs index 5bf5fbc6..bd1e4497 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DocumentationBuilder.cs @@ -95,7 +95,7 @@ static string GetArgKey (Type type) { if (type.IsArray) return $"{GetArgKey(type.GetElementType()!)}[{new string(',', type.GetArrayRank() - 1)}]"; if (!type.IsGenericType) return GetXmlKey(type); - var definition = type.GetGenericTypeDefinition(); + var definition = OpenGeneric(type); var name = definition.Name.Split('`')[0]; name = string.IsNullOrEmpty(definition.Namespace) ? name : $"{definition.Namespace}.{name}"; return $"{name}{{{string.Join(',', type.GetGenericArguments().Select(GetArgKey))}}}"; @@ -112,7 +112,7 @@ private void Append (IReadOnlyList summary) private static string GetXmlKey (Type type) { - if (type.IsGenericType) type = type.GetGenericTypeDefinition(); + if (type.IsGenericType) type = OpenGeneric(type); if (type.IsNested) return $"{GetXmlKey(type.DeclaringType!)}.{type.Name}"; return string.IsNullOrEmpty(type.Namespace) ? type.Name : $"{type.Namespace}.{type.Name}"; } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs index a7ad001a..266cf0e4 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/TypeSyntaxBuilder.cs @@ -16,21 +16,20 @@ public void EnterModule (JSModule module) public string BuildName (Type type) { - var full = BuildFullName(type); + var full = BuildRef(OpenGeneric(type)); var dotIdx = full.LastIndexOf('.'); return dotIdx > 0 ? full[(dotIdx + 1)..] : full; } - public string BuildFullName (Type type) + public string BuildRef (Type type) { - if (type.IsGenericType) type = type.GetGenericTypeDefinition(); return Build(type, null); } public string BuildArg (ParameterInfo param) { if (param.Member.DeclaringType!.IsGenericType) - param = param.Member.DeclaringType.GetGenericTypeDefinition() + param = OpenGeneric(param.Member.DeclaringType) .GetMethod(param.Member.Name)!.GetParameters()[param.Position]; var nul = GetNullity(param); if (param.HasDefaultValue) return $"?: {Build(param.ParameterType, nul)}"; @@ -48,7 +47,7 @@ public string BuildArg (EventInfo evt, ParameterInfo param) public string BuildReturn (MethodInfo method) { if (method.DeclaringType!.IsGenericType) - method = method.DeclaringType.GetGenericTypeDefinition().GetMethod(method.Name)!; + method = OpenGeneric(method.DeclaringType).GetMethod(method.Name)!; var nul = GetNullity(method.ReturnParameter); var post = IsNullable(method.ReturnType, nul) ? " | null" : ""; return Build(method.ReturnType, nul) + post; @@ -57,7 +56,7 @@ public string BuildReturn (MethodInfo method) public string BuildProperty (PropertyInfo prop) { if (prop.DeclaringType!.IsGenericType) - prop = prop.DeclaringType.GetGenericTypeDefinition().GetProperty(prop.Name)!; + prop = OpenGeneric(prop.DeclaringType).GetProperty(prop.Name)!; var nul = GetNullity(prop); var pre = IsNullable(prop.PropertyType, nul) ? "?: " : ": "; return pre + Build(prop.PropertyType, nul); diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index d996c56a..09d2186a 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.9.0-alpha.18 + 0.9.0-alpha.29 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs index 150fed74..4c72aeb3 100644 --- a/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs +++ b/src/js/test/cs/Test.Library/Modules/BidirectionalCS.cs @@ -5,7 +5,7 @@ namespace Test.Library; public class BidirectionalCS : IBidirectional { public event Action? OnBiChanged; - public Event OnSpecial { get; } = new(); + public IBidirectional.SpecialEvent OnSpecial { get; } = new(); public IBidirectional? Bi { get; set => NotifyChanged(field = value); } = null!; diff --git a/src/js/test/cs/Test.Library/Modules/IBidirectional.cs b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs index 1c937739..f8c5ebd2 100644 --- a/src/js/test/cs/Test.Library/Modules/IBidirectional.cs +++ b/src/js/test/cs/Test.Library/Modules/IBidirectional.cs @@ -4,10 +4,29 @@ namespace Test.Library; public interface IBidirectional { + public delegate void SpecialHandler (IBidirectional? bi); + public sealed class SpecialEvent : Event + { + public IBidirectional? Last { get; private set; } + + public void Broadcast (IBidirectional? bi) + { + Last = bi; + foreach (var handler in Handlers) + handler(bi); + } + + public override void Replay (SpecialHandler handler) + { + if (Last is { } last) + handler(last); + } + } + event Action? OnBiChanged; IBidirectional? Bi { get; set; } - Event OnSpecial { get; } + SpecialEvent OnSpecial { get; } IBidirectional? EchoBi (IBidirectional? bi); } diff --git a/src/js/test/cs/Test.Library/Modules/Modules.cs b/src/js/test/cs/Test.Library/Modules/Modules.cs index 1492b7d4..d8f2d1f6 100644 --- a/src/js/test/cs/Test.Library/Modules/Modules.cs +++ b/src/js/test/cs/Test.Library/Modules/Modules.cs @@ -68,7 +68,7 @@ public static void CanInteropWithBidirectional () IBidirectional? eventObserved = null; IBidirectional? specialObserved = null; Action eventHandler = b => eventObserved = b; - SpecialBiHandler specialHandler = b => specialObserved = b; + IBidirectional.SpecialHandler specialHandler = b => specialObserved = b; js.OnBiChanged += eventHandler; js.OnSpecial.Subscribe(specialHandler); Assert(js.EchoBi(null) == null); diff --git a/src/js/test/cs/Test.Library/Specialization.cs b/src/js/test/cs/Test.Library/Specialization.cs index 41e649db..8dabcb13 100644 --- a/src/js/test/cs/Test.Library/Specialization.cs +++ b/src/js/test/cs/Test.Library/Specialization.cs @@ -19,11 +19,12 @@ public class ComparerExport (IComparer cmp) : SpecializedExport(cmp) public int Compare (T? x, T? y) => cmp.Compare(x, y); } -public sealed class Event where T : Delegate +public abstract class Event where T : Delegate { - public List Handlers { get; } = []; + protected List Handlers { get; } = []; public void Subscribe (T handler) => Handlers.Add(handler); public void Unsubscribe (T handler) => Handlers.Remove(handler); + public abstract void Replay (T handler); } /// @@ -34,26 +35,36 @@ public sealed class Event where T : Delegate /// Opt parameter doc. public delegate void SpecialHandler (string? nul, string str, int? opt = null); -public delegate void SpecialBiHandler (IBidirectional? bi); +public sealed class SpecialHandlerEvent : Event +{ + public (string? nul, string str, int? opt)? Last { get; private set; } + + public void Broadcast (string? nul, string str, int? opt = null) + { + Last = (nul, str, opt); + foreach (var handler in Handlers) + handler(nul, str, opt); + } + + public override void Replay (SpecialHandler handler) + { + if (Last is { } last) + handler(last.nul, last.str, last.opt); + } +} public static class EventExtensions { - extension (Event @event) + extension (Event evt) { - public void Broadcast (string? nul, string str, int? opt = null) - { - foreach (var handler in @event.Handlers) - handler(nul, str, opt); - } + public void Broadcast (string? nul, string str, int? opt = null) => + ((SpecialHandlerEvent)evt).Broadcast(nul, str, opt); } - extension (Event @event) + extension (Event evt) { - public void Broadcast (IBidirectional? bi) - { - foreach (var handler in @event.Handlers) - handler(bi); - } + public void Broadcast (IBidirectional? bi) => + ((IBidirectional.SpecialEvent)evt).Broadcast(bi); } } @@ -62,7 +73,7 @@ public void Broadcast (IBidirectional? bi) """ protected override object Unwrap () { if (Event != null) return Event; - ImportedByEvent.Add(Event = new(), this); + ImportedByEvent.Add(Event = new $full(), this); Subscribe(Event.Broadcast); return Event; } @@ -76,11 +87,11 @@ protected override object Unwrap () { """, Decl: """ - export interface Event void> { - readonly last?: Parameters; - subscribe(handler: T): string; - unsubscribe(handler: T): void; - broadcast: T; + export interface $name { + readonly last?: Parameters<$T0>; + subscribe(handler: $T0): string; + unsubscribe(handler: $T0): void; + broadcast: $T0; } """)] public abstract class EventImport (int id) : SpecializedImport(id) where T : Delegate @@ -94,7 +105,11 @@ public abstract class EventImport (int id) : SpecializedImport(id) where T : [SpecializeExport(typeof(Event<>))] public sealed class EventExport (Event evt) : SpecializedExport(Resolve(evt)) where T : Delegate { - public void Subscribe (T handler) => evt.Subscribe(handler); + public void Subscribe (T handler) + { + evt.Subscribe(handler); + evt.Replay(handler); + } private static object Resolve (Event evt) => EventImport.ImportedByEvent.TryGetValue(evt, out var imported) ? imported : evt; diff --git a/src/js/test/cs/Test/Static.cs b/src/js/test/cs/Test/Static.cs index 25975efd..3a26dc46 100644 --- a/src/js/test/cs/Test/Static.cs +++ b/src/js/test/cs/Test/Static.cs @@ -34,8 +34,8 @@ public record Square : IShape [Import] public static partial string ImportedProperty { get; set; } [Export] public static string ExportedProperty { get; set; } = "initial exported"; - [Import] public static partial Event ImportedSpecial { get; } - [Export] public static Event ExportedSpecial { get; } = new(); + [Import] public static partial SpecialHandlerEvent ImportedSpecial { get; } + [Export] public static SpecialHandlerEvent ExportedSpecial { get; } = new(); [Import] public static partial byte[] EchoImported (byte[] bytes); [Export] public static byte[] EchoExported (byte[] bytes) => bytes; From 01ea193746bfa81595eb56939ae1fc0a948acb5e Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:40:58 +0300 Subject: [PATCH 2/2] cache export specializer wrappers --- src/cs/Bootsharp.Common.Test/InstancesTest.cs | 20 ++++++-- src/cs/Bootsharp.Common/Interop/Instances.cs | 31 ++++++++---- .../GenerateCS/CSInstanceTest.cs | 50 ++++++++++--------- .../GenerateCS/CSInstanceGenerator.cs | 16 ++++-- src/cs/Directory.Build.props | 2 +- 5 files changed, 76 insertions(+), 43 deletions(-) diff --git a/src/cs/Bootsharp.Common.Test/InstancesTest.cs b/src/cs/Bootsharp.Common.Test/InstancesTest.cs index 7eff3c96..8ef35678 100644 --- a/src/cs/Bootsharp.Common.Test/InstancesTest.cs +++ b/src/cs/Bootsharp.Common.Test/InstancesTest.cs @@ -61,13 +61,23 @@ public void ShortCircuitsImportedProxies () [Fact] public void ShortCircuitsSpecializedExportsWrappingImportedProxy () { - Assert.Equal(Export(new SpecializedExport(new Proxy(1))), Export(new SpecializedExport(new Proxy(1)))); + RegisterExport(typeof(Bar), _ => new SpecializedExport(new Proxy(1))); + Assert.Equal(Export(new Bar()), Export(new Bar())); } [Fact] public void GeneratesUniqueIdsForSpecializedExportsNotWrappingImportedProxy () { - Assert.NotEqual(Export(new SpecializedExport(new object())), Export(new SpecializedExport(new object()))); + RegisterExport(typeof(Foo), it => new SpecializedExport(it)); + Assert.NotEqual(Export(new Foo()), Export(new Foo())); + } + + [Fact] + public void ShortCircuitsRegisteredSpecializedExports () + { + var exported = new Foo(); + RegisterExport(typeof(Foo), it => new SpecializedExport(it)); + Assert.Equal(Export(exported), Export(exported)); } [Fact] @@ -87,10 +97,11 @@ public void InvokesExportFactoryCallbacks () { var exported = false; var disposed = false; - var id = Export(new object(), (_, _) => { + RegisterExport(typeof(Bar), null, (_, _) => { exported = true; return () => disposed = true; }); + var id = Export(new Bar()); Assert.True(exported); Assert.False(disposed); DisposeExported(id); @@ -154,7 +165,8 @@ public void UnwrapsResolvedSpecializedImportsOfValueType () public void UnwrapsResolvedSpecializedExports () { var exported = new Foo(); - Assert.Same(exported, Resolve(Export(new SpecializedExport(exported)))); + RegisterExport(typeof(Foo), it => new SpecializedExport(it)); + Assert.Same(exported, Resolve(Export(exported))); } [Fact] diff --git a/src/cs/Bootsharp.Common/Interop/Instances.cs b/src/cs/Bootsharp.Common/Interop/Instances.cs index e3c11a54..78cfcb15 100644 --- a/src/cs/Bootsharp.Common/Interop/Instances.cs +++ b/src/cs/Bootsharp.Common/Interop/Instances.cs @@ -12,15 +12,16 @@ namespace Bootsharp; public static class Instances { /// - /// Invoked on when registering the instance. + /// Invoked on when registering an exported instance. /// /// The unique identifier of the instance. - /// The registered instance. + /// The registered instance or the specialized wrapper, when applicable. /// The callback to invoke when disposing the instance. - public delegate Action ExportCallback (int id, T it) where T : class; + public delegate Action ExportCallback (int id, object it); private static readonly Dictionary importedById = []; private static readonly Dictionary> importers = []; + private static readonly Dictionary? spec, ExportCallback? cb)> exporters = []; private static readonly Dictionary exportedById = []; private static readonly Dictionary idByExported = new(ReferenceEqualityComparer.Instance); private static readonly Dictionary onDisposeById = []; @@ -47,20 +48,22 @@ public static class Instances /// /// Registers specified exported (C#) instance and returns the associated unique ID. /// Short-circuits already registered exported and imported instances. + /// Invokes applicable exporters registered with . /// /// The instance to register. - /// Callback to invoke when registering and disposing the instance. /// Unique ID associated with the registered instance. - public static int Export (T? it, ExportCallback? cb = null) where T : class + public static int Export (T? it) { if (it is null) return 0; if (it is JSProxy js) return js._id; if (it is Delegate { Target: JSProxy del }) return del._id; - if (it is SpecializedExport { _it: JSProxy ejs }) return ejs._id; - if (idByExported.TryGetValue(Key(it), out var id)) return id; + if (idByExported.TryGetValue(it, out var id)) return id; + exporters.TryGetValue(typeof(T), out var exporter); + var stored = exporter.spec is { } specialize ? specialize(it) : it; + if (stored is SpecializedExport { _it: JSProxy ejs }) return ejs._id; id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; - exportedById[idByExported[Key(it)] = id] = it; - if (cb != null) onDisposeById[id] = cb(id, it); + exportedById[idByExported[Key(stored)] = id] = stored; + if (exporter.cb is { } cb) onDisposeById[id] = cb(id, stored); return id; } @@ -91,6 +94,16 @@ public static void RegisterImport (Type type, Func factory) importers[type] = factory; } + /// + /// Registers special handling for exported (C#) instances of the specified type. + /// Specialized exports register their wrapper factory with the function. + /// Instances requiring custom callbacks on construction and disposal use delegate. + /// + public static void RegisterExport (Type type, Func? spec, ExportCallback? cb = null) + { + exporters[type] = (spec, cb); + } + /// /// Notifies that an imported interop instance with the specified ID is no longer used /// on the C# side and can be untracked. diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs index 2f5cd13a..38ae2258 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInstanceTest.cs @@ -96,7 +96,7 @@ public class Class } [Fact] - public void GeneratesSpecializedExportsForInstancesWithEvents () + public void RegistersExportsForInstancesWithEvents () { AddAssembly(With( """ @@ -114,19 +114,20 @@ public partial class Class Execute(); Contains( """ - internal static int Export (global::IExported it) => Export(it, static (_id, it) => { - it.Changed += HandleChanged; - return () => { - it.Changed -= HandleChanged; - }; - - void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.JS_Export_IExported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); - }); + Bootsharp.Instances.RegisterExport(typeof(global::IExported), null, static (_id, obj) => { + var it = (global::IExported)obj; + it.Changed += HandleChanged; + return () => { + it.Changed -= HandleChanged; + }; + + void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.JS_Export_IExported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); + }); """); } [Fact] - public void DoesNotGenerateDuplicateSpecializedExports () + public void DoesNotDuplicateExportsForInstanceWithEvents () { AddAssembly(With( """ @@ -143,7 +144,7 @@ public class Class } """)); Execute(); - Once(@"internal static int Export \(global::IBi it\)"); + Once(@"RegisterExport\(typeof\(global::IBi\)"); } [Fact] @@ -268,14 +269,15 @@ public sealed class JS_Import_Custom (int id) : global::CustomImport(id) """); Contains( """ - internal static int Export (global::Custom it) => Export(new global::CustomExport(it), static (_id, it) => { - it.AddedEvent += HandleAddedEvent; - return () => { - it.AddedEvent -= HandleAddedEvent; - }; - - void HandleAddedEvent () => Interop.JS_Export_Custom_BroadcastAddedEvent_Serialized(_id); - }); + Bootsharp.Instances.RegisterExport(typeof(global::Custom), static it => new global::CustomExport((global::Custom)it), static (_id, obj) => { + var it = (global::CustomExport)obj; + it.AddedEvent += HandleAddedEvent; + return () => { + it.AddedEvent -= HandleAddedEvent; + }; + + void HandleAddedEvent () => Interop.JS_Export_Custom_BroadcastAddedEvent_Serialized(_id); + }); """); } @@ -299,7 +301,7 @@ public class Class } """)); Execute(); - Contains("internal static int Export (global::IntEvent it) => Export(new global::EventExport(it));"); + Contains("Bootsharp.Instances.RegisterExport(typeof(global::IntEvent), static it => new global::EventExport((global::IntEvent)it));"); Contains("Instances.RegisterImport(typeof(global::IntEvent), static id => new global::Bootsharp.Generated.JS_Import_IntEvent(id));"); Contains( """ @@ -323,10 +325,10 @@ public void GeneratesForBuiltInSpecializations () [Export] public static CancellationToken Bar (CancellationToken ct) => default!; """)); Execute(); - Contains("int Export (global::System.Collections.Generic.ICollection it) => Export(new global::Bootsharp.Specialized.CollectionExport(it)"); - Contains("int Export (global::System.Collections.Generic.IList it) => Export(new global::Bootsharp.Specialized.ListExport(it)"); - Contains("int Export (global::System.Collections.Generic.IDictionary it) => Export(new global::Bootsharp.Specialized.DictionaryExport(it)"); - Contains("int Export (global::System.Threading.CancellationToken it) => Export(new global::Bootsharp.Specialized.CancellationTokenExport(it)"); + Contains("RegisterExport(typeof(global::System.Collections.Generic.ICollection), static it => new global::Bootsharp.Specialized.CollectionExport((global::System.Collections.Generic.ICollection)it));"); + Contains("RegisterExport(typeof(global::System.Collections.Generic.IList), static it => new global::Bootsharp.Specialized.ListExport((global::System.Collections.Generic.IList)it));"); + Contains("RegisterExport(typeof(global::System.Collections.Generic.IDictionary), static it => new global::Bootsharp.Specialized.DictionaryExport((global::System.Collections.Generic.IDictionary)it));"); + Contains("RegisterExport(typeof(global::System.Threading.CancellationToken), static it => new global::Bootsharp.Specialized.CancellationTokenExport((global::System.Threading.CancellationToken)it), static (_id, obj) => {"); Contains("class JS_Import_System_Collections_Generic_ICollection_Of_System_Int32 (int id) : global::Bootsharp.Specialized.CollectionImport"); Contains("class JS_Import_System_Collections_Generic_IList_Of_System_Int32 (int id) : global::Bootsharp.Specialized.ListImport"); Contains("class JS_Import_System_Collections_Generic_IDictionary_Of_System_Int32_And_System_String (int id) : global::Bootsharp.Specialized.DictionaryImport"); diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index c94f69fe..fe1d267d 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs @@ -20,7 +20,7 @@ namespace Bootsharp.Generated; public static partial class Instances { - internal static int Export (T? it, Bootsharp.Instances.ExportCallback? cb = null) where T : class => Bootsharp.Instances.Export(it, cb); + internal static int Export (T? it) => Bootsharp.Instances.Export(it); internal static T Exported (int id) where T : class => Bootsharp.Instances.Exported(id); internal static T? Resolve (int id) => Bootsharp.Instances.Resolve(id); @@ -36,7 +36,11 @@ internal static void RegisterImports () {{Fmt(its.Where(i => i.IK == InteropKind.Import).Select(EmitImporter), 2)}} } - {{Fmt(its.Where(i => i.Exporter != null).Select(EmitExporter), 1, "\n\n")}} + [ModuleInitializer] + internal static void RegisterExports () + { + {{Fmt(its.Where(i => i.Exporter != null).Select(EmitExporter), 2)}} + } [JSExport] private static void DisposeExported (int id) => Bootsharp.Instances.DisposeExported(id); [JSImport("instances.disposeImported", "Bootsharp")] private static partial void NotifyImportedDisposed (int id); @@ -56,11 +60,13 @@ private static string EmitImporter (InstanceMeta it) private static string EmitExporter (InstanceMeta it) { var evt = it.Members.OfType().ToArray(); - var arg = it.Proxy is SpecializedProxy sp ? $"new {sp.Export.Syntax}(it)" : "it"; - if (evt.Length == 0) return $"internal static int {it.Exporter} ({it.Syntax} it) => Export({arg});"; + var sp = it.Proxy as SpecializedProxy; + var spec = sp != null ? $"static it => new {sp.Export.Syntax}(({it.Syntax})it)" : "null"; + if (evt.Length == 0) return $"Bootsharp.Instances.RegisterExport(typeof({it.Syntax}), {spec});"; return $$""" - internal static int {{it.Exporter}} ({{it.Syntax}} it) => Export({{arg}}, static (_id, it) => { + Bootsharp.Instances.RegisterExport(typeof({{it.Syntax}}), {{spec}}, static (_id, obj) => { + var it = ({{sp?.Export.Syntax ?? it.Syntax}})obj; {{Fmt(evt.Select(e => $"it.{e.Name} += Handle{e.Name};"))}} return () => { {{Fmt(evt.Select(e => $"it.{e.Name} -= Handle{e.Name};"), 2)}} diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 09d2186a..39018b8e 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.9.0-alpha.29 + 0.9.0-alpha.30 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com