From d3ecc961a7ac47c2878d462a94cb9f4712ab241c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 2 May 2026 15:36:49 +0300 Subject: [PATCH 01/20] update docs --- README.md | 10 +++--- docs/.vitepress/config.ts | 4 +-- docs/guide/declarations.md | 2 +- docs/guide/events.md | 2 +- docs/guide/extensions/dependency-injection.md | 2 +- docs/guide/getting-started.md | 2 +- ...terop-interfaces.md => interop-modules.md} | 33 +++++++++++++------ docs/guide/namespaces.md | 16 ++++----- docs/guide/{emit-prefs.md => preferences.md} | 2 +- docs/index.md | 26 +++++++-------- src/js/test/spec/interop.spec.ts | 8 ++--- 11 files changed, 60 insertions(+), 47 deletions(-) rename docs/guide/{interop-interfaces.md => interop-modules.md} (51%) rename docs/guide/{emit-prefs.md => preferences.md} (98%) diff --git a/README.md b/README.md index 50fc5b9a..74a42dee 100644 --- a/README.md +++ b/README.md @@ -24,17 +24,17 @@ Facilitating high-level interoperation between C# and TypeScript, Bootsharp lets ✨ High-level C# <-> TypeScript interop -📦 Embeds binaries to single-file ES module +📦 Produces modern ES package with modules and types 🗺️ Works in browsers and JS runtimes (Node, Deno, Bun) -⚡ Generates bindings and types over C# interfaces +🧩 Generates bindings over abstract C# API surfaces -🏷️ Supports interop over object instances +🧬 Intelligently handles any type on the interop surface -🛠️ Allows customizing emitted bindings +🛠️ Allows customizing emitted interop API patterns -🔥 Supports multi-threading, NativeAOT-LLVM, trimming +⚡ Compiles optimized WASM with NativeAOT-LLVM and Binaryen ## 🎬 Get Started diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 0951b5a0..90764503 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -56,9 +56,9 @@ export default defineConfig({ { text: "Namespaces", link: "/guide/namespaces" }, { text: "Events", link: "/guide/events" }, { text: "Serialization", link: "/guide/serialization" }, - { text: "Interop Interfaces", link: "/guide/interop-interfaces" }, + { text: "Interop Modules", link: "/guide/interop-modules" }, { text: "Interop Instances", link: "/guide/interop-instances" }, - { text: "Emit Preferences", link: "/guide/emit-prefs" }, + { text: "Preferences", link: "/guide/preferences" }, { text: "Build Configuration", link: "/guide/build-config" }, { text: "Sideloading Binaries", link: "/guide/sideloading" }, { text: "NativeAOT-LLVM", link: "/guide/llvm" } diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index f967e4d0..82461e51 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -191,4 +191,4 @@ export namespace Foo { ## Configuring Type Mappings -You can override which type declaration are generated for associated C# types via `Type` patterns of [emit preferences](/guide/emit-prefs). +You can override which type declaration are generated for associated C# types via `Type` patterns of [emit preferences](/guide/preferences). diff --git a/docs/guide/events.md b/docs/guide/events.md index 83803f4a..feaa3baa 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -40,7 +40,7 @@ Program.onSomethingChanged.broadcast("updated"); Bootsharp supports all common event types: `Action`, `EventHandler`, and any custom delegate types without a return type. -Events on the [interop interfaces](/guide/interop-interfaces) are picked up automatically, so you don't have to annotate them. +Events on [modules](/guide/interop-modules) and [instances](/guide/interop-instances) are picked up automatically, so you don't have to annotate them. ## React Event Hooks diff --git a/docs/guide/extensions/dependency-injection.md b/docs/guide/extensions/dependency-injection.md index 20d27b57..67582f8b 100644 --- a/docs/guide/extensions/dependency-injection.md +++ b/docs/guide/extensions/dependency-injection.md @@ -1,6 +1,6 @@ # Dependency Injection -When using [interop interfaces](/guide/interop-interfaces), it's convenient to use a dependency injection mechanism to automatically route generated interop implementations for the services that needs them. +When using [modules](/guide/interop-modules), it's convenient to use a dependency injection mechanism to automatically route generated module implementations for the services that needs them. Reference `Bootsharp.Inject` extension in the project configuration: diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 528d51e8..816f6b1d 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -47,7 +47,7 @@ public static partial class Program ``` ::: info NOTE -Authoring interop via static methods is impractical for large API surfaces—it's shown here only as a simple way to get started. For real projects, consider using [interop interfaces](/guide/interop-interfaces) instead. +Authoring interop via static methods is impractical for large API surfaces—it's shown here only as a simple way to get started. For real projects, consider using [modules](/guide/interop-modules) instead. ::: ## Compile ES Module diff --git a/docs/guide/interop-interfaces.md b/docs/guide/interop-modules.md similarity index 51% rename from docs/guide/interop-interfaces.md rename to docs/guide/interop-modules.md index 06a14b91..8090c982 100644 --- a/docs/guide/interop-interfaces.md +++ b/docs/guide/interop-modules.md @@ -1,8 +1,8 @@ -# Interop Interfaces +# Interop Modules -Instead of manually authoring a binding for each member, let Bootsharp generate them automatically using the `[Import]` and `[Export]` assembly attributes. +Instead of manually authoring a binding for each member, let Bootsharp generate them automatically using the `[Import]` and `[Export]` assembly attributes. The type listed under each attribute defines an *interop module*. -For example, say we have a JavaScript UI (frontend) with a setting stored on the JS side, and a C# domain layer (backend) that wants to expose state changes back to JavaScript. You can describe the imported frontend APIs like this: +For example, say we have a JavaScript UI (frontend) with a setting stored on the JS side, and a C# domain layer (backend) that wants to expose state changes back to JavaScript. You can describe the imported frontend module like this: ```csharp interface IFrontend @@ -11,7 +11,7 @@ interface IFrontend } ``` -Now, add the interface type to the JS import list: +Now, add the module type to the JS import list: ```csharp [assembly: Import(typeof(IFrontend))] @@ -25,10 +25,12 @@ export namespace Frontend { } ``` -Now, export the backend contract to JavaScript: +Imported modules must be interfaces, since Bootsharp generates the C# implementation that calls into JavaScript. + +Now, define the backend contract to expose to JavaScript. An exported module can be either an interface or a non-static class — pick whichever fits your backend best: ```csharp -interface IBackend +public interface IBackend { event Action OnDataChanged; Data? Current { get; set; } @@ -36,13 +38,24 @@ interface IBackend } ``` -Export the interface to JavaScript: +```csharp +public class Backend +{ + public event Action? OnDataChanged; + public Data? Current { get; set; } + public void AddData (Data data) { /* ... */ } +} +``` + +Export the module to JavaScript: ```csharp [assembly: Export(typeof(IBackend))] +// or +[assembly: Export(typeof(Backend))] ``` -This will produce the following spec to be consumed on the JavaScript side: +Either form produces the following spec to be consumed on the JavaScript side: ```ts export namespace Backend { @@ -52,10 +65,10 @@ export namespace Backend { } ``` -Imported interface events work the other way around: declare a real C# event on the interface, and Bootsharp will generate a JavaScript `EventBroadcaster` plus a regular subscribable event on the generated C# implementation. +Imported module events work the other way around: declare a real C# event on the interface, and Bootsharp will generate a JavaScript `EventBroadcaster` plus a regular subscribable event on the generated C# implementation. To make Bootsharp automatically inject and initialize the generated interop implementations, use the [dependency injection](/guide/extensions/dependency-injection) extension. ::: tip Example -Find an example of using interop interfaces in the [React sample](https://github.com/elringus/bootsharp/tree/main/samples/react). +Find an example of using modules in the [React sample](https://github.com/elringus/bootsharp/tree/main/samples/react). ::: diff --git a/docs/guide/namespaces.md b/docs/guide/namespaces.md index fa592c1c..b411fc90 100644 --- a/docs/guide/namespaces.md +++ b/docs/guide/namespaces.md @@ -1,10 +1,10 @@ # Namespaces -Bootsharp maps generated binding APIs based on the name of the associated C# types. The rules are a bit different for static interop methods, interop interfaces and types. +Bootsharp maps binding APIs based on the fully qualified name of the C# types. -## Static Methods +## Static Members -Full type name (including namespace) of the declaring type of the static interop method is mapped into JavaScript object name: +Full type name (including namespace) of the declaring type of the static member is mapped into JavaScript object name: ```csharp class Class { [Export] static void Method() {} } @@ -37,20 +37,20 @@ import { Foo } from "bootsharp"; Foo.Class.Nested.method(); ``` -## Interop Interfaces +## Interop Modules -When generating bindings for [interop interfaces](/guide/interop-interfaces), it's assumed the interface name has "I" prefix, so the associated implementation name will have first character removed. In case interface is declared under namespace, it'll be mirrored in JavaScript. +When generating bindings for [modules](/guide/interop-modules), an interface name is assumed to have an "I" prefix, so the associated JavaScript name will have the first character removed. Class modules keep their name as-is. In either case, if the type is declared under a namespace, it'll be mirrored in JavaScript. ```csharp [Export( typeof(IExported), typeof(Foo.IExported), - typeof(Foo.Bar.IExported) + typeof(Foo.Bar.Exported) )] interface IExported { void Method(); } namespace Foo { interface IExported { void Method(); } } -namespace Foo.Bar { interface IExported { void Method(); } } +namespace Foo.Bar { class Exported { public void Method() {} } } ``` ```ts @@ -88,4 +88,4 @@ function methodImpl(r: Record): Foo.Record { ## Configuring Namespaces -You can control how namespaces are generated via `Space` patterns of [emit preferences](/guide/emit-prefs). +You can control how namespaces are generated with `Space` option in [preferences](/guide/preferences). diff --git a/docs/guide/emit-prefs.md b/docs/guide/preferences.md similarity index 98% rename from docs/guide/emit-prefs.md rename to docs/guide/preferences.md index 9a05eaa2..0761c09e 100644 --- a/docs/guide/emit-prefs.md +++ b/docs/guide/preferences.md @@ -1,4 +1,4 @@ -# Emit Preferences +# Preferences Use `[Preferences]` assembly attribute to customize Bootsharp behaviour at build time when the interop code is emitted. It has several properties that takes array of `(pattern, replacement)` strings, which are feed to [Regex.Replace](https://docs.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.replace?view=net-6.0#system-text-regularexpressions-regex-replace(system-string-system-string-system-string)) when emitted associated code. Each consequent pair is tested in order; on first match the result replaces the default. diff --git a/docs/index.md b/docs/index.md index 6e315db2..4c677386 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ titleTemplate: Bootsharp • :title hero: name: Bootsharp text: Use C# in web apps with comfort - tagline: Author the domain in C#, while fully leveraging the modern JavaScript frontend ecosystem. + tagline: Author the domain in C#, while fully leveraging the modern TypeScript frontend ecosystem. actions: - theme: brand text: Get Started @@ -31,7 +31,7 @@ hero:

High-level Interoperation

-

Generates JavaScript bindings and type declarations for your C# APIs, enabling seamless interop between domain and UI.

+

Generates JavaScript bindings and TypeScript declarations for your C# APIs, enabling seamless interop between domain and UI.

@@ -39,9 +39,9 @@ hero:
📦
-

Embed or Sideload

+

Modern ES Package

-

Choose between embedding all C# binaries into a single-file ES module for portability or sideloading for performance and size.

+

Just run "dotnet publish" and get a full-fledged ES package with "package.json" included—directly importable into your web project.

@@ -60,20 +60,20 @@ hero:
@@ -90,10 +90,10 @@ hero:
diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index fd39faab..543ebc8d 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -223,7 +223,7 @@ describe("while bootsharp is booted", () => { expect(Test.Invokable.getIdxEnumOne() === Test.IdxEnum.One).toBeTruthy(); expect(Test.Invokable.getIdxEnumOne() === Test.IdxEnum.Two).not.toBeTruthy(); }); - it("can interop with imported static interfaces", async () => { + it("can interop with imported modules", async () => { Test.Types.ImportedStatic.record = { id: "baz" }; expect(Test.Types.Interfaces.getImportedStaticRecordIdAndSet({ id: "qux" })).toStrictEqual("baz"); expect(Test.Types.ImportedStatic.record).toStrictEqual({ id: "qux" }); @@ -241,7 +241,7 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); Test.Types.Interfaces.onImportedStaticRecordEchoed.unsubscribe(handler); }); - it("can interop with imported interface instances", async () => { + it("can interop with imported instances", async () => { Test.Types.ImportedStatic.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); return new Imported(arg); @@ -265,7 +265,7 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); Test.Types.Interfaces.onImportedInstanceRecordEchoed.unsubscribe(handler); }); - it("can interop with exported static interfaces", () => { + it("can interop with exported modules", () => { const record = { id: "foo" }; const handler = vi.fn(); Test.Types.ExportedStatic.onRecordChanged.subscribe(handler); @@ -280,7 +280,7 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); Test.Types.ExportedStatic.onRecordChanged.unsubscribe(handler); }); - it("can interop with exported interface instances", async () => { + it("can interop with exported instances", async () => { const exported = await Test.Types.ExportedStatic.getInstanceAsync("bar"); const handler = vi.fn(); expect(exported.getInstanceArg()).toStrictEqual("bar"); From 71b09b7e05c6cf390b35aa792e7623081fada505 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 2 May 2026 16:42:58 +0300 Subject: [PATCH 02/20] iter --- .../Bootsharp.Common.Test/InterfacesTest.cs | 20 -- src/cs/Bootsharp.Common.Test/ModulesTest.cs | 28 +++ .../Attributes/ExportAttribute.cs | 12 +- .../Attributes/ImportAttribute.cs | 8 +- .../Interop/ExportInterface.cs | 9 - .../Bootsharp.Common/Interop/ExportModule.cs | 8 + .../Interop/ImportInterface.cs | 8 - .../Bootsharp.Common/Interop/ImportModule.cs | 7 + src/cs/Bootsharp.Common/Interop/Interfaces.cs | 44 ---- src/cs/Bootsharp.Common/Interop/Modules.cs | 40 ++++ .../Bootsharp.Inject.Test/ExtensionsTest.cs | 4 +- src/cs/Bootsharp.Inject/Extensions.cs | 12 +- .../Bootsharp.Publish.Test/Emit/EmitTest.cs | 9 +- .../Emit/InstancesTest.cs | 106 ++++++++++ .../Emit/InteropTest.cs | 10 +- .../{InterfacesTest.cs => ModulesTest.cs} | 196 ++++++++++-------- .../Pack/BindingTest.cs | 10 +- .../Pack/DeclarationTest.cs | 8 +- .../Common/Meta/MemberMeta.cs | 8 +- .../SolutionInspector/InspectionReporter.cs | 12 +- .../SolutionInspector/SolutionInspection.cs | 22 +- .../Emit/InstanceGenerator.cs | 83 ++++++++ .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 149 +++++++++++++ .../TypeDeclarationGenerator.cs | 2 +- src/cs/Bootsharp/Build/Bootsharp.targets | 12 +- .../cs/Test.Types/Interfaces/Interfaces.cs | 2 +- 26 files changed, 602 insertions(+), 227 deletions(-) delete mode 100644 src/cs/Bootsharp.Common.Test/InterfacesTest.cs create mode 100644 src/cs/Bootsharp.Common.Test/ModulesTest.cs delete mode 100644 src/cs/Bootsharp.Common/Interop/ExportInterface.cs create mode 100644 src/cs/Bootsharp.Common/Interop/ExportModule.cs delete mode 100644 src/cs/Bootsharp.Common/Interop/ImportInterface.cs create mode 100644 src/cs/Bootsharp.Common/Interop/ImportModule.cs delete mode 100644 src/cs/Bootsharp.Common/Interop/Interfaces.cs create mode 100644 src/cs/Bootsharp.Common/Interop/Modules.cs create mode 100644 src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs rename src/cs/Bootsharp.Publish.Test/Emit/{InterfacesTest.cs => ModulesTest.cs} (59%) create mode 100644 src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs create mode 100644 src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs diff --git a/src/cs/Bootsharp.Common.Test/InterfacesTest.cs b/src/cs/Bootsharp.Common.Test/InterfacesTest.cs deleted file mode 100644 index 77cd869d..00000000 --- a/src/cs/Bootsharp.Common.Test/InterfacesTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Bootsharp.Common.Test; - -public class InterfacesTest -{ - [Fact] - public void RegistersExports () - { - var export = new ExportInterface(typeof(IBackend), null); - Interfaces.Register(typeof(Backend), export); - Assert.Equal(typeof(IBackend), Interfaces.Exports[typeof(Backend)].Interface); - } - - [Fact] - public void RegistersImports () - { - var import = new ImportInterface(new Frontend()); - Interfaces.Register(typeof(IFrontend), import); - Assert.IsType(Interfaces.Imports[typeof(IFrontend)].Instance); - } -} diff --git a/src/cs/Bootsharp.Common.Test/ModulesTest.cs b/src/cs/Bootsharp.Common.Test/ModulesTest.cs new file mode 100644 index 00000000..2aee1928 --- /dev/null +++ b/src/cs/Bootsharp.Common.Test/ModulesTest.cs @@ -0,0 +1,28 @@ +namespace Bootsharp.Common.Test; + +public class ModulesTest +{ + [Fact] + public void RegistersInterfaceExport () + { + var export = new ExportModule(typeof(IBackend), null); + Modules.Register(typeof(Backend), export); + Assert.Equal(typeof(IBackend), Modules.Exports[typeof(Backend)].Handler); + } + + [Fact] + public void RegistersClassExport () + { + var export = new ExportModule(typeof(Backend), null); + Modules.Register(typeof(Backend), export); + Assert.Equal(typeof(Backend), Modules.Exports[typeof(Backend)].Handler); + } + + [Fact] + public void RegistersImport () + { + var import = new ImportModule(new Frontend()); + Modules.Register(typeof(IFrontend), import); + Assert.IsType(Modules.Imports[typeof(IFrontend)].Instance); + } +} diff --git a/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs index e9a13342..598e5cf7 100644 --- a/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs @@ -3,7 +3,7 @@ namespace Bootsharp; /// /// When applied to a static method makes it invokable in JavaScript. /// When applied to a static event allows JavaScript consumers subscribe to it. -/// When applied to WASM entry point assembly, specified interfaces will +/// When applied to WASM entry point assembly, specified module class or interfaces types will /// be automatically exported for consumption on the JavaScript side. /// /// @@ -22,11 +22,11 @@ namespace Bootsharp; /// [Export] /// public static event Action OnSomething; /// -/// Expose "IHandlerA" and "IHandlerB" C# APIs to JavaScript and wrap invocations in "Utils.Try()": +/// Expose "IService" and "Handler" C# API surfaces to JavaScript and wrap invocations in "Utils.Try()": /// /// [assembly: Export( -/// typeof(IHandlerA), -/// typeof(IHandlerB), +/// typeof(IService), +/// typeof(Handler), /// invokePattern = "(.+)", /// invokeReplacement = "Utils.Try(() => $1)" /// )] @@ -36,10 +36,10 @@ namespace Bootsharp; public sealed class ExportAttribute : Attribute { /// - /// When applied to assembly, lists the interface types to generated export bindings for. + /// When applied to assembly, lists the module (class or interface) types to generated export bindings for. /// public Type[] Types { get; } - /// The interface types to generate export bindings for (when applied to assembly). + /// The module types to generate export bindings for (when applied to assembly). public ExportAttribute (params Type[] types) => Types = types; } diff --git a/src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs index 2bd597b2..7e30ca31 100644 --- a/src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs @@ -3,8 +3,8 @@ namespace Bootsharp; /// /// When applied to a static partial method binds it with a JavaScript function. /// When applied to a static event allows JavaScript consumers broadcast it. -/// When applied to WASM entry point assembly, JavaScript bindings for the specified interfaces -/// will be automatically generated for consumption on the C# side. +/// When applied to WASM entry point assembly, JavaScript bindings for the specified module +/// interfaces will be automatically generated for consumption on the C# side. /// /// /// When used on the assembly level, generated bindings have to be implemented on the JavaScript side. @@ -34,10 +34,10 @@ namespace Bootsharp; public sealed class ImportAttribute : Attribute { /// - /// When applied to assembly, lists the interface types to generated import bindings for. + /// When applied to assembly, lists the module interface types to generate import bindings for. /// public Type[] Types { get; } - /// The interface types to generate import bindings for (when applied to assembly). + /// The module interface types to generate import bindings for (when applied to assembly). public ImportAttribute (params Type[] types) => Types = types; } diff --git a/src/cs/Bootsharp.Common/Interop/ExportInterface.cs b/src/cs/Bootsharp.Common/Interop/ExportInterface.cs deleted file mode 100644 index f71ceb85..00000000 --- a/src/cs/Bootsharp.Common/Interop/ExportInterface.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bootsharp; - -/// -/// Metadata about generated interop class for an interface supplied -/// under . -/// -/// Type of the exported interface. -/// Takes export interface implementation instance; returns interop class instance. -public record ExportInterface (Type Interface, Func Factory); diff --git a/src/cs/Bootsharp.Common/Interop/ExportModule.cs b/src/cs/Bootsharp.Common/Interop/ExportModule.cs new file mode 100644 index 00000000..0ced35e8 --- /dev/null +++ b/src/cs/Bootsharp.Common/Interop/ExportModule.cs @@ -0,0 +1,8 @@ +namespace Bootsharp; + +/// +/// Metadata about generated interop class for a module supplied under . +/// +/// Type of the exported module's handler (interface or class). +/// Takes export module handler instance; returns interop class instance. +public record ExportModule (Type Handler, Func Factory); diff --git a/src/cs/Bootsharp.Common/Interop/ImportInterface.cs b/src/cs/Bootsharp.Common/Interop/ImportInterface.cs deleted file mode 100644 index 8c299ff0..00000000 --- a/src/cs/Bootsharp.Common/Interop/ImportInterface.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Bootsharp; - -/// -/// Metadata about generated implementation for interface supplied -/// under . -/// -/// Import interface implementation instance. -public record ImportInterface (object Instance); diff --git a/src/cs/Bootsharp.Common/Interop/ImportModule.cs b/src/cs/Bootsharp.Common/Interop/ImportModule.cs new file mode 100644 index 00000000..4d1c6b09 --- /dev/null +++ b/src/cs/Bootsharp.Common/Interop/ImportModule.cs @@ -0,0 +1,7 @@ +namespace Bootsharp; + +/// +/// Metadata about generated implementation for module supplied under . +/// +/// Imported module implementation instance. +public record ImportModule (object Instance); diff --git a/src/cs/Bootsharp.Common/Interop/Interfaces.cs b/src/cs/Bootsharp.Common/Interop/Interfaces.cs deleted file mode 100644 index 1874b3d2..00000000 --- a/src/cs/Bootsharp.Common/Interop/Interfaces.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Bootsharp; - -/// -/// Provides access to generated interop types for interfaces supplied -/// under and . -/// -/// -/// Exported interfaces are C# APIs invoked in JavaScript. Their C# implementation -/// (handler) is assumed to be supplied via -/// on program boot (usually via DI), before associated APIs are accessed in JavaScript. -/// Imported interfaces are JavaScript APIs invoked in C#. Their implementation -/// is instantiated in generated code and is available before program start. -/// -[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] -public static class Interfaces -{ - /// - /// Interop classes generated for interfaces - /// mapped by the generated class type. Expected to have - /// invoked with the interface implementation (handler) before associated API usage in JS. - /// - public static IReadOnlyDictionary Exports => exports; - /// - /// Implementations generated for interop - /// interfaces mapped by the interface type of the associated implementation. - /// - public static IReadOnlyDictionary Imports => imports; - - private static readonly Dictionary exports = new(); - private static readonly Dictionary imports = new(); - - /// - /// Maps type of the generated export interop class to the associated metadata. - /// Invoked by the generated code before program start. - /// - public static void Register (Type @class, ExportInterface export) => exports[@class] = export; - /// - /// Maps interface type of the generated import implementation to the associated metadata. - /// Invoked by the generated code before program start. - /// - public static void Register (Type @interface, ImportInterface import) => imports[@interface] = import; -} diff --git a/src/cs/Bootsharp.Common/Interop/Modules.cs b/src/cs/Bootsharp.Common/Interop/Modules.cs new file mode 100644 index 00000000..21e52764 --- /dev/null +++ b/src/cs/Bootsharp.Common/Interop/Modules.cs @@ -0,0 +1,40 @@ +namespace Bootsharp; + +/// +/// Provides access to generated interop types for modules supplied +/// under and . +/// +/// +/// Exported modules are C# APIs invoked in JavaScript. Their C# implementation +/// (handler) is assumed to be supplied via +/// on program start (usually via DI), before associated APIs are accessed in JavaScript. +/// Imported modules are JavaScript APIs invoked in C#. Their implementation +/// is instantiated in generated code and is available before program start. +/// +public static class Modules +{ + /// + /// Export modules metadata generated for types (classes or interfaces) specified under + /// mapped by the generated wrapper type. + /// + public static IReadOnlyDictionary Exports => exports; + /// + /// Import modules metadata generated for interface types specified under + /// assembly attribute mapped by the specified interface type. + /// + public static IReadOnlyDictionary Imports => imports; + + private static readonly Dictionary exports = new(); + private static readonly Dictionary imports = new(); + + /// + /// Maps type of the generated export module wrapper to the associated export module metadata. + /// Invoked by the generated code before program start. + /// + public static void Register (Type wrapper, ExportModule export) => exports[wrapper] = export; + /// + /// Maps interface type of the imported module to the associated import module metadata. + /// Invoked by the generated code before program start. + /// + public static void Register (Type @interface, ImportModule import) => imports[@interface] = import; +} diff --git a/src/cs/Bootsharp.Inject.Test/ExtensionsTest.cs b/src/cs/Bootsharp.Inject.Test/ExtensionsTest.cs index 44c35b03..86d2337e 100644 --- a/src/cs/Bootsharp.Inject.Test/ExtensionsTest.cs +++ b/src/cs/Bootsharp.Inject.Test/ExtensionsTest.cs @@ -37,7 +37,7 @@ public void WhenMissingRequiredDependencyErrorIsThrown () // emulates auto-generated code behaviour on module initialization private static void AddAutogenerated () { - Interfaces.Register(typeof(JSBackend), new ExportInterface(typeof(IBackend), h => new JSBackend((IBackend)h))); - Interfaces.Register(typeof(IFrontend), new ImportInterface(new JSFrontend())); + Modules.Register(typeof(JSBackend), new ExportModule(typeof(IBackend), h => new JSBackend((IBackend)h))); + Modules.Register(typeof(IFrontend), new ImportModule(new JSFrontend())); } } diff --git a/src/cs/Bootsharp.Inject/Extensions.cs b/src/cs/Bootsharp.Inject/Extensions.cs index e9392a8c..bf2d7fbf 100644 --- a/src/cs/Bootsharp.Inject/Extensions.cs +++ b/src/cs/Bootsharp.Inject/Extensions.cs @@ -12,13 +12,13 @@ public static class Extensions /// public static IServiceCollection AddBootsharp (this IServiceCollection services) { - foreach (var (impl, binding) in Interfaces.Exports) + foreach (var (impl, binding) in Modules.Exports) services.AddSingleton(impl, provider => { - var handler = provider.GetService(binding.Interface); - if (handler is null) throw new Error($"Failed to run Bootsharp: '{binding.Interface}' dependency is not registered."); - return binding.Factory(provider.GetRequiredService(binding.Interface)); + var handler = provider.GetService(binding.Handler); + if (handler is null) throw new Error($"Failed to run Bootsharp: '{binding.Handler}' dependency is not registered."); + return binding.Factory(provider.GetRequiredService(binding.Handler)); }); - foreach (var (api, binding) in Interfaces.Imports) + foreach (var (api, binding) in Modules.Imports) services.AddSingleton(api, binding.Instance); return services; } @@ -28,7 +28,7 @@ public static IServiceCollection AddBootsharp (this IServiceCollection services) /// public static IServiceProvider RunBootsharp (this IServiceProvider provider) { - foreach (var (impl, _) in Interfaces.Exports) + foreach (var (impl, _) in Modules.Exports) provider.GetRequiredService(impl); return provider; } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs index d02cc70d..ead128b2 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs @@ -3,12 +3,14 @@ namespace Bootsharp.Publish.Test; public class EmitTest : TaskTest { protected BootsharpEmit Task { get; } - protected string GeneratedInterfaces => ReadProjectFile(interfacesPath); protected string GeneratedSerializer => ReadProjectFile(serializerPath); + protected string GeneratedInstances => ReadProjectFile(instancesPath); + protected string GeneratedModules => ReadProjectFile(modulesPath); protected string GeneratedInterop => ReadProjectFile(interopPath); - private string interfacesPath => $"{Project.Root}/Interfaces.g.cs"; private string serializerPath => $"{Project.Root}/Serializer.g.cs"; + private string instancesPath => $"{Project.Root}/Instances.g.cs"; + private string modulesPath => $"{Project.Root}/Modules.g.cs"; private string interopPath => $"{Project.Root}/Interop.g.cs"; public EmitTest () @@ -26,8 +28,9 @@ public override void Execute () private BootsharpEmit CreateTask () => new() { InspectedDirectory = Project.Root, EntryAssemblyName = "System.Runtime.dll", - InterfacesFilePath = interfacesPath, SerializerFilePath = serializerPath, + InstancesFilePath = instancesPath, + ModulesFilePath = modulesPath, InteropFilePath = interopPath, BuildEngine = Engine }; diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs new file mode 100644 index 00000000..ce1edf86 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs @@ -0,0 +1,106 @@ +namespace Bootsharp.Publish.Test; + +public class InstancesTest : EmitTest +{ + protected override string TestedContent => GeneratedInstances; + + [Fact] + public void GeneratesImportedInstanceInterface () + { + AddAssembly(With( + """ + public record Record; + + public interface IImported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Fun (string arg); + } + + public class Class + { + [Import] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); + } + """)); + Execute(); + Contains( + """ + namespace Bootsharp.Generated.Imports + { + public class JSImported (global::System.Int32 id) : global::IImported + { + internal readonly global::System.Int32 _id = id; + + ~JSImported() + { + global::Bootsharp.Instances.DisposeImported(_id); + global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + } + + public event global::System.Action OnRecordChanged; + internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); + public event global::IImported.SomethingChanged OnSomethingChanged; + internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); + global::Record? global::IImported.Record + { + get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(_id); + set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(_id, value); + } + void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); + } + } + """); + } + + [Fact] + public void DoesNotGenerateExportedInstanceInterface () + { + AddAssembly(With( + """ + public record Record; + + public interface IExported + { + delegate void SomethingChanged(); + + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; + + Record? Record { get; set; } + + void Fun (string arg); + } + + public class Class + { + [Export] public static IExported GetExported () => default; + } + """)); + Execute(); + DoesNotContain("JSExported"); + } + + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( + """ + public interface IExported { int Foo () => 0; } + public interface IImported { int Foo () => 0; } + + public class Class + { + [Export] public static IExported GetExported () => default; + [Import] public static IExported GetImported () => default; + } + """)); + Execute(); + DoesNotContain("Foo"); + } +} diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index 3a65a631..fb77719b 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -126,7 +126,7 @@ public void IgnoresEventsWithoutImportExportAttributes () } [Fact] - public void GeneratesForMethodsInStaticInterfaces () + public void GeneratesForMethodsInModules () { AddAssembly(With( """ @@ -171,7 +171,7 @@ public partial class Class } [Fact] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -251,7 +251,7 @@ public class Class } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -276,7 +276,7 @@ internal static unsafe void Initialize () """); Contains("void Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt (global::Space.Info obj) => Space_Exported_BroadcastEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Info));"); Contains("""[JSImport("Space.Exported.broadcastEvtSerialized", "Bootsharp")] internal static partial void Space_Exported_BroadcastEvt_Serialized ([JSMarshalAs] global::System.Int64 obj);"""); - Contains("[JSExport] internal static void Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt ([JSMarshalAs] global::System.Int64 obj) => ((global::Bootsharp.Generated.Imports.Space.JSImported)Interfaces.Imports[typeof(global::Space.IImported)].Instance).InvokeEvt(Serializer.Deserialize(obj, SerializerContext.Space_Info));"); + Contains("[JSExport] internal static void Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt ([JSMarshalAs] global::System.Int64 obj) => ((global::Bootsharp.Generated.Imports.Space.JSImported)Modules.Imports[typeof(global::Space.IImported)].Instance).InvokeEvt(Serializer.Deserialize(obj, SerializerContext.Space_Info));"); } [Fact] @@ -345,7 +345,7 @@ public class Class } [Fact] - public void DoesNotEmitDuplicateInterfaceRegistrations () + public void DoesNotEmitDuplicateModuleRegistrations () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs similarity index 59% rename from src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs rename to src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs index f41ee662..aea4c856 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs @@ -1,11 +1,11 @@ namespace Bootsharp.Publish.Test; -public class InterfacesTest : EmitTest +public class ModulesTest : EmitTest { - protected override string TestedContent => GeneratedInterfaces; + protected override string TestedContent => GeneratedModules; [Fact] - public void GeneratesImplementationForExportedStaticInterface () + public void GeneratesExportedInterfaceModule () { AddAssembly(With( """ @@ -34,12 +34,12 @@ public interface IExported """ namespace Bootsharp.Generated { - internal static class InterfaceRegistrations + internal static class ModuleRegistrations { [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () + internal static void RegisterModules () { - Interfaces.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportInterface(typeof(global::IExported), handler => new Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); + Modules.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::IExported), handler => new Bootsharp.Generated.Exports.JSExported((global::IExported)handler))); } } } @@ -72,28 +72,28 @@ public JSExported (global::IExported handler) } [Fact] - public void GeneratesImplementationForImportedStaticInterface () + public void GeneratesExportedClassModule () { AddAssembly(With( """ - [assembly:Import(typeof(IImported))] + [assembly:Export(typeof(Exported))] public record Record; - public interface IImported + public class Exported { - delegate void SomethingChanged(); + public delegate void SomethingChanged(); - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; + public event Action OnRecordChanged; + public event SomethingChanged OnSomethingChanged; - Record? Record { get; set; } + public Record? Record { get; set; } - void Inv (string? a); - Task InvAsync (); - Record? InvRecord (); - Task InvAsyncResult (); - string[] InvArray (int[] a); + public virtual void Inv (string? a) {} + public Task InvAsync () => Task.CompletedTask; + public Record? InvRecord () => null; + public Task InvAsyncResult () => Task.FromResult(""); + public string[] InvArray (int[] a) => []; } """)); Execute(); @@ -101,44 +101,67 @@ public interface IImported """ namespace Bootsharp.Generated { - internal static class InterfaceRegistrations + internal static class ModuleRegistrations { [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () + internal static void RegisterModules () { - Interfaces.Register(typeof(global::IImported), new ImportInterface(new Bootsharp.Generated.Imports.JSImported())); + Modules.Register(typeof(Bootsharp.Generated.Exports.JSExported), new ExportModule(typeof(global::Exported), handler => new Bootsharp.Generated.Exports.JSExported((global::Exported)handler))); } } } - namespace Bootsharp.Generated.Imports + namespace Bootsharp.Generated.Exports { - public class JSImported : global::IImported + public class JSExported { - public event global::System.Action OnRecordChanged; - internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); - public event global::IImported.SomethingChanged OnSomethingChanged; - internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); - global::Record? global::IImported.Record + private static global::Exported handler = null!; + + public JSExported (global::Exported handler) { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(value); + JSExported.handler = handler; + handler.OnRecordChanged += OnRecordChanged.Invoke; + handler.OnSomethingChanged += OnSomethingChanged.Invoke; } - void global::IImported.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Inv(a); - global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsync(); - global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvRecord(); - global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); - global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvArray(a); + + [Export] public static event global::System.Action OnRecordChanged; + [Export] public static event global::Exported.SomethingChanged OnSomethingChanged; + [Export] public static global::Record? GetPropertyRecord () => handler.Record; + [Export] public static void SetPropertyRecord (global::Record? value) => handler.Record = value; + [Export] public static void Inv (global::System.String? a) => handler.Inv(a); + [Export] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); + [Export] public static global::Record? InvRecord () => handler.InvRecord(); + [Export] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); + [Export] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); } } """); } [Fact] - public void GeneratesImplementationForImportedInstanceInterface () + public void DoesNotGenerateExportedStaticClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(StaticExported))] + + public static class StaticExported + { + public static void Inv () {} + } + """)); + Execute(); + DoesNotContain("JSStaticExported"); + Assert.Contains(Engine.Warnings, w => w.Contains("must be an interface or non-static class")); + } + + [Fact] + public void GeneratesImportedInterfaceModule () { AddAssembly(With( """ + [assembly:Import(typeof(IImported))] + public record Record; public interface IImported @@ -150,74 +173,70 @@ public interface IImported Record? Record { get; set; } - void Fun (string arg); - } - - public class Class - { - [Import] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); + void Inv (string? a); + Task InvAsync (); + Record? InvRecord (); + Task InvAsyncResult (); + string[] InvArray (int[] a); } """)); Execute(); Contains( """ - namespace Bootsharp.Generated.Imports + namespace Bootsharp.Generated { - public class JSImported (global::System.Int32 id) : global::IImported + internal static class ModuleRegistrations { - internal readonly global::System.Int32 _id = id; - - ~JSImported() + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () { - global::Bootsharp.Instances.DisposeImported(_id); - global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + Modules.Register(typeof(global::IImported), new ImportModule(new Bootsharp.Generated.Imports.JSImported())); } + } + } + namespace Bootsharp.Generated.Imports + { + public class JSImported : global::IImported + { public event global::System.Action OnRecordChanged; internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); public event global::IImported.SomethingChanged OnSomethingChanged; internal void InvokeOnSomethingChanged () => OnSomethingChanged?.Invoke(); global::Record? global::IImported.Record { - get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(_id); - set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(_id, value); + get => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(); + set => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(value); } - void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); + void global::IImported.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Inv(a); + global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsync(); + global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvRecord(); + global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); + global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_InvArray(a); } } """); } [Fact] - public void DoesNotGenerateImplementationForExportedInstanceInterface () + public void DoesNotGenerateImportedClassModule () { AddAssembly(With( """ - public record Record; - - public interface IExported - { - delegate void SomethingChanged(); - - event Action OnRecordChanged; - event SomethingChanged OnSomethingChanged; - - Record? Record { get; set; } - - void Fun (string arg); - } + [assembly:Import(typeof(Imported))] - public class Class + public class Imported { - [Export] public static IExported GetExported () => default; + public void Inv () {} } """)); Execute(); - DoesNotContain("JSExported"); + DoesNotContain("JSImported"); + Assert.Contains(Engine.Warnings, w => w.Contains("must be an interface")); } [Fact] - public void RespectsInterfaceNamespace () + public void RespectsModuleNamespace () { AddAssembly(With( """ @@ -236,13 +255,13 @@ public interface IImported { void Fun (Record a); } """ namespace Bootsharp.Generated { - internal static class InterfaceRegistrations + internal static class ModuleRegistrations { [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () + internal static void RegisterModules () { - Interfaces.Register(typeof(Bootsharp.Generated.Exports.Space.JSExported), new ExportInterface(typeof(global::Space.IExported), handler => new Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); - Interfaces.Register(typeof(global::Space.IImported), new ImportInterface(new Bootsharp.Generated.Imports.Space.JSImported())); + Modules.Register(typeof(Bootsharp.Generated.Exports.Space.JSExported), new ExportModule(typeof(global::Space.IExported), handler => new Bootsharp.Generated.Exports.Space.JSExported((global::Space.IExported)handler))); + Modules.Register(typeof(global::Space.IImported), new ImportModule(new Bootsharp.Generated.Imports.Space.JSImported())); } } } @@ -277,21 +296,30 @@ public void IgnoresImplementedInterfaceMethods () { AddAssembly(With( """ - [assembly:Export(typeof(IExportedStatic))] - [assembly:Import(typeof(IImportedStatic))] + [assembly:Export(typeof(IExported))] + [assembly:Import(typeof(IImported))] + + public interface IExported { int Foo () => 0; } + public interface IImported { int Foo () => 0; } + """)); + Execute(); + DoesNotContain("Foo"); + } - public interface IExportedStatic { int Foo () => 0; } - public interface IImportedStatic { int Foo () => 0; } - public interface IExportedInstanced { int Foo () => 0; } - public interface IImportedInstanced { int Foo () => 0; } + [Fact] + public void IgnoresStaticMembersOnExportedClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Exported))] - public class Class + public class Exported { - [Export] public static IExportedInstanced GetExported () => default; - [Import] public static IImportedInstanced GetImported () => default; + public static void StaticMethod () {} + public void Inst () {} } """)); Execute(); - DoesNotContain("Foo"); + DoesNotContain("StaticMethod"); } } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 772399e2..4447fe47 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -535,7 +535,7 @@ public void RespectsSpacePreferenceInStaticMembers () } [Fact] - public void RespectsSpacePreferenceInStaticInterfaces () + public void RespectsSpacePreferenceInModules () { AddAssembly(With( """ @@ -592,7 +592,7 @@ public static class Imports { [Import] public static void Fun () {} } } [Fact] - public void GeneratesForMethodsInStaticInterfaces () + public void GeneratesForMethodsInModules () { AddAssembly(With( """ @@ -662,7 +662,7 @@ class JSExported { } [Fact] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -766,7 +766,7 @@ class JSExported { } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -846,7 +846,7 @@ class JSExported { } [Fact] - public void DoesNotEmitDuplicateInterfaceRegistrations () + public void DoesNotEmitDuplicateModuleRegistrations () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index e411aafa..f9e13ff6 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -722,7 +722,7 @@ export interface Foo { } [Fact] - public void GeneratesForMethodsInStaticInterfaces () + public void GeneratesForMethodsInModules () { AddAssembly(With( """ @@ -788,7 +788,7 @@ export namespace Class { } [Fact] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -892,7 +892,7 @@ export namespace Class { } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -1125,7 +1125,7 @@ export namespace Fun.Class { } [Fact] - public void RespectsSpacePrefInStaticInterfaces () + public void RespectsSpacePrefInModules () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs index 64e53b1e..36db1f83 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs @@ -3,7 +3,7 @@ namespace Bootsharp.Publish; /// -/// An interop member declared on a static API surface or interop interface. +/// An interop member declared on a static, module or instanced API surface. /// internal abstract record MemberMeta { @@ -44,7 +44,7 @@ internal abstract record MemberMeta } /// -/// An interop method declared on a static API surface or interop interface. +/// An interop method declared on a static, module or instanced API surface. /// /// /// Return value of the method is described in . @@ -70,7 +70,7 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta } /// -/// An interop event declared on a static API surface or interop interface. +/// An interop event declared on a static, module or instanced API surface. /// internal sealed record EventMeta (EventInfo Info) : MemberMeta { @@ -85,7 +85,7 @@ internal sealed record EventMeta (EventInfo Info) : MemberMeta } /// -/// An interop property declared on an interop interface. +/// An interop property declared on a module or instanced API surface. /// internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta { diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs index f78cc280..14bf1658 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs @@ -18,17 +18,17 @@ public void Report (SolutionInspection inspection) private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) { - return inspection.StaticMembers.Select(m => m.Assembly) - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members.Select(m => m.Assembly))) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members.Select(m => m.Assembly))) + return inspection.Static.Select(m => m.Assembly) + .Concat(inspection.Modules.SelectMany(i => i.Members.Select(m => m.Assembly))) + .Concat(inspection.Instanced.SelectMany(i => i.Members.Select(m => m.Assembly))) .ToHashSet(); } private HashSet GetDiscoveredMembers (SolutionInspection inspection) { - return inspection.StaticMembers - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members)) + return inspection.Static + .Concat(inspection.Modules.SelectMany(i => i.Members)) + .Concat(inspection.Instanced.SelectMany(i => i.Members)) .Select(m => m.ToString()) .ToHashSet(); } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index bd6e1af5..11559ebe 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -14,22 +14,22 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable { /// - /// Interop interfaces specified under or - /// for which static bindings have to be emitted. + /// Static interop members, ie methods or events with + /// or found on user-defined static classes. /// - public required IReadOnlyCollection StaticInterfaces { get; init; } + public required IReadOnlyCollection Static { get; init; } /// - /// Interop interfaces found in interop method arguments or return values. - /// Such interfaces are considered instanced interop APIs, ie stateful objects with - /// interop methods and properties. Both members of - /// and can be sources of the instanced interfaces. + /// Interop modules specified under or + /// for which binding wrappers have to be emitted. /// - public required IReadOnlyCollection InstancedInterfaces { get; init; } + public required IReadOnlyCollection Modules { get; init; } /// - /// Static interop members, ie methods or events with - /// or found on user-defined static classes. + /// Interop interfaces found in interop method arguments or return values. + /// Such interfaces are considered instanced interop APIs, ie stateful objects with + /// interop methods and properties. Both members of + /// and can be sources of the instanced interfaces. /// - public required IReadOnlyCollection StaticMembers { get; init; } + public required IReadOnlyCollection Instanced { get; init; } /// /// All the types that cross the interop boundary or referenced by them. /// diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs new file mode 100644 index 00000000..dc4b0f58 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -0,0 +1,83 @@ +namespace Bootsharp.Publish; + +/// +/// Generates interop wrappers for imported instanced interfaces. +/// +internal sealed class InstanceGenerator +{ + private InterfaceMeta it = null!; + + public string Generate (SolutionInspection inspection) + { + var its = inspection.Instanced.Where(i => i.Interop == InteropKind.Import); + return + $""" + #nullable enable + #pragma warning disable + + {Fmt(its.Select(EmitWrapper), 0, "\n\n")} + """; + } + + private string EmitWrapper (InterfaceMeta it) => + $$""" + namespace {{(this.it = it).Namespace}} + { + public class {{it.Name}} (global::System.Int32 id) : {{it.TypeSyntax}} + { + internal readonly global::System.Int32 _id = id; + + ~{{it.Name}}() + { + global::Bootsharp.Instances.DisposeImported(_id); + global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + } + + {{Fmt(it.Members.Select(EmitMemberImport), 2)}} + } + } + """; + + private string EmitMemberImport (MemberMeta member) => member switch { + EventMeta evt => EmitEventImport(evt), + PropertyMeta prop => EmitPropertyImport(prop), + _ => EmitMethodImport((MethodMeta)member), + }; + + private string EmitEventImport (EventMeta evt) + { + var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullability(evt.Info)); + var args = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", evt.Arguments.Select(a => a.Name)); + return Fmt(0, + $"public event {type} {evt.Name};", + $"internal void Invoke{evt.Name} ({args}) => {evt.Name}?.Invoke({callArgs});" + ); + } + + private string EmitPropertyImport (PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.{prop.Space.Replace('.', '_')}"; + var getArgs = PrependIdArg(""); + var setArgs = PrependIdArg("value"); + return + $$""" + {{prop.Value.TypeSyntax}} {{it.TypeSyntax}}.{{prop.Name}} + { + {{Fmt( + prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, + prop.CanSet ? $"set => {space}_SetProperty{prop.Name}({setArgs});" : null + )}} + } + """; + } + + private string EmitMethodImport (MethodMeta method) + { + var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = PrependIdArg(string.Join(", ", method.Arguments.Select(a => a.Name))); + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + return $"{method.Value.TypeSyntax} {it.TypeSyntax}.{method.Name} ({args}) => " + + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; + } +} diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs new file mode 100644 index 00000000..9dd232cc --- /dev/null +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -0,0 +1,149 @@ +namespace Bootsharp.Publish; + +/// +/// Generates implementations for interop modules and wrappers for instanced interfaces. +/// +internal sealed class ModuleGenerator +{ + private InterfaceMeta it = null!; + + public string Generate (SolutionInspection inspection) => + $$""" + #nullable enable + #pragma warning disable + + namespace Bootsharp.Generated + { + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + {{Fmt(inspection.Modules.Select(EmitRegistration), 3)}} + } + } + } + + {{Fmt(inspection.Modules.Select(EmitModule), 0, "\n\n")}} + """; + + private string EmitRegistration (InterfaceMeta it) + { + var type = it.Interop == InteropKind.Import + ? $"typeof({it.TypeSyntax})" + : $"typeof({it.FullName})"; + var factory = it.Interop == InteropKind.Import + ? $"new ImportModule(new {it.FullName}())" + : $"new ExportModule(typeof({it.TypeSyntax}), handler => new {it.FullName}(({it.TypeSyntax})handler))"; + return $"Modules.Register({type}, {factory});"; + } + + private string EmitModule (InterfaceMeta it) + { + this.it = it; + if (it.Interop == InteropKind.Export) return EmitModuleExport(); + return EmitModuleImport(); + } + + private string EmitModuleExport () => + $$""" + namespace {{it.Namespace}} + { + public class {{it.Name}} + { + private static {{it.TypeSyntax}} handler = null!; + + public {{it.Name}} ({{it.TypeSyntax}} handler) + { + {{Fmt([ + $"{it.Name}.handler = handler;", + ..it.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") + ], 3)}} + } + + {{Fmt(it.Members.Select(EmitMemberExport), 2)}} + } + } + """; + + private string EmitModuleImport () => + $$""" + namespace {{it.Namespace}} + { + public class {{it.Name}} : {{it.TypeSyntax}} + { + {{Fmt(it.Members.Select(EmitMemberImport), 2)}} + } + } + """; + + private string EmitMemberExport (MemberMeta member) => member switch { + EventMeta evt => EmitEventExport(evt), + PropertyMeta prop => EmitPropertyExport(prop), + _ => EmitMethodExport((MethodMeta)member) + }; + + private string EmitMemberImport (MemberMeta member) => member switch { + EventMeta evt => EmitEventImport(evt), + PropertyMeta prop => EmitPropertyImport(prop), + _ => EmitMethodImport((MethodMeta)member), + }; + + private string EmitEventExport (EventMeta evt) + { + var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullability(evt.Info)); + return $"[Export] public static event {type} {evt.Name};"; + } + + private string EmitEventImport (EventMeta evt) + { + var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullability(evt.Info)); + var args = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", evt.Arguments.Select(a => a.Name)); + return Fmt(0, + $"public event {type} {evt.Name};", + $"internal void Invoke{evt.Name} ({args}) => {evt.Name}?.Invoke({callArgs});" + ); + } + + private string EmitPropertyExport (PropertyMeta prop) + { + var name = prop.Name; + var type = prop.Value.TypeSyntax; + var get = $"[Export] public static {type} GetProperty{name} () => handler.{name};"; + var set = $"[Export] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; + return Fmt(0, prop.CanGet ? get : null, prop.CanSet ? set : null); + } + + private string EmitPropertyImport (PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.{prop.Space.Replace('.', '_')}"; + return + $$""" + {{prop.Value.TypeSyntax}} {{it.TypeSyntax}}.{{prop.Name}} + { + {{Fmt( + prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, + prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(value);" : null + )}} + } + """; + } + + private string EmitMethodExport (MethodMeta method) + { + var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var sig = $"public static {method.Value.TypeSyntax} {method.Name} ({args})"; + var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); + return $"[Export] {sig} => handler.{method.Name}({callArgs});"; + } + + private string EmitMethodImport (MethodMeta method) + { + var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + return $"{method.Value.TypeSyntax} {it.TypeSyntax}.{method.Name} ({args}) => " + + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; + } +} diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 1444cb92..765b1ec6 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -21,7 +21,7 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) public string Generate (SolutionInspection inspection) { docs = new(inspection.Documentation); - instanced = [..inspection.InstancedInterfaces]; + instanced = [..inspection.Instanced]; types = inspection.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 0b3c9bcf..26115627 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -6,8 +6,9 @@ $(BsRoot)/llvm $(BsRoot)/tasks/Bootsharp.Publish.dll $(IntermediateOutputPath)bootsharp - $(BsIntermediateDir)/Interfaces.g.cs $(BsIntermediateDir)/Serializer.g.cs + $(BsIntermediateDir)/Instances.g.cs + $(BsIntermediateDir)/Modules.g.cs $(BsIntermediateDir)/Interop.g.cs $(AssemblyName).dll CopyNativeBinary @@ -60,15 +61,18 @@ - + + - + + diff --git a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs index b5572e26..0aab97ba 100644 --- a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs +++ b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs @@ -82,6 +82,6 @@ void Handle (IImportedInstanced caller, Record? record) private static IImportedStatic GetImportedStatic () { - return (IImportedStatic)Bootsharp.Interfaces.Imports[typeof(IImportedStatic)].Instance; + return (IImportedStatic)Modules.Imports[typeof(IImportedStatic)].Instance; } } From feaf13b147d6381e00149d8f6d55bab0dc43b315 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 3 May 2026 17:12:02 +0300 Subject: [PATCH 03/20] refactor modules and instanced --- .gitignore | 1 + AGENTS.md | 11 +- .../Emit/ModulesTest.cs | 2 - .../Emit/SerializerTest.cs | 20 +- .../Pack/DeclarationTest.cs | 43 ++-- src/cs/Bootsharp.Publish.Test/TaskTest.cs | 19 +- .../Common/Global/GlobalInspection.cs | 20 +- .../{InterfaceMeta.cs => InstancedMeta.cs} | 12 +- .../Common/Meta/InteropKind.cs | 8 + .../Common/Meta/MemberMeta.cs | 30 +-- .../Common/Meta/ValueMeta.cs | 10 +- .../SolutionInspector/InspectionReporter.cs | 29 ++- .../SolutionInspector/InstancedInspector.cs | 78 +++++++ .../SolutionInspector/InterfaceInspector.cs | 76 ------- .../SolutionInspector/MemberInspector.cs | 90 ++++---- .../SolutionInspector/SerializedInspector.cs | 2 +- .../SolutionInspector/SolutionInspection.cs | 23 +- .../SolutionInspector/SolutionInspector.cs | 122 ++++------- .../Bootsharp.Publish/Emit/BootsharpEmit.cs | 35 +-- .../Emit/InstanceGenerator.cs | 29 ++- .../Emit/InterfaceGenerator.cs | 181 --------------- .../Emit/InteropGenerator.cs | 206 +++++++++--------- .../Emit/InteropInitializerGenerator.cs | 36 --- .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 35 +-- .../Emit/SerializerGenerator.cs | 4 +- .../BindingGenerator/BindingClassGenerator.cs | 6 +- .../Pack/BindingGenerator/BindingGenerator.cs | 111 +++++----- .../Bootsharp.Publish/Pack/BootsharpPack.cs | 12 +- .../DeclarationGenerator.cs | 10 +- .../MemberDeclarationGenerator.cs | 13 +- .../TypeDeclarationGenerator.cs | 29 +-- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 4 +- .../Pack/ModulePatcher/ModulePatcher.cs | 1 - src/cs/Directory.Build.props | 2 +- 34 files changed, 540 insertions(+), 770 deletions(-) rename src/cs/Bootsharp.Publish/Common/Meta/{InterfaceMeta.cs => InstancedMeta.cs} (79%) create mode 100644 src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs delete mode 100644 src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs delete mode 100644 src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs diff --git a/.gitignore b/.gitignore index 0b6f602a..9c7bdde1 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ obj *.user *.nupkg package-lock.json +last-failed-test-dump.txt \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index ff191125..7af9ddf2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,16 +39,7 @@ To check JS coverage, run `npm run cover` under `src/js`. # Inspecting Generated Output -C# tests under `Bootsharp.Publish.Test` generate files inside a temporary `MockProject` root, which is deleted when the test is disposed. When you need to inspect the generated content, write it to a scratch file outside the mock project, for example: - -```csharp -AddAssembly(With("// fixture source code")); -Execute(); -File.WriteAllText(Path.Combine(Path.GetTempPath(), "scratch.txt"), GeneratedDeclarations); -Contains("// asserted generated content"); -``` - -Then run the focused test, read the scratch file and remove the probe before finalizing. Do not commit debug dumps or temporary file writes. +C# tests under `Bootsharp.Publish.Test` generate files inside a temporary `MockProject` root, which is deleted when the test is disposed. When you need to inspect the generated content of the last failed test, read the `src/cs/Bootsharp.Publish.Test/last-failed-test-dump.txt` file. # Running Shell Scripts diff --git a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs index aea4c856..e36d406f 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs @@ -152,7 +152,6 @@ public static void Inv () {} """)); Execute(); DoesNotContain("JSStaticExported"); - Assert.Contains(Engine.Warnings, w => w.Contains("must be an interface or non-static class")); } [Fact] @@ -232,7 +231,6 @@ public void Inv () {} """)); Execute(); DoesNotContain("JSImported"); - Assert.Contains(Engine.Warnings, w => w.Contains("must be an interface")); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index b4b07664..5d58cc86 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -170,7 +170,7 @@ public void OrdersNestedCollectionsAfterElements () """ namespace Space; - public class Item + public record Item { public string? Value { get; init; } } @@ -215,7 +215,7 @@ public void UsesParameterizedConstructorForGetterOnlyProperties () { AddAssembly(With( """ - public class Node + public record Node { public Node (string id) => Id = id; public string Id { get; } @@ -235,7 +235,7 @@ public void UsesParameterlessConstructorForWritablePropertiesWhenAvailable () { AddAssembly(With( """ - public class Node + public record Node { public Node () { } public Node (string id) => Id = id; @@ -257,7 +257,7 @@ public void UsesObjectInitializerForPublicInitOnlyProperties () { AddAssembly(With( """ - public class Node + public record Node { public string Id { get; init; } = string.Empty; } @@ -276,12 +276,12 @@ public void DoesNotAssignConstructorBoundPropertiesTwice () { AddAssembly(With( """ - public class RecordA (string Id) + public record RecordA (string Id) { public string Id { get; init; } = Id; } - public class RecordB + public record RecordB { public string Id { get; set; } @@ -326,7 +326,7 @@ public void AssignsRequiredWritableMembersAfterConstruction () { AddAssembly(With( """ - public class CompletionItem + public record CompletionItem { public CompletionItem () { } public required string Label { get; set; } @@ -538,9 +538,9 @@ public void SerializesAllTheCrawledSerializableTypes () With("n", "public readonly record struct ReadonlyRecordStruct(double A);"), With("n", "public record class RecordClass(double A);"), With("n", "public enum Enum { A, B }"), - With("n", "public class Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"), - WithClass("n", "public class Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"), - With("n", "public class Baz { public List Bars { get; } }"), + With("n", "public record Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"), + WithClass("n", "public record Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"), + With("n", "public record Baz { public List Bars { get; } }"), WithClass("n", "[Export] public static Task GetBaz (Enum e) => default;")); Execute(); Contains("Binary "); diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index f9e13ff6..87d35de3 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -348,10 +348,12 @@ export namespace n { export interface Base { } export interface Derived extends n.Base, n.Interface { - foo: n.Interface; + readonly foo: n.Interface; + bar(b: n.Interface): void; } export interface Interface { - foo: n.Interface; + readonly foo: n.Interface; + bar(b: n.Interface): void; } } @@ -375,7 +377,7 @@ export namespace n { export interface Item { } export interface Container { - items: Array; + readonly items: Array; } } @@ -397,7 +399,7 @@ public void DefinitionIsGeneratedForTypeWithJaggedArrayProperty () """ export namespace n { export interface Container { - items: Array>; + readonly items: Array>; } export interface Item { } @@ -423,7 +425,7 @@ export namespace n { export interface Item { } export interface Container { - items: Array; + readonly items: Array; } } @@ -447,7 +449,7 @@ export namespace n { export interface Item { } export interface Container { - items: Map; + readonly items: Map; } } @@ -471,7 +473,7 @@ export namespace n { export interface Item { } export interface Container { - items: Map; + readonly items: Map; } } @@ -495,7 +497,7 @@ export namespace n { export interface Item { } export interface Container { - items: Array; + readonly items: Array; } } @@ -519,7 +521,7 @@ export namespace n { export interface Item { } export interface Container { - items: Array; + readonly items: Array; } } @@ -645,8 +647,8 @@ public class Class { [Export] public static Baz GetBaz () => default; } """ export namespace Space { export interface Baz { - bars: Array; - e: Space.Enum; + readonly bars: Array; + readonly e: Space.Enum; } export interface Bar extends Space.Foo { rrs: Space.ReadonlyRecordStruct; @@ -691,6 +693,7 @@ public void StaticPropertiesAreNotIncluded () """ export namespace Class { export interface Foo { + readonly soo: string; } } """); @@ -772,13 +775,13 @@ public class Class export interface IImported { fun(info: Info, str: string): Info; } + export interface Info { + value: string; + } export interface IExported { inv(str: string, info: Info): Info; reset(): void; } - export interface Info { - value: string; - } export namespace Class { export function getExported(inst: IImported): Promise; @@ -949,13 +952,13 @@ public class Class export interface IImported { changed: EventBroadcaster<[arg1: IImported, arg2: Info, arg3: string]>; } + export interface Info { + value: string; + } export interface IExported { changed: EventSubscriber<[obj: Info]>; done: EventSubscriber<[]>; } - export interface Info { - value: string; - } export namespace Class { export function getExported(inst: IImported): IExported; @@ -1043,10 +1046,10 @@ public void NullablePropertiesHaveOptionalModificator () """ export namespace n { export interface Bar { - foo?: n.Foo; + readonly foo?: n.Foo; } export interface Foo { - bool?: boolean; + readonly bool?: boolean; } } @@ -1068,7 +1071,7 @@ public void NullableEnumsAreCrawled () """ export namespace n { export interface Bar { - foo?: n.Foo; + readonly foo?: n.Foo; } export enum Foo { A, diff --git a/src/cs/Bootsharp.Publish.Test/TaskTest.cs b/src/cs/Bootsharp.Publish.Test/TaskTest.cs index 2c6182de..2441db2d 100644 --- a/src/cs/Bootsharp.Publish.Test/TaskTest.cs +++ b/src/cs/Bootsharp.Publish.Test/TaskTest.cs @@ -51,23 +51,27 @@ protected MockSource With (string code) protected void Contains (string content) { - Assert.Contains(content, TestedContent); + try { Assert.Contains(content, TestedContent); } + catch (Exception ex) { DumpAndThrow(ex); } } protected void DoesNotContain (string content) { - Assert.DoesNotContain(content, TestedContent, StringComparison.OrdinalIgnoreCase); + try { Assert.DoesNotContain(content, TestedContent, StringComparison.OrdinalIgnoreCase); } + catch (Exception ex) { DumpAndThrow(ex); } } protected MatchCollection Matches (string pattern) { - Assert.Matches(pattern, TestedContent); + try { Assert.Matches(pattern, TestedContent); } + catch (Exception ex) { DumpAndThrow(ex); } return Regex.Matches(TestedContent, pattern); } protected void Once (string pattern) { - Assert.Single(Matches(pattern)); + try { Assert.Single(Matches(pattern)); } + catch (Exception ex) { DumpAndThrow(ex); } } protected string ReadProjectFile (string fileName) @@ -75,4 +79,11 @@ protected string ReadProjectFile (string fileName) var filePath = Path.Combine(Project.Root, fileName); return File.Exists(filePath) ? File.ReadAllText(filePath) : null; } + + private void DumpAndThrow (Exception ex) + { + var path = Path.Combine(Project.Root, "..", "..", "..", "..", "last-failed-test-dump.txt"); + File.WriteAllText(path, TestedContent); + throw ex; + } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index f3727bfa..a8108306 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -1,5 +1,4 @@ global using static Bootsharp.Publish.GlobalInspection; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -31,6 +30,18 @@ public static bool IsUserType (Type type) return IsUserAssembly(type.Assembly.FullName!); } + public static bool IsInstancedType (Type type) + { + // Instanced types are mutable user types that are passed by reference when crossing the + // interop boundary (as opposed to serialized immutable types, which are copied by value). + if (!IsUserType(type)) return false; + if (type.IsInterface) return true; + var isRecord = type.GetMethod("$", // records are immutable by convention + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null; + var isStatic = type.IsAbstract && type.IsSealed; + return type.IsClass && !isStatic && !isRecord; + } + public static bool IsAutoProperty (PropertyInfo prop) { var backingFieldName = $"<{prop.Name}>k__BackingField"; @@ -39,13 +50,6 @@ public static bool IsAutoProperty (PropertyInfo prop) return backingField != null; } - public static bool IsInstancedInterface (Type type, [NotNullWhen(true)] out Type? instanceType) - { - if (IsTaskWithResult(type, out instanceType)) - return IsInstancedInterface(instanceType, out instanceType); - return (instanceType = type.IsInterface && IsUserType(type) ? type : null) != null; - } - public static string WithPrefs (IReadOnlyCollection prefs, string input, string @default) { foreach (var pref in prefs) diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs similarity index 79% rename from src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs rename to src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs index e64f9232..19aa3031 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs @@ -5,7 +5,7 @@ namespace Bootsharp.Publish; /// or representing static interop API, or in /// an interop method, representing instanced interop API. /// -internal sealed record InterfaceMeta +internal sealed record InstancedMeta { /// /// Whether the interface represents C# API consumed in @@ -13,13 +13,9 @@ internal sealed record InterfaceMeta /// public required InteropKind Interop { get; init; } /// - /// C# type of the interface. + /// Type info of the instance. /// - public required Type Type { get; init; } - /// - /// C# syntax of the interface type, as specified in source code. - /// - public required string TypeSyntax { get; init; } + public required TypeMeta Type { get; init; } /// /// C# namespace of the generated interop class implementation. /// @@ -31,7 +27,7 @@ internal sealed record InterfaceMeta /// /// Full C# type name of the generated interop class implementation. /// - public required string FullName { get; init; } + public string FullName => $"{Namespace}.{Name}"; /// /// JS name of the generated interop class implementation. /// diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs b/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs index 788347e1..566d7e5e 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InteropKind.cs @@ -14,3 +14,11 @@ internal enum InteropKind /// Import } + +internal static class InteropKindExtensions +{ + extension (InteropKind k) + { + public InteropKind Invert () => k == InteropKind.Export ? InteropKind.Import : InteropKind.Export; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs index 36db1f83..5e4a2d1f 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Bootsharp.Publish; @@ -17,10 +18,6 @@ internal abstract record MemberMeta /// public required InteropKind Interop { get; init; } /// - /// C# assembly name (DLL file name, w/o the extension), under which the member is declared. - /// - public required string Assembly { get; init; } - /// /// Full name of the C# type (including namespace), under which the member is declared. /// public required string Space { get; init; } @@ -37,18 +34,11 @@ internal abstract record MemberMeta /// JavaScript name of the member as will be specified in source code. /// public required string JSName { get; init; } - /// - /// Metadata of the value carried by the member. - /// - public required ValueMeta Value { get; init; } } /// /// An interop method declared on a static, module or instanced API surface. /// -/// -/// Return value of the method is described in . -/// internal record MethodMeta (MethodInfo Info) : MemberMeta { /// @@ -60,6 +50,10 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta /// public required IReadOnlyList Arguments { get; init; } /// + /// Method's return value. + /// + public required ValueMeta Return { get; init; } + /// /// Whether the method returns void. /// public required bool Void { get; init; } @@ -94,13 +88,23 @@ internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta /// public override PropertyInfo Info { get; } = Info; /// + /// Get value of the property or null when getter is not accessible. + /// + public required ValueMeta? GetValue { get; init; } + /// + /// Set value of the property or null when setter is not accessible. + /// + public required ValueMeta? SetValue { get; init; } + /// /// Whether the property has an accessible getter. /// - public required bool CanGet { get; init; } + [MemberNotNullWhen(true, nameof(GetValue))] + public bool CanGet => GetValue != null; /// /// Whether the property has an accessible setter. /// - public required bool CanSet { get; init; } + [MemberNotNullWhen(true, nameof(SetValue))] + public bool CanSet => SetValue != null; } /// diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index d42e7865..d13b91b3 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs @@ -9,7 +9,7 @@ namespace Bootsharp.Publish; internal sealed record ValueMeta { /// - /// The type of the value. + /// Type info of the value. /// public required TypeMeta Type { get; init; } /// @@ -30,9 +30,9 @@ internal sealed record ValueMeta /// public required SerializedMeta? Serialized { get; init; } /// - /// Associated interop interface instance type when , null otherwise. + /// Instance info when , null otherwise. /// - public required Type? InstanceType { get; init; } + public required InstancedMeta? Instanced { get; init; } /// /// Whether the value has to be serialized to cross the interop boundary. /// @@ -41,6 +41,6 @@ internal sealed record ValueMeta /// /// Whether the value is an interop instance. /// - [MemberNotNullWhen(true, nameof(InstanceType))] - public bool IsInstance => InstanceType != null; + [MemberNotNullWhen(true, nameof(Instanced))] + public bool IsInstanced => Instanced != null; } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs index 14bf1658..199f37c4 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs @@ -5,31 +5,36 @@ namespace Bootsharp.Publish; internal sealed class InspectionReporter (TaskLoggingHelper logger) { - public void Report (SolutionInspection inspection) + public void Report (SolutionInspection spec) { logger.LogMessage(MessageImportance.Normal, "Bootsharp assembly inspection result:"); logger.LogMessage(MessageImportance.Normal, Fmt("Discovered assemblies:", - Fmt(GetDiscoveredAssemblies(inspection)))); + Fmt(GetDiscoveredAssemblies(spec)))); logger.LogMessage(MessageImportance.Normal, Fmt("Discovered interop members:", - Fmt(GetDiscoveredMembers(inspection)))); - foreach (var warning in inspection.Warnings) + Fmt(GetDiscoveredMembers(spec)))); + foreach (var warning in spec.Warnings) logger.LogWarning(warning); } - private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) + private HashSet GetDiscoveredAssemblies (SolutionInspection spec) { - return inspection.Static.Select(m => m.Assembly) - .Concat(inspection.Modules.SelectMany(i => i.Members.Select(m => m.Assembly))) - .Concat(inspection.Instanced.SelectMany(i => i.Members.Select(m => m.Assembly))) + return spec.Static.Select(GetAssemblyName) + .Concat(spec.Modules.SelectMany(i => i.Members.Select(GetAssemblyName))) + .Concat(spec.Instanced.SelectMany(i => i.Members.Select(GetAssemblyName))) .ToHashSet(); } - private HashSet GetDiscoveredMembers (SolutionInspection inspection) + private HashSet GetDiscoveredMembers (SolutionInspection spec) { - return inspection.Static - .Concat(inspection.Modules.SelectMany(i => i.Members)) - .Concat(inspection.Instanced.SelectMany(i => i.Members)) + return spec.Static + .Concat(spec.Modules.SelectMany(i => i.Members)) + .Concat(spec.Instanced.SelectMany(i => i.Members)) .Select(m => m.ToString()) .ToHashSet(); } + + private string GetAssemblyName (MemberMeta member) + { + return member.Info.DeclaringType!.Assembly.GetName().Name!; + } } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs new file mode 100644 index 00000000..276ff863 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs @@ -0,0 +1,78 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class InstancedInspector (TypeInspector types) +{ + private readonly Dictionary byType = []; + + public InstancedMeta? Inspect (Type type, InteropKind ik, MemberInspector members) + { + if (byType.TryGetValue(type, out var meta)) return meta; + if (IsTaskWithResult(type, out var result)) return Inspect(result, ik, members); + if (!IsInstancedType(type)) return null; + return CollectMembers(byType[type] = InspectType(type, ik), members); + } + + public IReadOnlyCollection Collect () + { + return byType.Values.ToArray(); + } + + private InstancedMeta InspectType (Type type, InteropKind ik) => new() { + Interop = ik, + Type = types.Inspect(type), + Namespace = BuildInstanceSpace(type, ik), + Name = BuildInstanceName(type), + JSName = BuildInstanceJSName(type), + Members = new List() + }; + + private InstancedMeta CollectMembers (InstancedMeta it, MemberInspector members) + { + var ik = it.Interop; + var type = it.Type.Clr; + var cl = (List)it.Members; + cl.AddRange(type.GetEvents().Select(m => members.Inspect(m, ik, it))); + cl.AddRange(type.GetProperties().Where(ShouldInspectProperty).Select(m => members.Inspect(m, ik, it))); + cl.AddRange(type.GetMethods().Where(ShouldInspectMethod).Select(m => members.Inspect(m, ik, it))); + return it; + } + + private bool ShouldInspectProperty (PropertyInfo prop) + { + if (prop.GetIndexParameters().Length != 0) return false; + if (prop.DeclaringType!.IsInterface) + return prop.GetMethod?.IsAbstract == true || + prop.SetMethod?.IsAbstract == true; + return true; + } + + private bool ShouldInspectMethod (MethodInfo method) + { + if (method.IsSpecialName) return false; + if (method.DeclaringType!.FullName == typeof(object).FullName) return false; + if (method.DeclaringType!.IsInterface) return method.IsAbstract; + return !method.IsStatic; + } + + private string BuildInstanceSpace (Type type, InteropKind ik) + { + var space = "Bootsharp.Generated." + (ik == InteropKind.Export ? "Exports" : "Imports"); + if (type.Namespace != null) space += $".{type.Namespace}"; + return space; + } + + private string BuildInstanceName (Type type) + { + var trimmed = type.IsInterface ? type.Name[1..] : type.Name; + return "JS" + trimmed; + } + + private string BuildInstanceJSName (Type type) + { + var name = BuildInstanceName(type); + if (type.Namespace == null) return name; + return $"{type.Namespace}.{name}".Replace(".", "_"); + } +} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs deleted file mode 100644 index 517a0672..00000000 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs +++ /dev/null @@ -1,76 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -internal sealed class InterfaceInspector (MemberInspector members, string entryAssemblyName) -{ - private InteropKind interop; - private string memberSpace = null!; - - public InterfaceMeta Inspect (Type interfaceType, InteropKind interopKind) - { - var space = BuildInterfaceSpace(interfaceType, interopKind); - var name = BuildInterfaceName(interfaceType); - return new InterfaceMeta { - Interop = interop = interopKind, - Type = interfaceType, - TypeSyntax = BuildSyntax(interfaceType), - Namespace = space, - Name = name, - FullName = memberSpace = $"{space}.{name}", - JSName = BuildInterfaceJSName(interfaceType), - Members = [ - ..interfaceType.GetEvents().Select(CreateEvent), - ..interfaceType.GetProperties().Where(ShouldInspectProperty).Select(CreateProperty), - ..interfaceType.GetMethods().Where(ShouldInspectMethod).Select(CreateMethod) - ] - }; - } - - private bool ShouldInspectProperty (PropertyInfo prop) - { - if (prop.GetIndexParameters().Length != 0) return false; - return prop.GetMethod?.IsAbstract == true || prop.SetMethod?.IsAbstract == true; - } - - private bool ShouldInspectMethod (MethodInfo method) - { - return method.IsAbstract && !method.IsSpecialName; - } - - private EventMeta CreateEvent (EventInfo info) => members.Inspect(info, interop) with { - Assembly = entryAssemblyName, - Space = memberSpace - }; - - private PropertyMeta CreateProperty (PropertyInfo info) => members.Inspect(info, interop) with { - Assembly = entryAssemblyName, - Space = memberSpace, - CanGet = info.GetMethod?.IsAbstract == true, - CanSet = info.SetMethod?.IsAbstract == true - }; - - private MethodMeta CreateMethod (MethodInfo info) => members.Inspect(info, interop) with { - Assembly = entryAssemblyName, - Space = memberSpace - }; - - private static string BuildInterfaceSpace (Type type, InteropKind interop) - { - var space = "Bootsharp.Generated." + (interop == InteropKind.Export ? "Exports" : "Imports"); - if (type.Namespace != null) space += $".{type.Namespace}"; - return space; - } - - private static string BuildInterfaceName (Type type) - { - return "JS" + type.Name[1..]; - } - - private static string BuildInterfaceJSName (Type type) - { - var name = BuildInterfaceName(type); - if (type.Namespace == null) return name; - return $"{type.Namespace}.{name}".Replace(".", "_"); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index 4d886799..01fa504b 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -2,28 +2,27 @@ namespace Bootsharp.Publish; -internal sealed class MemberInspector (Preferences prefs, TypeInspector types, SerializedInspector serde) +internal sealed class MemberInspector (Preferences prefs, TypeInspector types, + SerializedInspector serde, InstancedInspector instanced) { - public EventMeta Inspect (EventInfo evt, InteropKind interop) + public EventMeta Inspect (EventInfo evt, InteropKind ik, InstancedMeta? host) { - var inv = evt.EventHandlerType!.GetMethod("Invoke")!; return new(evt) { - Interop = interop, - Assembly = evt.DeclaringType!.Assembly.GetName().Name!, - Space = evt.DeclaringType.FullName!, + Interop = ik, + Space = BuildSpace(evt.DeclaringType!, host), + JSSpace = BuildJSSpace(evt.DeclaringType!), Name = evt.Name, - Arguments = inv.GetParameters().Select((p, i) => CreateArg(p, GetArgNullability(p, i))).ToArray(), - JSSpace = BuildJSSpace(evt.DeclaringType), JSName = WithPrefs(prefs.Function, evt.Name, ToFirstLower(evt.Name)), - Value = CreateValue(inv.ReturnParameter.ParameterType, GetNullability(inv.ReturnParameter)) + Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() + .Select((p, i) => CreateArg(p, GetArgNullability(p, i), ik)).ToArray() }; - NullabilityInfo GetArgNullability (ParameterInfo param, int index) + NullabilityInfo GetArgNullability (ParameterInfo param, int idx) { if (evt.EventHandlerType!.IsGenericType) { var genType = evt.EventHandlerType.GetGenericTypeDefinition() - .GetMethod("Invoke")!.GetParameters()[index].ParameterType; + .GetMethod("Invoke")!.GetParameters()[idx].ParameterType; if (genType.IsGenericParameter) return GetNullability(evt).GenericTypeArguments[genType.GenericParameterPosition]; } @@ -31,48 +30,57 @@ NullabilityInfo GetArgNullability (ParameterInfo param, int index) } } - public PropertyMeta Inspect (PropertyInfo prop, InteropKind interop) => new(prop) { - Interop = interop, - Assembly = prop.DeclaringType!.Assembly.GetName().Name!, - Space = prop.DeclaringType.FullName!, - JSSpace = BuildJSSpace(prop.DeclaringType), - Name = prop.Name, - JSName = ToFirstLower(prop.Name), - Value = CreateValue(prop.PropertyType, GetNullability(prop)), - CanGet = prop.GetMethod != null, - CanSet = prop.SetMethod != null - }; + public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik, InstancedMeta? host) + { + return new PropertyMeta(prop) { + Interop = ik, + Space = BuildSpace(prop.DeclaringType!, host), + JSSpace = BuildJSSpace(prop.DeclaringType!), + Name = prop.Name, + JSName = ToFirstLower(prop.Name), + GetValue = CreateValue(prop.GetMethod, ik), + SetValue = CreateValue(prop.SetMethod, ik.Invert()), + }; + + ValueMeta? CreateValue (MethodInfo? method, InteropKind ik) + { + if (method is null) return null; + if (prop.DeclaringType!.IsInterface && !method.IsAbstract) return null; + return this.CreateValue(prop.PropertyType, GetNullability(prop), ik); + } + } - public MethodMeta Inspect (MethodInfo method, InteropKind interop) => new(method) { - Interop = interop, - Assembly = method.DeclaringType!.Assembly.GetName().Name!, - Space = method.DeclaringType.FullName!, + public MethodMeta Inspect (MethodInfo method, InteropKind ik, InstancedMeta? host) => new(method) { + Interop = ik, + Space = BuildSpace(method.DeclaringType!, host), + JSSpace = BuildJSSpace(method.DeclaringType!), Name = method.Name, - Arguments = method.GetParameters().Select(p => CreateArg(p, GetNullability(p))).ToArray(), - JSSpace = BuildJSSpace(method.DeclaringType), JSName = WithPrefs(prefs.Function, method.Name, ToFirstLower(method.Name)), - Value = CreateValue(method.ReturnParameter.ParameterType, GetNullability(method.ReturnParameter)), + Arguments = method.GetParameters().Select(p => CreateArg(p, GetNullability(p), ik.Invert())).ToArray(), + Return = CreateValue(method.ReturnParameter.ParameterType, GetNullability(method.ReturnParameter), ik), Void = IsVoid(method.ReturnParameter.ParameterType), Async = IsTaskLike(method.ReturnParameter.ParameterType) }; - private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil) => new(param) { + private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { Name = param.Name!, JSName = param.Name == "function" ? "fn" : param.Name!, - Value = CreateValue(param.ParameterType, nil) + Value = CreateValue(param.ParameterType, nil, ik) }; - private ValueMeta CreateValue (Type type, NullabilityInfo nil) + private ValueMeta CreateValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { + Type = types.Inspect(type), + TypeSyntax = BuildSyntax(type, nil), + Nullable = IsNullable(type, nil), + Nullability = nil, + Serialized = serde.Inspect(type), + Instanced = instanced.Inspect(type, ik, this) + }; + + private string BuildSpace (Type decl, InstancedMeta? host) { - IsInstancedInterface(type, out var instanceType); - return new() { - Type = types.Inspect(type), - TypeSyntax = BuildSyntax(type, nil), - Nullable = IsNullable(type, nil), - Nullability = nil, - Serialized = serde.Inspect(type), - InstanceType = instanceType - }; + if (host != null) return host.FullName; + return decl.FullName!; } private string BuildJSSpace (Type decl) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs index 175637c0..a96b3abc 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -40,7 +40,7 @@ private static bool ShouldSerialize (Type type) if (IsVoid(type)) return false; if (IsNullable(type, out var value)) return ShouldSerialize(value); if (IsTaskWithResult(type, out var result)) return ShouldSerialize(result); - if (IsInstancedInterface(type, out _)) return false; + if (IsInstancedType(type)) return false; return !native.Contains(type.FullName!); } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index 11559ebe..b7f922da 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -14,31 +14,28 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable { /// - /// Static interop members, ie methods or events with + /// Individual interop members, ie methods or events with /// or found on user-defined static classes. /// public required IReadOnlyCollection Static { get; init; } /// - /// Interop modules specified under or - /// for which binding wrappers have to be emitted. + /// Interop API surfaces specified under assembly-level + /// or attributes. /// - public required IReadOnlyCollection Modules { get; init; } + public required IReadOnlyCollection Modules { get; init; } /// - /// Interop interfaces found in interop method arguments or return values. - /// Such interfaces are considered instanced interop APIs, ie stateful objects with - /// interop methods and properties. Both members of - /// and can be sources of the instanced interfaces. - /// - public required IReadOnlyCollection Instanced { get; init; } - /// - /// All the types that cross the interop boundary or referenced by them. + /// All the types that either directly cross the interop boundary or are referenced by such types. /// public required IReadOnlyCollection Types { get; init; } /// - /// All the types that require serialization to cross the interop boundary. + /// All the immutable types that are serialized and copied by value when crossing the interop boundary. /// public required IReadOnlyCollection Serialized { get; init; } /// + /// All the mutable types that are passed by instance reference when crossing the interop boundary. + /// + public required IReadOnlyCollection Instanced { get; init; } + /// /// C# XML documentation for the inspected assemblies. /// public required IReadOnlyCollection Documentation { get; init; } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs index 2f3eb1f9..9c857c55 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs @@ -5,20 +5,19 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspector { - private readonly List staticInterfaces = []; - private readonly List instancedInterfaces = []; - private readonly List staticMembers = []; + private readonly List statics = []; + private readonly List modules = []; private readonly List docs = []; private readonly List warnings = []; - private readonly TypeInspector typeInspector = new(); - private readonly SerializedInspector serdeInspector = new(); - private readonly MemberInspector memberInspector; - private readonly InterfaceInspector interfaceInspector; + private readonly TypeInspector types = new(); + private readonly SerializedInspector serde = new(); + private readonly InstancedInspector instanced; + private readonly MemberInspector members; - public SolutionInspector (Preferences prefs, string entryAssemblyName) + public SolutionInspector (Preferences prefs) { - memberInspector = new(prefs, typeInspector, serdeInspector); - interfaceInspector = new(memberInspector, entryAssemblyName); + instanced = new(types); + members = new(prefs, types, serde, instanced); } /// @@ -52,11 +51,11 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception } private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { - StaticInterfaces = staticInterfaces.DistinctBy(i => i.FullName).ToArray(), - InstancedInterfaces = instancedInterfaces.DistinctBy(i => i.FullName).ToArray(), - StaticMembers = staticMembers.ToArray(), - Types = typeInspector.Collect(), - Serialized = serdeInspector.Collect(), + Static = statics.ToArray(), + Modules = modules.ToArray(), + Types = types.Collect().Where(t => !modules.Any(m => m.Type == t)).ToArray(), + Instanced = instanced.Collect().Except(modules).ToArray(), + Serialized = serde.Collect(), Documentation = docs.ToArray(), Warnings = warnings.ToArray() }; @@ -69,91 +68,44 @@ private void InspectDocumentation (string assemblyPath, string assemblyName) private void InspectAssembly (Assembly assembly) { - foreach (var exported in assembly.GetExportedTypes()) - InspectExportedType(exported); - foreach (var attribute in assembly.CustomAttributes) - InspectAssemblyAttribute(attribute); + foreach (var type in assembly.GetExportedTypes()) + InspectStatic(type); + foreach (var attr in assembly.CustomAttributes) + InspectModules(attr); } - private void InspectExportedType (Type type) + private void InspectStatic (Type type) { if (type.Namespace?.StartsWith("Bootsharp.Generated") ?? false) return; foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.Static)) - InspectStaticEvent(evt); + if (ResolveInterop(evt) is { } interop) + statics.Add(members.Inspect(evt, interop, null)); foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - InspectStaticMethod(method); + if (ResolveInterop(method) is { } interop) + statics.Add(members.Inspect(method, interop, null)); } - private void InspectAssemblyAttribute (CustomAttributeData attr) + private void InspectModules (CustomAttributeData attr) { - var interop = default(InteropKind); - var name = attr.AttributeType.FullName; - if (name == typeof(ExportAttribute).FullName) interop = InteropKind.Export; - else if (name == typeof(ImportAttribute).FullName) interop = InteropKind.Import; - else return; + if (ResolveInterop(attr) is not { } interop) return; foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) - InspectStaticInterface((Type)arg.Value!, interop); + if (instanced.Inspect((Type)arg.Value!, interop, members) is { } it) + if (interop == InteropKind.Export || it.Type.Clr.IsInterface) + modules.Add(it); } - private void InspectStaticMethod (MethodInfo info) + private InteropKind? ResolveInterop (MemberInfo info) { - var interop = default(InteropKind?); - foreach (var attr in info.CustomAttributes.Select(a => a.AttributeType.FullName)) - if (attr == typeof(ExportAttribute).FullName) interop = InteropKind.Export; - else if (attr == typeof(ImportAttribute).FullName) interop = InteropKind.Import; - if (interop is { } ik) - { - var method = memberInspector.Inspect(info, ik); - staticMembers.Add(method); - InspectMember(method); - } + foreach (var attr in info.CustomAttributes) + if (ResolveInterop(attr) is { } interop) + return interop; + return null; } - private void InspectStaticEvent (EventInfo info) + private InteropKind? ResolveInterop (CustomAttributeData attr) { - var interop = default(InteropKind?); - foreach (var attr in info.CustomAttributes.Select(a => a.AttributeType.FullName)) - if (attr == typeof(ExportAttribute).FullName) interop = InteropKind.Export; - else if (attr == typeof(ImportAttribute).FullName) interop = InteropKind.Import; - if (interop is { } ik) - { - var evt = memberInspector.Inspect(info, ik); - staticMembers.Add(evt); - InspectMember(evt); - } - } - - private void InspectStaticInterface (Type type, InteropKind interop) - { - var interfaceMeta = interfaceInspector.Inspect(type, interop); - staticInterfaces.Add(interfaceMeta); - foreach (var member in interfaceMeta.Members) - InspectMember(member); - } - - private void InspectMember (MemberMeta meta) - { - // When interop instance is an argument of exported method, it's imported (JS) API and vice versa. - var interop = meta.Interop == InteropKind.Export ? InteropKind.Import : InteropKind.Export; - if (meta is PropertyMeta prop) - { - if (prop.CanSet) InspectType(prop.Value.Type.Clr, interop); - if (prop.CanGet) InspectType(prop.Value.Type.Clr, prop.Interop); - } - else if (meta is MethodMeta method) - { - foreach (var arg in method.Arguments) - InspectType(arg.Value.Type.Clr, interop); - if (!method.Void) InspectType(method.Value.Type.Clr, method.Interop); - } - else if (meta is EventMeta evt) - foreach (var arg in evt.Arguments) - InspectType(arg.Value.Type.Clr, evt.Interop); - } - - private void InspectType (Type type, InteropKind interop) - { - if (IsInstancedInterface(type, out var instanceType)) - instancedInterfaces.Add(interfaceInspector.Inspect(instanceType, interop)); + if (attr.AttributeType.FullName == typeof(ExportAttribute).FullName) return InteropKind.Export; + if (attr.AttributeType.FullName == typeof(ImportAttribute).FullName) return InteropKind.Import; + return null; } } diff --git a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs index 40ec5793..6ecfaf5d 100644 --- a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs +++ b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs @@ -7,16 +7,18 @@ public sealed class BootsharpEmit : Microsoft.Build.Utilities.Task { public required string InspectedDirectory { get; set; } public required string EntryAssemblyName { get; set; } - public required string InterfacesFilePath { get; set; } public required string SerializerFilePath { get; set; } + public required string InstancesFilePath { get; set; } + public required string ModulesFilePath { get; set; } public required string InteropFilePath { get; set; } public override bool Execute () { var prefs = ResolvePreferences(); using var inspection = InspectSolution(prefs); - GenerateInterfaces(inspection); GenerateSerializer(inspection); + GenerateInstances(inspection); + GenerateModules(inspection); GenerateInterop(inspection); return true; } @@ -29,31 +31,38 @@ private Preferences ResolvePreferences () private SolutionInspection InspectSolution (Preferences prefs) { - var inspector = new SolutionInspector(prefs, EntryAssemblyName); + var inspector = new SolutionInspector(prefs); var inspected = Directory.GetFiles(InspectedDirectory, "*.dll").Order(); var inspection = inspector.Inspect(InspectedDirectory, inspected); new InspectionReporter(Log).Report(inspection); return inspection; } - private void GenerateInterfaces (SolutionInspection inspection) + private void GenerateSerializer (SolutionInspection spec) { - var generator = new InterfaceGenerator(); - var content = generator.Generate(inspection); - WriteGenerated(InterfacesFilePath, content); + var generator = new SerializerGenerator(); + var content = generator.Generate(spec); + WriteGenerated(SerializerFilePath, content); } - private void GenerateSerializer (SolutionInspection inspection) + private void GenerateInstances (SolutionInspection spec) { - var generator = new SerializerGenerator(); - var content = generator.Generate(inspection); - WriteGenerated(SerializerFilePath, content); + var generator = new InstanceGenerator(); + var content = generator.Generate(spec); + WriteGenerated(InstancesFilePath, content); + } + + private void GenerateModules (SolutionInspection spec) + { + var generator = new ModuleGenerator(); + var content = generator.Generate(spec); + WriteGenerated(ModulesFilePath, content); } - private void GenerateInterop (SolutionInspection inspection) + private void GenerateInterop (SolutionInspection spec) { var generator = new InteropGenerator(); - var content = generator.Generate(inspection); + var content = generator.Generate(spec); WriteGenerated(InteropFilePath, content); } diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs index dc4b0f58..75b4e76c 100644 --- a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -5,25 +5,23 @@ namespace Bootsharp.Publish; /// internal sealed class InstanceGenerator { - private InterfaceMeta it = null!; + private InstancedMeta it = null!; - public string Generate (SolutionInspection inspection) - { - var its = inspection.Instanced.Where(i => i.Interop == InteropKind.Import); - return - $""" - #nullable enable - #pragma warning disable + public string Generate (SolutionInspection spec) => + $""" + #nullable enable + #pragma warning disable - {Fmt(its.Select(EmitWrapper), 0, "\n\n")} - """; - } + {Fmt(spec.Instanced + .Where(i => i.Interop == InteropKind.Import) + .Select(EmitWrapper), 0, "\n\n")} + """; - private string EmitWrapper (InterfaceMeta it) => + private string EmitWrapper (InstancedMeta it) => $$""" namespace {{(this.it = it).Namespace}} { - public class {{it.Name}} (global::System.Int32 id) : {{it.TypeSyntax}} + public class {{it.Name}} (global::System.Int32 id) : {{it.Type.Syntax}} { internal readonly global::System.Int32 _id = id; @@ -57,12 +55,13 @@ private string EmitEventImport (EventMeta evt) private string EmitPropertyImport (PropertyMeta prop) { + var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; var space = $"global::Bootsharp.Generated.Interop.{prop.Space.Replace('.', '_')}"; var getArgs = PrependIdArg(""); var setArgs = PrependIdArg("value"); return $$""" - {{prop.Value.TypeSyntax}} {{it.TypeSyntax}}.{{prop.Name}} + {{type}} {{it.Type.Syntax}}.{{prop.Name}} { {{Fmt( prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, @@ -77,7 +76,7 @@ private string EmitMethodImport (MethodMeta method) var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = PrependIdArg(string.Join(", ", method.Arguments.Select(a => a.Name))); var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Value.TypeSyntax} {it.TypeSyntax}.{method.Name} ({args}) => " + + return $"{method.Return.TypeSyntax} {it.Type.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs deleted file mode 100644 index b5d15192..00000000 --- a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs +++ /dev/null @@ -1,181 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Generates implementations for interop interfaces. -/// -internal sealed class InterfaceGenerator -{ - private IReadOnlyCollection instanced = []; - - public string Generate (SolutionInspection inspection) - { - instanced = inspection.InstancedInterfaces; - var classes = new HashSet(); - foreach (var i in inspection.StaticInterfaces) - if (i.Interop == InteropKind.Export) classes.Add(EmitExportClass(i)); - else classes.Add(EmitImportClass(i)); - foreach (var i in inspection.InstancedInterfaces) - if (i.Interop == InteropKind.Import) - classes.Add(EmitInstancedImportClass(i)); - return - $$""" - #nullable enable - #pragma warning disable - - namespace Bootsharp.Generated - { - internal static class InterfaceRegistrations - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () - { - {{Fmt(inspection.StaticInterfaces.Select(EmitRegistration), 3)}} - } - } - } - - {{Fmt(classes, 0, "\n\n")}} - """; - } - - private string EmitRegistration (InterfaceMeta i) - { - var it = i.Interop == InteropKind.Import - ? $"new ImportInterface(new {i.FullName}())" - : $"new ExportInterface(typeof({i.TypeSyntax}), handler => new {i.FullName}(({i.TypeSyntax})handler))"; - var key = i.Interop == InteropKind.Import - ? $"typeof({i.TypeSyntax})" - : $"typeof({i.FullName})"; - return $"Interfaces.Register({key}, {it});"; - } - - private string EmitExportClass (InterfaceMeta i) => - $$""" - namespace {{i.Namespace}} - { - public class {{i.Name}} - { - private static {{i.TypeSyntax}} handler = null!; - - public {{i.Name}} ({{i.TypeSyntax}} handler) - { - {{Fmt([ - $"{i.Name}.handler = handler;", - ..i.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") - ], 3)}} - } - - {{Fmt(i.Members.Select(EmitExport), 2)}} - } - } - """; - - private string EmitImportClass (InterfaceMeta i) => - $$""" - namespace {{i.Namespace}} - { - public class {{i.Name}} : {{i.TypeSyntax}} - { - {{Fmt(i.Members.Select(m => EmitImport(i, m)), 2)}} - } - } - """; - - private string EmitInstancedImportClass (InterfaceMeta i) => - $$""" - namespace {{i.Namespace}} - { - public class {{i.Name}} (global::System.Int32 id) : {{i.TypeSyntax}} - { - internal readonly global::System.Int32 _id = id; - - ~{{i.Name}}() - { - global::Bootsharp.Instances.DisposeImported(_id); - global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - } - - {{Fmt(i.Members.Select(m => EmitImport(i, m)), 2)}} - } - } - """; - - private string EmitExport (MemberMeta member) => member switch { - EventMeta evt => EmitEventExport(evt), - PropertyMeta prop => EmitPropertyExport(prop), - _ => EmitMethodExport((MethodMeta)member) - }; - - private string EmitImport (InterfaceMeta i, MemberMeta member) => member switch { - EventMeta evt => EmitEventImport(evt), - PropertyMeta prop => EmitPropertyImport(i, prop), - _ => EmitMethodImport(i, (MethodMeta)member), - }; - - private string EmitEventExport (EventMeta evt) - { - var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullability(evt.Info)); - return $"[Export] public static event {type} {evt.Name};"; - } - - private string EmitEventImport (EventMeta evt) - { - var type = BuildSyntax(evt.Info.EventHandlerType!, GetNullability(evt.Info)); - var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var args = string.Join(", ", evt.Arguments.Select(a => a.Name)); - return Fmt(0, - $"public event {type} {evt.Name};", - $"internal void Invoke{evt.Name} ({sigArgs}) => {evt.Name}?.Invoke({args});" - ); - } - - private string EmitPropertyExport (PropertyMeta prop) - { - var name = prop.Name; - var type = prop.Value.TypeSyntax; - var get = $"[Export] public static {type} GetProperty{name} () => handler.{name};"; - var set = $"[Export] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; - return Fmt(0, prop.CanGet ? get : null, prop.CanSet ? set : null); - } - - private string EmitPropertyImport (InterfaceMeta i, PropertyMeta prop) - { - var inst = IsInstanced(prop); - var space = $"global::Bootsharp.Generated.Interop.{prop.Space.Replace('.', '_')}"; - var getArgs = inst ? "_id" : ""; - var setArgs = inst ? "_id, value" : "value"; - return - $$""" - {{prop.Value.TypeSyntax}} {{i.TypeSyntax}}.{{prop.Name}} - { - {{Fmt( - prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, - prop.CanSet ? $"set => {space}_SetProperty{prop.Name}({setArgs});" : null - )}} - } - """; - } - - private string EmitMethodExport (MethodMeta method) - { - var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var sig = $"public static {method.Value.TypeSyntax} {method.Name} ({sigArgs})"; - var args = string.Join(", ", method.Arguments.Select(a => a.Name)); - return $"[Export] {sig} => handler.{method.Name}({args});"; - } - - private string EmitMethodImport (InterfaceMeta i, MethodMeta method) - { - var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var args = string.Join(", ", method.Arguments.Select(a => a.Name)); - if (IsInstanced(method)) args = PrependIdArg(args); - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Value.TypeSyntax} {i.TypeSyntax}.{method.Name} ({sigArgs}) => " + - $"global::Bootsharp.Generated.Interop.{name}({args});"; - } - - private bool IsInstanced (MemberMeta member) - { - return instanced.Any(i => i.Members.Contains(member)); - } -} diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 9014aad1..be850a10 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -7,56 +7,81 @@ namespace Bootsharp.Publish; /// internal sealed class InteropGenerator { - private readonly HashSet registered = []; - private IReadOnlyCollection instanced = []; + private readonly HashSet registered = []; + private InstancedMeta? it, md; + [MemberNotNullWhen(true, nameof(it))] private bool isIt => it != null; + [MemberNotNullWhen(true, nameof(md))] private bool isMd => md != null; - public string Generate (SolutionInspection inspection) - { - instanced = inspection.InstancedInterfaces; - return - $$""" - #nullable enable - #pragma warning disable + public string Generate (SolutionInspection spec) => + $$""" + #nullable enable + #pragma warning disable - using System.Runtime.CompilerServices; - using System.Runtime.InteropServices.JavaScript; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices.JavaScript; - namespace Bootsharp.Generated; + namespace Bootsharp.Generated; - public static partial class Interop + public static partial class Interop + { + [JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id); + [JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); + + [ModuleInitializer] + internal static unsafe void Initialize () { - [JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id); - [JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); + {{Fmt([ + ..spec.Static.OfType() + .Concat(spec.Modules.SelectMany(i => i.Members.OfType())) + .Where(e => e.Interop == InteropKind.Export) + .Select(EmitEventSubscription), + ..spec.Static.OfType() + .Where(m => m.Interop == InteropKind.Import) + .Select(EmitMethodAssignment) + ], 2)}} + } + {{Fmt(spec.Static.SelectMany(m => EmitMember(m, null, null)))}} + {{Fmt(spec.Modules.SelectMany(md => md.Members.SelectMany(m => EmitMember(m, null, md))))}} + {{Fmt(spec.Instanced.SelectMany(it => it.Members.SelectMany(m => EmitMember(m, it, null))))}} + } + """; - {{new InteropInitializerGenerator().Generate(inspection)}} + private static string EmitEventSubscription (EventMeta evt) + { + var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; + return $"global::{evt.Space}.{evt.Name} += {handler};"; + } - {{Fmt(inspection.StaticMembers.SelectMany(EmitMember))}} - {{Fmt(inspection.StaticInterfaces.SelectMany(i => i.Members.SelectMany(EmitMember)))}} - {{Fmt(inspection.InstancedInterfaces.SelectMany(i => i.Members.SelectMany(EmitMember)))}} - } - """; + private static string EmitMethodAssignment (MethodMeta method) + { + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; } - private IEnumerable EmitMember (MemberMeta member) => member switch { - EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), - EventMeta { Interop: InteropKind.Import } e => EmitEventImport(e), - PropertyMeta { Interop: InteropKind.Export } p => EmitPropertyExport(p), - PropertyMeta { Interop: InteropKind.Import } p => EmitPropertyImport(p), - MethodMeta { Interop: InteropKind.Export } m => EmitMethodExport(m), - _ => EmitMethodImport((MethodMeta)member) - }; + private IEnumerable EmitMember (MemberMeta member, InstancedMeta? it, InstancedMeta? md) + { + this.it = it; + this.md = md; + return member switch { + EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), + EventMeta { Interop: InteropKind.Import } e => EmitEventImport(e), + PropertyMeta { Interop: InteropKind.Export } p => EmitPropertyExport(p), + PropertyMeta { Interop: InteropKind.Import } p => EmitPropertyImport(p), + MethodMeta { Interop: InteropKind.Export } m => EmitMethodExport(m), + _ => EmitMethodImport((MethodMeta)member) + }; + } private IEnumerable EmitEventExport (EventMeta evt) { - var inst = TryInstanced(evt, out var instance); var attr = $"""[JSImport("{evt.JSSpace}.broadcast{evt.Name}Serialized", "Bootsharp")] """; var name = $"{evt.JSSpace.Replace('.', '_')}_Broadcast{evt.Name}_Serialized"; var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; yield return $"{attr}internal static partial void {name} ({args});"; - if (inst) yield return EmitInstanceRegistrar(instance!); - if (inst) yield break; // instanced export event handlers are emitted in the registrar + if (isIt) yield return EmitInstanceRegistrar(it); + if (isIt) yield break; // instanced export event handlers are emitted in the registrar var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); @@ -65,14 +90,11 @@ public static partial class Interop private IEnumerable EmitEventImport (EventMeta evt) { - var inst = TryInstanced(evt, out var instance); var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var invName = evt.Info.DeclaringType is { IsInterface: true } it - ? inst - ? $"Instances.Import(_id, static id => new global::{instance!.FullName}(id)).Invoke{evt.Name}" - : $"((global::{evt.Space})Interfaces.Imports[typeof({BuildSyntax(it)})].Instance).Invoke{evt.Name}" + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var invName = isIt ? $"Instances.Import(_id, static id => new global::{it.FullName}(id)).Invoke{evt.Name}" + : isMd ? $"((global::{evt.Space})Modules.Imports[typeof({md.Type.Syntax})].Instance).Invoke{evt.Name}" : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; var invArgs = string.Join(", ", evt.Arguments.Select(Deserialize)); yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; @@ -80,25 +102,24 @@ private IEnumerable EmitEventImport (EventMeta evt) private IEnumerable EmitPropertyExport (PropertyMeta prop) { - var inst = TryInstanced(prop, out var instance); if (prop.CanGet) { - var attr = $"[JSExport] {MarshalAmbiguous(prop.Value, true)}"; + var attr = $"[JSExport] {MarshalAmbiguous(prop.GetValue, true)}"; var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var args = inst ? $"{BuildSyntax(typeof(int))} _id" : ""; - var body = Serialize(prop.Value, inst - ? $"Instances.Exported<{instance!.TypeSyntax}>(_id).{prop.Name}" + var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; + var body = Serialize(prop.GetValue, isIt + ? $"Instances.Exported<{it.Type.Syntax}>(_id).{prop.Name}" : $"global::{prop.Space}.GetProperty{prop.Name}()"); - yield return $"{attr}internal static {BuildValueSyntax(prop.Value)} {name} ({args}) => {body};"; + yield return $"{attr}internal static {BuildValueSyntax(prop.GetValue)} {name} ({args}) => {body};"; } if (prop.CanSet) { var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; - var args = BuildParameter(prop.Value, "value"); - if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Deserialize(prop.Value, "value"); - var body = inst - ? $"Instances.Exported<{instance!.TypeSyntax}>(_id).{prop.Name} = {value}" + var args = BuildParameter(prop.SetValue, "value"); + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var value = Deserialize(prop.SetValue, "value"); + var body = isIt + ? $"Instances.Exported<{it.Type.Syntax}>(_id).{prop.Name} = {value}" : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; yield return $"[JSExport] internal static void {name} ({args}) => {body};"; } @@ -106,83 +127,80 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) private IEnumerable EmitPropertyImport (PropertyMeta prop) { - var inst = TryInstanced(prop, out _); if (prop.CanGet) { var endpoint = $"""("{prop.JSSpace}.getProperty{prop.Name}Serialized", "Bootsharp")"""; - var attr = $"[JSImport{endpoint}] {MarshalAmbiguous(prop.Value, true)}"; + var attr = $"[JSImport{endpoint}] {MarshalAmbiguous(prop.GetValue, true)}"; var serdeName = $"{prop.JSSpace.Replace('.', '_')}_GetProperty{prop.Name}_Serialized"; - var args = inst ? $"{BuildSyntax(typeof(int))} _id" : ""; - yield return $"{attr}internal static partial {BuildValueSyntax(prop.Value)} {serdeName} ({args});"; + var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; + yield return $"{attr}internal static partial {BuildValueSyntax(prop.GetValue)} {serdeName} ({args});"; var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var body = Deserialize(prop.Value, inst ? $"{serdeName}(_id)" : $"{serdeName}()"); - yield return $"public static {prop.Value.TypeSyntax} {name}({args}) => {body};"; + var body = Deserialize(prop.GetValue, isIt ? $"{serdeName}(_id)" : $"{serdeName}()"); + yield return $"public static {prop.GetValue.TypeSyntax} {name}({args}) => {body};"; } if (prop.CanSet) { var attr = $"""[JSImport("{prop.JSSpace}.setProperty{prop.Name}Serialized", "Bootsharp")] """; var serdeName = $"{prop.JSSpace.Replace('.', '_')}_SetProperty{prop.Name}_Serialized"; - var serdeArgs = BuildParameter(prop.Value, "value"); - if (inst) serdeArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(serdeArgs)}"; + var serdeArgs = BuildParameter(prop.SetValue, "value"); + if (isIt) serdeArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(serdeArgs)}"; yield return $"{attr}internal static partial void {serdeName} ({serdeArgs});"; var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; - var args = $"{prop.Value.TypeSyntax} value"; - if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Serialize(prop.Value, "value"); - var body = inst ? $"{serdeName}(_id, {value})" : $"{serdeName}({value})"; + var args = $"{prop.SetValue.TypeSyntax} value"; + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var value = Serialize(prop.SetValue, "value"); + var body = isIt ? $"{serdeName}(_id, {value})" : $"{serdeName}({value})"; yield return $"public static void {name}({args}) => {body};"; } } private IEnumerable EmitMethodExport (MethodMeta method) { - var inst = TryInstanced(method, out var instance); var wait = ShouldWait(method); - var attr = $"[JSExport] {MarshalAmbiguous(method.Value, true)}"; + var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var @return = BuildValueSyntax(method.Value); + var @return = BuildValueSyntax(method.Return); if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (inst) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; + if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); - var invName = inst - ? $"Instances.Exported<{instance!.TypeSyntax}>(_id).{method.Name}" + var invName = isIt + ? $"Instances.Exported<{it.Type.Syntax}>(_id).{method.Name}" : $"global::{method.Space}.{method.Name}"; - var body = Serialize(method.Value, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; } private IEnumerable EmitMethodImport (MethodMeta method) { - var inst = TryInstanced(method, out _); - var marshalAs = MarshalAmbiguous(method.Value, true); + var marshalAs = MarshalAmbiguous(method.Return, true); var attr = $"""[JSImport("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")] {marshalAs}"""; var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var @return = BuildValueSyntax(method.Value); + var @return = BuildValueSyntax(method.Return); if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; var args = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; yield return $"{attr}internal static partial {@return} {name}_Serialized ({args});"; var wait = ShouldWait(method); - @return = $"{(wait ? "async " : "")}{method.Value.TypeSyntax}"; + @return = $"{(wait ? "async " : "")}{method.Return.TypeSyntax}"; var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - if (inst) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; + if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; var invArgs = string.Join(", ", method.Arguments.Select(Serialize)); - if (inst) invArgs = PrependIdArg(invArgs); - var body = Deserialize(method.Value, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); + if (isIt) invArgs = PrependIdArg(invArgs); + var body = Deserialize(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); yield return $"public static {@return} {name} ({sigArgs}) => {body};"; } - private string? EmitInstanceRegistrar (InterfaceMeta instance) + private string? EmitInstanceRegistrar (InstancedMeta it) { - if (!registered.Add(instance)) return null; - var events = instance.Members.OfType().ToArray(); + if (!registered.Add(it)) return null; + var events = it.Members.OfType().ToArray(); return $$""" - private static int Register ({{instance.TypeSyntax}} instance) => Instances.Export(instance, static (_id, instance) => { + private static int Register ({{it.Type.Syntax}} instance) => Instances.Export(instance, static (_id, instance) => { {{Fmt(events.Select(e => $"instance.{e.Name} += Handle{e.Name};"))}} return () => { {{Fmt(events.Select(e => $"instance.{e.Name} -= Handle{e.Name};"), 2)}} @@ -207,7 +225,7 @@ private string BuildParameter (ValueMeta value, string name) private string Serialize (ArgumentMeta arg) => Serialize(arg.Value, arg.Name); private string Serialize (ValueMeta value, string exp) { - if (value.IsInstance) return RegisterInstance(value, exp); + if (value.IsInstanced) return RegisterInstance(value.Instanced, exp); if (Serialized(value, out var id)) return $"Serializer.Serialize({exp}, {id})"; return exp; } @@ -215,11 +233,10 @@ private string Serialize (ValueMeta value, string exp) private string Deserialize (ArgumentMeta arg) => Deserialize(arg.Value, arg.Name); private string Deserialize (ValueMeta value, string exp) { - if (value.InstanceType is { } it) + if (value.Instanced is { } it) { - var instance = instanced.First(i => i.Type == it); - if (instance.Interop == InteropKind.Export) return $"Instances.Exported<{instance.TypeSyntax}>({exp})"; - return $"Instances.Import({exp}, static id => new global::{instance.FullName}(id))"; + if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Type.Syntax}>({exp})"; + return $"Instances.Import({exp}, static id => new global::{it.FullName}(id))"; } if (Serialized(value, out var id)) return $"Serializer.Deserialize({exp}, {id})"; return exp; @@ -228,7 +245,7 @@ private string Deserialize (ValueMeta value, string exp) private string BuildValueSyntax (ValueMeta value) { var nil = value.Nullable && !value.IsSerialized ? "?" : ""; - if (value.IsInstance) return $"global::System.Int32{nil}"; + if (value.IsInstanced) return $"global::System.Int32{nil}"; if (value.IsSerialized) return $"global::System.Int64{nil}"; return value.TypeSyntax; } @@ -256,23 +273,16 @@ private static bool Serialized (ValueMeta meta, [NotNullWhen(true)] out string? return id != null; } - private string RegisterInstance (ValueMeta value, string exp) + private string RegisterInstance (InstancedMeta it, string exp) { - var instance = instanced.First(i => i.Type == value.InstanceType); - if (instance.Interop == InteropKind.Import) return $"((global::{instance.FullName}){exp})._id"; - if (instance.Members.OfType().Any()) return $"Register({exp})"; + if (it.Interop == InteropKind.Import) return $"((global::{it.FullName}){exp})._id"; + if (it.Members.OfType().Any()) return $"Register({exp})"; return $"Instances.Export({exp})"; } - private bool TryInstanced (MemberMeta member, [NotNullWhen(true)] out InterfaceMeta? instance) - { - instance = instanced.FirstOrDefault(i => i.Members.Contains(member)); - return instance is not null; - } - private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; - return method.Value.IsSerialized || method.Value.IsInstance; + return method.Return.IsSerialized || method.Return.IsInstanced; } } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs deleted file mode 100644 index 44c6be98..00000000 --- a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class InteropInitializerGenerator -{ - public string Generate (SolutionInspection inspection) - { - var events = inspection.StaticMembers.OfType() - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members.OfType())) - .Where(e => e.Interop == InteropKind.Export).ToArray(); - var methods = inspection.StaticMembers.OfType() - .Where(m => m.Interop == InteropKind.Import).ToArray(); - if (methods.Length == 0 && events.Length == 0) return ""; - return $$""" - [ModuleInitializer] - internal static unsafe void Initialize () - { - {{Fmt(2, [ - ..events.Select(BuildEventSubscription), - ..methods.Select(BuildMethodAssignment) - ])}} - } - """; - } - - private static string BuildEventSubscription (EventMeta evt) - { - var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; - return $"global::{evt.Space}.{evt.Name} += {handler};"; - } - - private static string BuildMethodAssignment (MethodMeta method) - { - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; - } -} diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs index 9dd232cc..f99a4c9e 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -5,9 +5,9 @@ namespace Bootsharp.Publish; /// internal sealed class ModuleGenerator { - private InterfaceMeta it = null!; + private InstancedMeta it = null!; - public string Generate (SolutionInspection inspection) => + public string Generate (SolutionInspection spec) => $$""" #nullable enable #pragma warning disable @@ -19,26 +19,26 @@ internal static class ModuleRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterModules () { - {{Fmt(inspection.Modules.Select(EmitRegistration), 3)}} + {{Fmt(spec.Modules.Select(EmitRegistration), 3)}} } } } - {{Fmt(inspection.Modules.Select(EmitModule), 0, "\n\n")}} + {{Fmt(spec.Modules.Select(EmitModule), 0, "\n\n")}} """; - private string EmitRegistration (InterfaceMeta it) + private string EmitRegistration (InstancedMeta it) { var type = it.Interop == InteropKind.Import - ? $"typeof({it.TypeSyntax})" + ? $"typeof({it.Type.Syntax})" : $"typeof({it.FullName})"; var factory = it.Interop == InteropKind.Import ? $"new ImportModule(new {it.FullName}())" - : $"new ExportModule(typeof({it.TypeSyntax}), handler => new {it.FullName}(({it.TypeSyntax})handler))"; + : $"new ExportModule(typeof({it.Type.Syntax}), handler => new {it.FullName}(({it.Type.Syntax})handler))"; return $"Modules.Register({type}, {factory});"; } - private string EmitModule (InterfaceMeta it) + private string EmitModule (InstancedMeta it) { this.it = it; if (it.Interop == InteropKind.Export) return EmitModuleExport(); @@ -51,9 +51,9 @@ namespace {{it.Namespace}} { public class {{it.Name}} { - private static {{it.TypeSyntax}} handler = null!; + private static {{it.Type.Syntax}} handler = null!; - public {{it.Name}} ({{it.TypeSyntax}} handler) + public {{it.Name}} ({{it.Type.Syntax}} handler) { {{Fmt([ $"{it.Name}.handler = handler;", @@ -70,7 +70,7 @@ private string EmitModuleImport () => $$""" namespace {{it.Namespace}} { - public class {{it.Name}} : {{it.TypeSyntax}} + public class {{it.Name}} : {{it.Type.Syntax}} { {{Fmt(it.Members.Select(EmitMemberImport), 2)}} } @@ -109,7 +109,7 @@ private string EmitEventImport (EventMeta evt) private string EmitPropertyExport (PropertyMeta prop) { var name = prop.Name; - var type = prop.Value.TypeSyntax; + var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; var get = $"[Export] public static {type} GetProperty{name} () => handler.{name};"; var set = $"[Export] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; return Fmt(0, prop.CanGet ? get : null, prop.CanSet ? set : null); @@ -117,10 +117,11 @@ private string EmitPropertyExport (PropertyMeta prop) private string EmitPropertyImport (PropertyMeta prop) { - var space = $"global::Bootsharp.Generated.Interop.{prop.Space.Replace('.', '_')}"; + var space = $"global::Bootsharp.Generated.Interop.{it.FullName.Replace('.', '_')}"; + var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; return $$""" - {{prop.Value.TypeSyntax}} {{it.TypeSyntax}}.{{prop.Name}} + {{type}} {{it.Type.Syntax}}.{{prop.Name}} { {{Fmt( prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, @@ -133,7 +134,7 @@ private string EmitPropertyImport (PropertyMeta prop) private string EmitMethodExport (MethodMeta method) { var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var sig = $"public static {method.Value.TypeSyntax} {method.Name} ({args})"; + var sig = $"public static {method.Return.TypeSyntax} {method.Name} ({args})"; var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); return $"[Export] {sig} => handler.{method.Name}({callArgs});"; } @@ -142,8 +143,8 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Value.TypeSyntax} {it.TypeSyntax}.{method.Name} ({args}) => " + + var name = $"{it.FullName.Replace('.', '_')}_{method.Name}"; + return $"{method.Return.TypeSyntax} {it.Type.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index b27d9c53..9d9b3b90 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -2,9 +2,9 @@ namespace Bootsharp.Publish; internal sealed class SerializerGenerator { - public string Generate (SolutionInspection inspection) + public string Generate (SolutionInspection spec) { - var serialized = inspection.Serialized; + var serialized = spec.Serialized; if (serialized.Count == 0) return ""; return $$""" using System.Runtime.CompilerServices; diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs index 6741c3ed..28ca63b1 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs @@ -2,13 +2,13 @@ namespace Bootsharp.Publish; internal sealed class BindingClassGenerator { - public string Generate (IReadOnlyCollection instanced) + public string Generate (SolutionInspection spec) { - var exported = instanced.Where(i => i.Interop == InteropKind.Export); + var exported = spec.Instanced.Where(i => i.Interop == InteropKind.Export); return Fmt(exported.Select(EmitClass), 0) + '\n'; } - private string EmitClass (InterfaceMeta instance) => + private string EmitClass (InstancedMeta instance) => $$""" class {{instance.JSName}} { {{Fmt([ diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 0d2725a6..e23c5d54 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -5,28 +5,28 @@ namespace Bootsharp.Publish; internal sealed class BindingGenerator (Preferences prefs, bool debug) { - private record Binding (MemberMeta? Member, Type? Enum, string Namespace); + private record Binding (MemberMeta? Member, Type? Enum, InstancedMeta? It, string Namespace); private Binding binding => bindings[index]; private Binding? prevBinding => index == 0 ? null : bindings[index - 1]; private Binding? nextBinding => index == bindings.Length - 1 ? null : bindings[index + 1]; private readonly StringBuilder builder = new(); - private IReadOnlyCollection instanced = []; + [MemberNotNullWhen(true, nameof(it))] private bool isIt => it != null; + private InstancedMeta? it => binding.It; private Binding[] bindings = []; private int index, level; - public string Generate (SolutionInspection inspection) + public string Generate (SolutionInspection spec) { - instanced = inspection.InstancedInterfaces; - bindings = inspection.StaticMembers - .Select(m => new Binding(m, null, m.JSSpace)) - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members - .Select(m => new Binding(m, null, m.JSSpace)))) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members - .Select(m => new Binding(m, null, m.JSSpace)))) - .Concat(inspection.Serialized.Where(t => t.Type.IsEnum) - .Select(t => new Binding(null, t.Type, BuildJSSpace(t.Type, prefs)))) + bindings = spec.Static + .Select(m => new Binding(m, null, null, m.JSSpace)) + .Concat(spec.Modules.SelectMany(it => it.Members + .Select(m => new Binding(m, null, null, m.JSSpace)))) + .Concat(spec.Instanced.SelectMany(it => it.Members + .Select(m => new Binding(m, null, it, m.JSSpace)))) + .Concat(spec.Serialized.Where(t => t.Type.IsEnum) + .Select(t => new Binding(null, t.Type, null, BuildJSSpace(t.Type, prefs)))) .OrderBy(m => m.Namespace).ToArray(); if (bindings.Length == 0) return ""; @@ -42,16 +42,16 @@ public string Generate (SolutionInspection inspection) EmitHelpers(); builder.Append("\n\n"); - builder.Append(new BindingSerializerGenerator().Generate(inspection.Serialized)); + builder.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); builder.Append("\n\n"); - foreach (var instance in inspection.InstancedInterfaces + foreach (var instance in spec.Instanced .Where(i => i.Interop == InteropKind.Import && i.Members.OfType().Any())) EmitInstanceRegistrar(instance); builder.Append("\n\n"); - if (inspection.InstancedInterfaces.Count > 0) - builder.Append(new BindingClassGenerator().Generate(inspection.InstancedInterfaces)); + if (spec.Instanced.Count > 0) + builder.Append(new BindingClassGenerator().Generate(spec)); for (index = 0; index < bindings.Length; index++) EmitBinding(); @@ -175,16 +175,15 @@ private void EmitMember (MemberMeta member) private void EmitEventExport (EventMeta evt) { - var inst = TryInstanced(evt, out var instance); var name = $"broadcast{evt.Name}Serialized"; var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); var invArgs = string.Join(", ", evt.Arguments.Select(arg => // By default, we use 'null' for missing collection items, but here the event args array // represents args specified to the event's 'broadcast' function, so user expects 'undefined'. $"{Deserialize(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); - if (inst) + if (isIt) { - var invName = $"instances.export(_id, id => new {instance!.JSName}(id)).broadcast{evt.Name}"; + var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; builder.Append($"{Br}{name}({PrependIdArg(args)}) {{ {invName}({invArgs}); }}"); } else @@ -197,7 +196,7 @@ private void EmitEventExport (EventMeta evt) private void EmitEventImport (EventMeta evt) { - if (TryInstanced(evt, out _)) return; // instanced import event handlers are emitted in the registrar + if (isIt) return; // instanced import event handlers are emitted in the registrar var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; var invName = debug ? $"""getExport("{name}")""" : $"exports.{name}"; var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); @@ -207,73 +206,69 @@ private void EmitEventImport (EventMeta evt) private void EmitPropertyExport (PropertyMeta prop) { - var inst = TryInstanced(prop, out _); if (prop.CanGet) { var fnName = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var body = Deserialize(prop.Value, inst ? $"{invName}(_id)" : $"{invName}()"); - if (prop.Value.Nullable && !prop.Value.IsInstance) body += " ?? undefined"; - if (inst) builder.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); + var body = Deserialize(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); + if (prop.GetValue.Nullable && !prop.GetValue.IsInstanced) body += " ?? undefined"; + if (isIt) builder.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); else builder.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); } if (prop.CanSet) { var fnName = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var value = Serialize(prop.Value, "value"); - var body = inst ? $"{invName}(_id, {value})" : $"{invName}({value})"; - if (inst) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); + var value = Serialize(prop.SetValue, "value"); + var body = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; + if (isIt) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); else builder.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); } } private void EmitPropertyImport (PropertyMeta prop) { - var inst = TryInstanced(prop, out _); if (prop.CanGet) { - if (!inst) builder.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); - var args = inst ? "_id" : ""; - var body = Serialize(prop.Value, inst ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}"); + if (!isIt) builder.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); + var args = isIt ? "_id" : ""; + var body = Serialize(prop.GetValue, isIt ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}"); builder.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); } if (prop.CanSet) { - if (!inst) builder.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); - var value = Deserialize(prop.Value, "value"); - var args = inst ? "_id, value" : "value"; - var body = inst ? $"instances.imported(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; + if (!isIt) builder.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); + var value = Deserialize(prop.SetValue, "value"); + var args = isIt ? "_id, value" : "value"; + var body = isIt ? $"instances.imported(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; builder.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); } } private void EmitMethodExport (MethodMeta method) { - var inst = TryInstanced(method, out _); var wait = ShouldWait(method); var fnName = $"{method.Space.Replace('.', '_')}_{method.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (inst) args = PrependIdArg(args); + if (isIt) args = PrependIdArg(args); var invArgs = string.Join(", ", method.Arguments.Select(Serialize)); - if (inst) invArgs = PrependIdArg(invArgs); - var body = Deserialize(method.Value, $"{(wait ? "await " : "")}{invName}({invArgs})"); + if (isIt) invArgs = PrependIdArg(invArgs); + var body = Deserialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); builder.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); } private void EmitMethodImport (MethodMeta method) { - var inst = TryInstanced(method, out _); var wait = ShouldWait(method); var name = method.JSName; var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (inst) args = PrependIdArg(args); + if (isIt) args = PrependIdArg(args); var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); - var invName = inst ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; - var body = Serialize(method.Value, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var invName = isIt ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; + var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; - if (inst) builder.Append($"{Br}{name}Serialized: {serdeHandler}"); + if (isIt) builder.Append($"{Br}{name}Serialized: {serdeHandler}"); else { var serde = $"this.{name}SerializedHandler"; @@ -293,7 +288,7 @@ private void EmitEnum (Type @enum) builder.Append($"{Br}{@enum.Name}: {{ {fields} }}"); } - private void EmitInstanceRegistrar (InterfaceMeta instance) + private void EmitInstanceRegistrar (InstancedMeta instance) { var events = instance.Members.OfType().ToArray(); builder.Append( @@ -321,7 +316,7 @@ private void EmitInstanceRegistrar (InterfaceMeta instance) private string Serialize (ArgumentMeta arg) => Serialize(arg.Value, arg.JSName); private string Serialize (ValueMeta value, string exp) { - if (value.IsInstance) return RegisterInstance(value, exp); + if (value.IsInstanced) return RegisterInstance(value.Instanced, exp); if (value.IsSerialized) return $"serialize({exp}, {value.Serialized.Id})"; return exp; } @@ -329,40 +324,32 @@ private string Serialize (ValueMeta value, string exp) private string Deserialize (ArgumentMeta arg) => Deserialize(arg.Value, arg.JSName); private string Deserialize (ValueMeta value, string exp) { - if (value.InstanceType is { } it) + if (value.IsInstanced) { - var instance = instanced.First(i => i.Type == it); - if (instance.Interop == InteropKind.Import) return $"instances.imported({exp})"; - return $"instances.export({exp}, id => new {instance.JSName}(id))"; + if (value.Instanced.Interop == InteropKind.Import) return $"instances.imported({exp})"; + return $"instances.export({exp}, id => new {value.Instanced.JSName}(id))"; } if (value.IsSerialized) return $"deserialize({exp}, {value.Serialized.Id})"; return exp; } - private string RegisterInstance (ValueMeta value, string exp) + private string RegisterInstance (InstancedMeta it, string exp) { - var it = instanced.First(i => i.Type == value.InstanceType); if (it.Interop == InteropKind.Export) return $"{exp}._id"; if (it.Members.OfType().Any()) return $"{BuildRegistrarName(it)}({exp})"; return $"instances.import({exp})"; } - private static string BuildRegistrarName (InterfaceMeta it) + private static string BuildRegistrarName (InstancedMeta it) { - return $"register_{it.Type.FullName!.Replace('.', '_').Replace('+', '_')}"; - } - - private bool TryInstanced (MemberMeta member, [NotNullWhen(true)] out InterfaceMeta? instance) - { - instance = instanced.FirstOrDefault(i => i.Members.Contains(member)); - return instance is not null; + return $"register_{it.Type.Clr.FullName!.Replace('.', '_').Replace('+', '_')}"; } private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; - return method.Arguments.Any(a => a.Value.IsSerialized || a.Value.IsInstance) || - method.Value.IsSerialized || method.Value.IsInstance; + return method.Arguments.Any(a => a.Value.IsSerialized || a.Value.IsInstanced) || + method.Return.IsSerialized || method.Return.IsInstanced; } private string Br => $"{Comma}\n{Pad(level + 1)}"; diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs index cd8c0da9..aae6c9a3 100644 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs @@ -34,7 +34,7 @@ private Preferences ResolvePreferences () private SolutionInspection InspectSolution (Preferences prefs) { - var inspector = new SolutionInspector(prefs, EntryAssemblyName); + var inspector = new SolutionInspector(prefs); var inspection = inspector.Inspect(InspectedDirectory, GetFiles()); new InspectionReporter(Log).Report(inspection); return inspection; @@ -51,21 +51,21 @@ IEnumerable GetFiles () } } - private void GenerateBindings (Preferences prefs, SolutionInspection inspection) + private void GenerateBindings (Preferences prefs, SolutionInspection spec) { var generator = new BindingGenerator(prefs, Debug); - var content = generator.Generate(inspection); + var content = generator.Generate(spec); File.WriteAllText(Path.Combine(BuildDirectory, "bindings.g.js"), content); } - private void GenerateDeclarations (Preferences prefs, SolutionInspection inspection) + private void GenerateDeclarations (Preferences prefs, SolutionInspection spec) { var generator = new DeclarationGenerator(prefs); - var content = generator.Generate(inspection); + var content = generator.Generate(spec); File.WriteAllText(Path.Combine(BuildDirectory, "bindings.g.d.ts"), content); } - private void GenerateResources (SolutionInspection inspection) + private void GenerateResources (SolutionInspection spec) { var generator = new ResourceGenerator(EntryAssemblyName, EmbedBinaries, Debug, Globalization); var content = generator.Generate(BuildDirectory, DebugDirectory); diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs index 056af12d..d92d5454 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs @@ -2,12 +2,12 @@ namespace Bootsharp.Publish; internal sealed class DeclarationGenerator (Preferences prefs) { - private readonly MemberDeclarationGenerator membersGenerator = new(prefs); - private readonly TypeDeclarationGenerator typesGenerator = new(prefs); + private readonly MemberDeclarationGenerator members = new(prefs); + private readonly TypeDeclarationGenerator types = new(prefs); - public string Generate (SolutionInspection inspection) => Fmt(0, + public string Generate (SolutionInspection spec) => Fmt(0, """import type { EventBroadcaster, EventSubscriber } from "./event";""", - typesGenerator.Generate(inspection), - membersGenerator.Generate(inspection) + types.Generate(spec), + members.Generate(spec) ) + "\n"; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs index 1bb6583d..0b59817e 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs @@ -15,11 +15,11 @@ internal sealed class MemberDeclarationGenerator (Preferences prefs) private MemberMeta[] members = null!; private int index; - public string Generate (SolutionInspection inspection) + public string Generate (SolutionInspection spec) { - docs = new(inspection.Documentation); - members = inspection.StaticMembers - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) + docs = new(spec.Documentation); + members = spec.Static + .Concat(spec.Modules.SelectMany(i => i.Members)) .OrderBy(m => m.JSSpace).ToArray(); for (index = 0; index < members.Length; index++) DeclareMember(); @@ -73,10 +73,11 @@ private void DeclareEvent (EventMeta evt) private void DeclareProperty (PropertyMeta prop) { + var value = prop.GetValue ?? prop.SetValue!; builder.Append(docs.BuildProperty(prop.Info, 1)); builder.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); - builder.Append(typeBuilder.Build(prop.Value.Type.Clr, prop.Value.Nullability)); - if (prop.Value.Nullable) builder.Append(" | undefined"); + builder.Append(typeBuilder.Build(value.Type.Clr, value.Nullability)); + if (value.Nullable) builder.Append(" | undefined"); builder.Append(';'); } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 765b1ec6..cfbe3df9 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -6,7 +6,7 @@ namespace Bootsharp.Publish; internal sealed class TypeDeclarationGenerator (Preferences prefs) { private readonly StringBuilder builder = new(); - private readonly TypeSyntaxBuilder typeBuilder = new(prefs); + private readonly TypeSyntaxBuilder ts = new(prefs); private Type type => GetTypeAt(index); private Type? prevType => index == 0 ? null : GetTypeAt(index - 1); @@ -14,15 +14,15 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) private int indent => !string.IsNullOrEmpty(GetNamespace(type)) ? 1 : 0; private DocumentationBuilder docs = null!; - private InterfaceMeta[] instanced = null!; + private InstancedMeta[] instanced = null!; private Type[] types = null!; private int index; - public string Generate (SolutionInspection inspection) + public string Generate (SolutionInspection spec) { - docs = new(inspection.Documentation); - instanced = [..inspection.Instanced]; - types = inspection.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); + docs = new(spec.Documentation); + instanced = [..spec.Instanced]; + types = spec.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); return builder.ToString(); @@ -87,8 +87,8 @@ private void DeclareInterface () AppendLine($"export interface {BuildTypeName(type)}", indent); AppendExtensions(); builder.Append(" {"); - if (instanced.FirstOrDefault(i => i.Type == type) is { } inst) - foreach (var member in inst.Members) + if (instanced.FirstOrDefault(i => i.Type.Clr == type) is { } it) + foreach (var member in it.Members) switch (member) { case EventMeta e: AppendInstancedEvent(e); break; @@ -105,7 +105,7 @@ private void AppendExtensions () if (type.BaseType is { } baseType && types.Contains(baseType)) extTypes.Insert(0, baseType); if (extTypes.Count > 0) - builder.Append(" extends ").AppendJoin(", ", extTypes.Select(t => typeBuilder.Build(t, null))); + builder.Append(" extends ").AppendJoin(", ", extTypes.Select(t => ts.Build(t, null))); } private void AppendProperties () @@ -125,7 +125,7 @@ private void AppendProperty (string name, Type type, NullabilityInfo? nullabilit if (IsNullable(type, nullability)) builder.Append('?'); builder.Append(": "); if (type.IsGenericTypeParameter) builder.Append(type.Name); - else builder.Append(typeBuilder.Build(type, nullability)); + else builder.Append(ts.Build(type, nullability)); builder.Append(';'); } @@ -135,15 +135,16 @@ private void AppendInstancedEvent (EventMeta evt) var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; AppendLine(evt.JSName, indent + 1); builder.Append($": {type}<["); - builder.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); + builder.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); builder.Append("]>;"); } private void AppendInstancedProperty (PropertyMeta prop) { + var value = prop.GetValue ?? prop.SetValue!; builder.Append(docs.BuildProperty(prop.Info, indent + 1)); var name = prop.CanGet && !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; - AppendProperty(name, prop.Value.Type.Clr, prop.Value.Nullability); + AppendProperty(name, value.Type.Clr, value.Nullability); } private void AppendInstancedFunction (MethodMeta meta) @@ -151,9 +152,9 @@ private void AppendInstancedFunction (MethodMeta meta) builder.Append(docs.BuildFunction(meta, indent + 1)); AppendLine(meta.JSName, indent + 1); builder.Append('('); - builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); + builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); builder.Append("): "); - builder.Append(typeBuilder.BuildReturn(meta)); + builder.Append(ts.BuildReturn(meta)); builder.Append(';'); } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index 8a367327..08b87f4c 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -14,8 +14,8 @@ public string BuildArg (ArgumentMeta arg) public string BuildReturn (MethodMeta method) { - var nil = method.Value.Nullable ? " | null" : ""; - return Build(method.Value.Type.Clr, method.Value.Nullability) + nil; + var nil = method.Return.Nullable ? " | null" : ""; + return Build(method.Return.Type.Clr, method.Return.Nullability) + nil; } public string Build (Type type, NullabilityInfo? nullability) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs index 15450d64..af06e685 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs @@ -43,7 +43,6 @@ private void RemoveMaps () { // Microsoft bundles .NET JavaScript sources pre-minified/uglified with source maps // referencing upstream sources we don't publish with the package. - // TODO: Raise an issue asking them to add an option to not uglify the sources. var regex = new Regex(@"^\s*//# sourceMappingURL=.*?\.map\s*$\r?\n?", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline); diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index ffad2bc0..d354997d 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.158 + 0.8.0-alpha.161 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From a2b301a5b9a94c3f99d4cfae04b2d75668b61436 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 4 May 2026 01:38:31 +0300 Subject: [PATCH 04/20] iter --- .../Pack/DeclarationTest.cs | 132 ++++++++++----- .../SolutionInspector/InstancedInspector.cs | 4 + .../Pack/BindingGenerator/BindingGenerator.cs | 72 ++++---- .../MemberDeclarationGenerator.cs | 46 +++--- .../TypeDeclarationGenerator.cs | 155 +++++++++--------- 5 files changed, 228 insertions(+), 181 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 87d35de3..79ba72d1 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -47,8 +47,8 @@ public void WhenNoNamespaceDeclaresUnderRoot () Execute(); Contains( """ - export interface Record { - } + export type Record = Readonly<{ + }>; export enum Enum { A, B @@ -70,8 +70,8 @@ public void NestedTypesAreDeclaredUnderClassSpace () Contains( """ export namespace Foo { - export interface Bar { - } + export type Bar = Readonly<{ + }>; } export namespace Class { @@ -635,12 +635,13 @@ public void CanCrawlCustomTypes () public struct Struct { public double A { get; set; } } public readonly struct ReadonlyStruct { public double A { get; init; } } public readonly record struct ReadonlyRecordStruct(double A); - public record class RecordClass(double A); + public record class RecordClassA (double A, ReadonlyRecordStruct Str); + public record class RecordClassB (double B) : RecordClassA(42, new(24)); public enum Enum { A, B } public class Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } } - public class Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } } + public class Bar : Foo { public Dictionary Rc { get; } } public class Baz { public List Bars { get; } public Enum E { get; } } - public class Class { [Export] public static Baz GetBaz () => default; } + public class Class { [Export] public static Dictionary GetBaz () => default; } """)); Execute(); Contains( @@ -651,24 +652,27 @@ export interface Baz { readonly e: Space.Enum; } export interface Bar extends Space.Foo { - rrs: Space.ReadonlyRecordStruct; - rc: Space.RecordClass; + readonly rc: Map; } - export interface ReadonlyRecordStruct { + export type RecordClassB = Space.RecordClassA & Readonly<{ + b: number; + }>; + export type ReadonlyRecordStruct = Readonly<{ a: number; - } - export interface RecordClass { + }>; + export type RecordClassA = Readonly<{ a: number; - } - export interface Struct { + str: Space.ReadonlyRecordStruct; + }>; + export type Struct = Readonly<{ a: number; - } - export interface ReadonlyStruct { + }>; + export type ReadonlyStruct = Readonly<{ a: number; - } + }>; export interface Foo { - s: Space.Struct; - rs: Space.ReadonlyStruct; + readonly s: Space.Struct; + readonly rs: Space.ReadonlyStruct; } export enum Enum { A, @@ -677,13 +681,13 @@ export enum Enum { } export namespace Space.Class { - export function getBaz(): Space.Baz; + export function getBaz(): Map; } """); } [Fact] - public void StaticPropertiesAreNotIncluded () + public void StaticPropertiesAreIncluded () { AddAssembly( WithClass("public class Foo { public static string Soo { get; } }"), @@ -699,6 +703,50 @@ export interface Foo { """); } + [Fact] + public void IndexerPropertiesAreNotIncluded () + { + AddAssembly(WithClass( + """ + public record Foo + { + public bool this[int index] => true; + } + + [Export] public static Foo Bar () => default; + """)); + Execute(); + Contains( + """ + export namespace Class { + export type Foo = Readonly<{ + }>; + } + """); + } + + [Fact] + public void SetOnlyPropertiesAreNotIncluded () + { + AddAssembly(WithClass( + """ + public record Foo + { + public bool SetOnly { set { } } + } + + [Export] public static Foo Bar () => default; + """)); + Execute(); + Contains( + """ + export namespace Class { + export type Foo = Readonly<{ + }>; + } + """); + } + [Fact] public void ComputedPropertiesAreIncluded () { @@ -707,8 +755,6 @@ public void ComputedPropertiesAreIncluded () public record Foo { public bool Boo => true; - public bool SetOnly { set { } } - public bool this[int index] => true; } [Export] public static Foo Bar () => default; @@ -717,9 +763,9 @@ public bool SetOnly { set { } } Contains( """ export namespace Class { - export interface Foo { + export type Foo = Readonly<{ boo: boolean; - } + }>; } """); } @@ -740,9 +786,9 @@ public interface IImported { Info Fun (string str, Info info); } Execute(); Contains( """ - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export namespace Exported { export function inv(str: string, info: Info): Info; @@ -775,9 +821,9 @@ public class Class export interface IImported { fun(info: Info, str: string): Info; } - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export interface IExported { inv(str: string, info: Info): Info; reset(): void; @@ -821,9 +867,9 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export interface IExportedInstanced { } export interface IImportedInstanced { @@ -878,9 +924,9 @@ export interface IImported { readonly imported: IImported; exported: IExported; } - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export interface IExported { state: Info; readonly exported: IExported; @@ -913,9 +959,9 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export interface IExportedInstanced { } export interface IImportedInstanced { @@ -952,9 +998,9 @@ public class Class export interface IImported { changed: EventBroadcaster<[arg1: IImported, arg2: Info, arg3: string]>; } - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; export interface IExported { changed: EventSubscriber<[obj: Info]>; done: EventSubscriber<[]>; @@ -1180,10 +1226,10 @@ [Export] public static void Inv (Record r, Generic g) {} Execute(); Contains( """ - export interface Record { - } - export interface Generic { - } + export type Record = Readonly<{ + }>; + export type Generic = Readonly<{ + }>; export namespace Class { export function inv(r: Foo, g: Bar): void; @@ -1336,12 +1382,12 @@ export enum Kind { /** * A payload sent across interop. */ - export interface Payload { + export type Payload = Readonly<{ /** * The payload name. */ name: string; - } + }>; """); Contains( """ diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs index 276ff863..1b00059e 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs @@ -10,7 +10,11 @@ internal sealed class InstancedInspector (TypeInspector types) { if (byType.TryGetValue(type, out var meta)) return meta; if (IsTaskWithResult(type, out var result)) return Inspect(result, ik, members); + if (IsList(type, out var element)) return Inspect(element, ik, members); + if (IsDictionary(type, out _, out var value)) return Inspect(value, ik, members); if (!IsInstancedType(type)) return null; + if (type.BaseType is { } b && Inspect(b, ik, members) is { } bm) byType[b] = bm; + // TODO: I dont like this crawling shit here, especially the base type. return CollectMembers(byType[type] = InspectType(type, ik), members); } diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index e23c5d54..d60e7b6f 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -11,7 +11,7 @@ private record Binding (MemberMeta? Member, Type? Enum, InstancedMeta? It, strin private Binding? prevBinding => index == 0 ? null : bindings[index - 1]; private Binding? nextBinding => index == bindings.Length - 1 ? null : bindings[index + 1]; - private readonly StringBuilder builder = new(); + private readonly StringBuilder bld = new(); [MemberNotNullWhen(true, nameof(it))] private bool isIt => it != null; private InstancedMeta? it => binding.It; private Binding[] bindings = []; @@ -31,37 +31,37 @@ public string Generate (SolutionInspection spec) if (bindings.Length == 0) return ""; EmitImports(); - builder.Append("\n\n"); + bld.Append("\n\n"); if (debug) { EmitDebugHelpers(); - builder.Append("\n\n"); + bld.Append("\n\n"); } EmitHelpers(); - builder.Append("\n\n"); + bld.Append("\n\n"); - builder.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); - builder.Append("\n\n"); + bld.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); + bld.Append("\n\n"); foreach (var instance in spec.Instanced .Where(i => i.Interop == InteropKind.Import && i.Members.OfType().Any())) EmitInstanceRegistrar(instance); - builder.Append("\n\n"); + bld.Append("\n\n"); if (spec.Instanced.Count > 0) - builder.Append(new BindingClassGenerator().Generate(spec)); + bld.Append(new BindingClassGenerator().Generate(spec)); for (index = 0; index < bindings.Length; index++) EmitBinding(); - return builder.ToString(); + return bld.ToString(); } private void EmitImports () { - builder.Append( + bld.Append( """ import { exports } from "./exports"; import { Event } from "./event"; @@ -73,7 +73,7 @@ private void EmitImports () private void EmitDebugHelpers () { - builder.Append( + bld.Append( """ function getExport(name) { return (...args) => { @@ -97,7 +97,7 @@ function getImport(handler, serializedHandler, name) { private void EmitHelpers () { - builder.Append( + bld.Append( """ function importEvent(handler) { const event = new Event(); @@ -130,8 +130,8 @@ private void OpenNamespace () var parts = binding.Namespace.Split('.'); while (prevParts.ElementAtOrDefault(level) == parts[level]) level++; for (var i = level; i < parts.Length; level = i, i++) - if (i == 0) builder.Append($"\nexport const {parts[i]} = {{"); - else builder.Append($"{Comma}\n{Pad(i)}{parts[i]}: {{"); + if (i == 0) bld.Append($"\nexport const {parts[i]} = {{"); + else bld.Append($"{Comma}\n{Pad(i)}{parts[i]}: {{"); } private bool ShouldCloseNamespace () @@ -144,8 +144,8 @@ private void CloseNamespace () { var target = GetCloseLevel(); for (; level >= target; level--) - if (level == 0) builder.Append("\n};"); - else builder.Append($"\n{Pad(level)}}}"); + if (level == 0) bld.Append("\n};"); + else bld.Append($"\n{Pad(level)}}}"); int GetCloseLevel () { @@ -184,13 +184,13 @@ private void EmitEventExport (EventMeta evt) if (isIt) { var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; - builder.Append($"{Br}{name}({PrependIdArg(args)}) {{ {invName}({invArgs}); }}"); + bld.Append($"{Br}{name}({PrependIdArg(args)}) {{ {invName}({invArgs}); }}"); } else { var invName = $"{evt.JSSpace}.{evt.JSName}.broadcast"; - builder.Append($"{Br}{evt.JSName}: new Event()"); - builder.Append($"{Br}{name}: ({args}) => {invName}({invArgs})"); + bld.Append($"{Br}{evt.JSName}: new Event()"); + bld.Append($"{Br}{name}: ({args}) => {invName}({invArgs})"); } } @@ -201,7 +201,7 @@ private void EmitEventImport (EventMeta evt) var invName = debug ? $"""getExport("{name}")""" : $"exports.{name}"; var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); - builder.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); + bld.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); } private void EmitPropertyExport (PropertyMeta prop) @@ -212,8 +212,8 @@ private void EmitPropertyExport (PropertyMeta prop) var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var body = Deserialize(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); if (prop.GetValue.Nullable && !prop.GetValue.IsInstanced) body += " ?? undefined"; - if (isIt) builder.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); - else builder.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); + if (isIt) bld.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); + else bld.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); } if (prop.CanSet) { @@ -221,8 +221,8 @@ private void EmitPropertyExport (PropertyMeta prop) var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var value = Serialize(prop.SetValue, "value"); var body = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; - if (isIt) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); - else builder.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); + if (isIt) bld.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); + else bld.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); } } @@ -230,18 +230,18 @@ private void EmitPropertyImport (PropertyMeta prop) { if (prop.CanGet) { - if (!isIt) builder.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); + if (!isIt) bld.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); var args = isIt ? "_id" : ""; var body = Serialize(prop.GetValue, isIt ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}"); - builder.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); + bld.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); } if (prop.CanSet) { - if (!isIt) builder.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); + if (!isIt) bld.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); var value = Deserialize(prop.SetValue, "value"); var args = isIt ? "_id, value" : "value"; var body = isIt ? $"instances.imported(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; - builder.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); + bld.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); } } @@ -255,7 +255,7 @@ private void EmitMethodExport (MethodMeta method) var invArgs = string.Join(", ", method.Arguments.Select(Serialize)); if (isIt) invArgs = PrependIdArg(invArgs); var body = Deserialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); - builder.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); + bld.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); } private void EmitMethodImport (MethodMeta method) @@ -268,14 +268,14 @@ private void EmitMethodImport (MethodMeta method) var invName = isIt ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; - if (isIt) builder.Append($"{Br}{name}Serialized: {serdeHandler}"); + if (isIt) bld.Append($"{Br}{name}Serialized: {serdeHandler}"); else { var serde = $"this.{name}SerializedHandler"; var serdeExp = debug ? $"getImport({invName}, {serde}, \"{binding.Namespace}.{name}\")" : serde; - builder.Append($"{Br}get {name}() {{ return {invName}; }}"); - builder.Append($"{Br}set {name}(handler) {{ {invName} = handler; {serde} = {serdeHandler}; }}"); - builder.Append($"{Br}get {name}Serialized() {{ return {serdeExp}; }}"); + bld.Append($"{Br}get {name}() {{ return {invName}; }}"); + bld.Append($"{Br}set {name}(handler) {{ {invName} = handler; {serde} = {serdeHandler}; }}"); + bld.Append($"{Br}get {name}Serialized() {{ return {serdeExp}; }}"); } } @@ -285,13 +285,13 @@ private void EmitEnum (Type @enum) var fields = string.Join(", ", values .Select(v => $"\"{v}\": \"{Enum.GetName(@enum, v)}\"") .Concat(values.Select(v => $"\"{Enum.GetName(@enum, v)}\": {v}"))); - builder.Append($"{Br}{@enum.Name}: {{ {fields} }}"); + bld.Append($"{Br}{@enum.Name}: {{ {fields} }}"); } private void EmitInstanceRegistrar (InstancedMeta instance) { var events = instance.Members.OfType().ToArray(); - builder.Append( + bld.Append( $$""" function {{BuildRegistrarName(instance)}}(instance) { return instances.import(instance, _id => { @@ -354,5 +354,5 @@ private bool ShouldWait (MethodMeta method) private string Br => $"{Comma}\n{Pad(level + 1)}"; private string Pad (int level) => new(' ', level * 4); - private string Comma => builder[^1] == '{' ? "" : ","; + private string Comma => bld[^1] == '{' ? "" : ","; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs index 0b59817e..70907ea9 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs @@ -4,8 +4,8 @@ namespace Bootsharp.Publish; internal sealed class MemberDeclarationGenerator (Preferences prefs) { - private readonly StringBuilder builder = new(); - private readonly TypeSyntaxBuilder typeBuilder = new(prefs); + private readonly StringBuilder bld = new(); + private readonly TypeSyntaxBuilder ts = new(prefs); private MemberMeta member => members[index]; private MemberMeta? prevMember => index == 0 ? null : members[index - 1]; @@ -23,7 +23,7 @@ public string Generate (SolutionInspection spec) .OrderBy(m => m.JSSpace).ToArray(); for (index = 0; index < members.Length; index++) DeclareMember(); - return builder.ToString(); + return bld.ToString(); } private void DeclareMember () @@ -47,8 +47,8 @@ private bool ShouldOpenNamespace () private void OpenNamespace () { - builder.Append(docs.BuildType(member.Info.DeclaringType!, 0)); - builder.Append($"\nexport namespace {member.JSSpace} {{"); + bld.Append(docs.BuildType(member.Info.DeclaringType!, 0)); + bld.Append($"\nexport namespace {member.JSSpace} {{"); } private bool ShouldCloseNamespace () @@ -59,41 +59,41 @@ private bool ShouldCloseNamespace () private void CloseNamespace () { - builder.Append("\n}"); + bld.Append("\n}"); } private void DeclareEvent (EventMeta evt) { - builder.Append(docs.BuildEvent(evt, 1)); + bld.Append(docs.BuildEvent(evt, 1)); var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; - builder.Append($"\n export const {evt.JSName}: {type}<["); - builder.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append("]>;"); + bld.Append($"\n export const {evt.JSName}: {type}<["); + bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.Append("]>;"); } private void DeclareProperty (PropertyMeta prop) { var value = prop.GetValue ?? prop.SetValue!; - builder.Append(docs.BuildProperty(prop.Info, 1)); - builder.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); - builder.Append(typeBuilder.Build(value.Type.Clr, value.Nullability)); - if (value.Nullable) builder.Append(" | undefined"); - builder.Append(';'); + bld.Append(docs.BuildProperty(prop.Info, 1)); + bld.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); + bld.Append(ts.Build(value.Type.Clr, value.Nullability)); + if (value.Nullable) bld.Append(" | undefined"); + bld.Append(';'); } private void DeclareMethodExport (MethodMeta method) { - builder.Append(docs.BuildFunction(method, 1)); - builder.Append($"\n export function {method.JSName}("); - builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append($"): {typeBuilder.BuildReturn(method)};"); + bld.Append(docs.BuildFunction(method, 1)); + bld.Append($"\n export function {method.JSName}("); + bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.Append($"): {ts.BuildReturn(method)};"); } private void DeclareMethodImport (MethodMeta method) { - builder.Append(docs.BuildFunction(method, 1)); - builder.Append($"\n export let {method.JSName}: ("); - builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append($") => {typeBuilder.BuildReturn(method)};"); + bld.Append(docs.BuildFunction(method, 1)); + bld.Append($"\n export let {method.JSName}: ("); + bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.Append($") => {ts.BuildReturn(method)};"); } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index cfbe3df9..1a32db52 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -5,40 +5,35 @@ namespace Bootsharp.Publish; internal sealed class TypeDeclarationGenerator (Preferences prefs) { - private readonly StringBuilder builder = new(); + private readonly StringBuilder bld = new(); private readonly TypeSyntaxBuilder ts = new(prefs); - private Type type => GetTypeAt(index); - private Type? prevType => index == 0 ? null : GetTypeAt(index - 1); - private Type? nextType => index == types.Length - 1 ? null : GetTypeAt(index + 1); + private Type type => types[index]; + private Type? prevType => index == 0 ? null : types[index - 1]; + private Type? nextType => index == types.Length - 1 ? null : types[index + 1]; private int indent => !string.IsNullOrEmpty(GetNamespace(type)) ? 1 : 0; private DocumentationBuilder docs = null!; - private InstancedMeta[] instanced = null!; + private Dictionary itByType = null!; private Type[] types = null!; private int index; public string Generate (SolutionInspection spec) { docs = new(spec.Documentation); - instanced = [..spec.Instanced]; + itByType = spec.Instanced.ToDictionary(it => it.Type.Clr); types = spec.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); - return builder.ToString(); - } - - private Type GetTypeAt (int index) - { - var type = types[index]; - return type.IsGenericType ? type.GetGenericTypeDefinition() : type; + return bld.ToString(); } private void DeclareType () { if (ShouldOpenNamespace()) OpenNamespace(); - if (type.IsEnum) DeclareEnum(); - else DeclareInterface(); + if (itByType.TryGetValue(type, out var it)) DeclareInstanced(it); + else if (type.IsEnum) DeclareEnum(); + else DeclareSerialized(); if (ShouldCloseNamespace()) CloseNamespace(); } @@ -69,111 +64,113 @@ private void CloseNamespace () private void DeclareEnum () { - builder.Append(docs.BuildType(type, indent)); + bld.Append(docs.BuildType(type, indent)); AppendLine($"export enum {type.Name} {{", indent); var names = Enum.GetNames(type); for (int i = 0; i < names.Length; i++) { - builder.Append(docs.BuildProperty(type.GetField(names[i])!, indent + 1)); + bld.Append(docs.BuildProperty(type.GetField(names[i])!, indent + 1)); if (i == names.Length - 1) AppendLine(names[i], indent + 1); else AppendLine($"{names[i]},", indent + 1); } AppendLine("}", indent); } - private void DeclareInterface () - { - builder.Append(docs.BuildType(type, indent)); - AppendLine($"export interface {BuildTypeName(type)}", indent); - AppendExtensions(); - builder.Append(" {"); - if (instanced.FirstOrDefault(i => i.Type.Clr == type) is { } it) - foreach (var member in it.Members) - switch (member) - { - case EventMeta e: AppendInstancedEvent(e); break; - case PropertyMeta p: AppendInstancedProperty(p); break; - case MethodMeta m: AppendInstancedFunction(m); break; - } - else AppendProperties(); - AppendLine("}", indent); - } - - private void AppendExtensions () + private void DeclareSerialized () { - var extTypes = new List(type.GetInterfaces().Where(types.Contains)); + bld.Append(docs.BuildType(type, indent)); + AppendLine($"export type {BuildTypeName(type)} = ", indent); if (type.BaseType is { } baseType && types.Contains(baseType)) - extTypes.Insert(0, baseType); - if (extTypes.Count > 0) - builder.Append(" extends ").AppendJoin(", ", extTypes.Select(t => ts.Build(t, null))); - } - - private void AppendProperties () - { + bld.Append(ts.Build(baseType, null)).Append(" & "); + bld.Append("Readonly<{"); var flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance; foreach (var prop in type.GetProperties(flags)) if (prop.GetMethod != null && prop.GetIndexParameters().Length == 0) { - builder.Append(docs.BuildProperty(prop, indent + 1)); + bld.Append(docs.BuildProperty(prop, indent + 1)); AppendProperty(ToFirstLower(prop.Name), prop.PropertyType, GetNullability(prop)); } + AppendLine("}>;", indent); } - private void AppendProperty (string name, Type type, NullabilityInfo? nullability) + private void DeclareInstanced (InstancedMeta it) { - AppendLine(name, indent + 1); - if (IsNullable(type, nullability)) builder.Append('?'); - builder.Append(": "); - if (type.IsGenericTypeParameter) builder.Append(type.Name); - else builder.Append(ts.Build(type, nullability)); - builder.Append(';'); - } + bld.Append(docs.BuildType(type, indent)); + AppendLine($"export interface {BuildTypeName(type)}", indent); + AppendExtensions(); + bld.Append(" {"); + foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Type.Clr)) + if (member is EventMeta evt) AppendEvent(evt); + else if (member is PropertyMeta prop) AppendProperty(prop); + else AppendMethod((MethodMeta)member); + AppendLine("}", indent); - private void AppendInstancedEvent (EventMeta evt) - { - builder.Append(docs.BuildEvent(evt, indent + 1)); - var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; - AppendLine(evt.JSName, indent + 1); - builder.Append($": {type}<["); - builder.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); - builder.Append("]>;"); - } + void AppendExtensions () + { + var extTypes = new List(type.GetInterfaces().Where(types.Contains)); + if (type.BaseType is { } baseType && types.Contains(baseType)) + extTypes.Insert(0, baseType); + if (extTypes.Count > 0) + bld.Append(" extends ").AppendJoin(", ", extTypes.Select(t => ts.Build(t, null))); + } - private void AppendInstancedProperty (PropertyMeta prop) - { - var value = prop.GetValue ?? prop.SetValue!; - builder.Append(docs.BuildProperty(prop.Info, indent + 1)); - var name = prop.CanGet && !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; - AppendProperty(name, value.Type.Clr, value.Nullability); + void AppendEvent (EventMeta evt) + { + bld.Append(docs.BuildEvent(evt, indent + 1)); + AppendLine(evt.JSName, indent + 1); + var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; + bld.Append($": {type}<["); + bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.Append("]>;"); + } + + void AppendProperty (PropertyMeta prop) + { + bld.Append(docs.BuildProperty(prop.Info, indent + 1)); + var name = !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; + var value = prop.GetValue ?? prop.SetValue!; + this.AppendProperty(name, value.Type.Clr, value.Nullability); + } + + void AppendMethod (MethodMeta meta) + { + bld.Append(docs.BuildFunction(meta, indent + 1)); + AppendLine(meta.JSName, indent + 1); + bld.Append('('); + bld.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.Append("): "); + bld.Append(ts.BuildReturn(meta)); + bld.Append(';'); + } } - private void AppendInstancedFunction (MethodMeta meta) + private void AppendProperty (string name, Type type, NullabilityInfo? nullability) { - builder.Append(docs.BuildFunction(meta, indent + 1)); - AppendLine(meta.JSName, indent + 1); - builder.Append('('); - builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); - builder.Append("): "); - builder.Append(ts.BuildReturn(meta)); - builder.Append(';'); + AppendLine(name, indent + 1); + if (IsNullable(type, nullability)) bld.Append('?'); + bld.Append(": "); + if (type.IsGenericTypeParameter) bld.Append(type.GetGenericTypeDefinition().Name); + else bld.Append(ts.Build(type, nullability)); + bld.Append(';'); } private void AppendLine (string content, int level) { - builder.Append('\n'); + bld.Append('\n'); Append(content, level); } private void Append (string content, int level) { for (int i = 0; i < level * 4; i++) - builder.Append(' '); - builder.Append(content); + bld.Append(' '); + bld.Append(content); } private string BuildTypeName (Type type) { if (!type.IsGenericType) return type.Name; + type = type.GetGenericTypeDefinition(); var name = TrimGeneric(type.Name); var args = string.Join(", ", type.GetGenericArguments().Select(BuildTypeName)); return $"{name}<{args}>"; From 22a86d16697ec9e3994f18c7420fe7145221976b Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 4 May 2026 17:11:31 +0300 Subject: [PATCH 05/20] iter --- .../Pack/DeclarationTest.cs | 40 +++++++++-- .../DeclarationGenerator.cs | 4 +- ...rator.cs => ModuleDeclarationGenerator.cs} | 16 ++--- .../TypeDeclarationGenerator.cs | 51 ++++++-------- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 68 ++++++++++++++++--- 5 files changed, 120 insertions(+), 59 deletions(-) rename src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/{MemberDeclarationGenerator.cs => ModuleDeclarationGenerator.cs} (87%) diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 79ba72d1..8b4f336a 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -535,8 +535,8 @@ export namespace n.Class { public void DefinitionIsGeneratedForGenericClass () { AddAssembly( - With("n", "public class Generic where T: notnull { public T Value { get; set; } }"), - With("n", "public class GenericNull { public T Value { get; set; } }"), + With("n", "public class Generic where T: notnull { public required T Value { get; set; } }"), + With("n", "public class GenericNull { public T? Value { get; } public T? Foo (T? t) => default; }"), WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); Execute(); Contains( @@ -546,7 +546,8 @@ export interface Generic { value: T; } export interface GenericNull { - value?: T; + readonly value?: T; + foo(t: T | undefined): T | null; } } @@ -556,6 +557,31 @@ export namespace n.Class { """); } + [Fact] + public void DefinitionIsGeneratedForGenericRecord () + { + AddAssembly( + With("n", "public record Generic where T: notnull { public T Value { get; set; } }"), + With("n", "public record GenericNull { public T? Value { get; set; } }"), + WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); + Execute(); + Contains( + """ + export namespace n { + export type Generic = Readonly<{ + value: T; + }>; + export type GenericNull = Readonly<{ + value?: T; + }>; + } + + export namespace n.Class { + export function method(a: n.Generic, b: n.GenericNull): void; + } + """); + } + [Fact] public void DefinitionIsGeneratedForGenericInterface () { @@ -1212,7 +1238,7 @@ public void RespectsTypePreference () AddAssembly(With( """ [assembly: Bootsharp.Preferences( - Type = [@"Record", "Foo", @".+`.+", "Bar"] + Type = [@"Record", "Foo", @".+`.+", "Bar"] )] public record Record; @@ -1226,13 +1252,13 @@ [Export] public static void Inv (Record r, Generic g) {} Execute(); Contains( """ - export type Record = Readonly<{ + export type Foo = Readonly<{ }>; - export type Generic = Readonly<{ + export type Bar = Readonly<{ }>; export namespace Class { - export function inv(r: Foo, g: Bar): void; + export function inv(r: Foo, g: Bar): void; } """); } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs index d92d5454..68431743 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs @@ -2,12 +2,12 @@ namespace Bootsharp.Publish; internal sealed class DeclarationGenerator (Preferences prefs) { - private readonly MemberDeclarationGenerator members = new(prefs); + private readonly ModuleDeclarationGenerator modules = new(prefs); private readonly TypeDeclarationGenerator types = new(prefs); public string Generate (SolutionInspection spec) => Fmt(0, """import type { EventBroadcaster, EventSubscriber } from "./event";""", types.Generate(spec), - members.Generate(spec) + modules.Generate(spec) ) + "\n"; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs similarity index 87% rename from src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs rename to src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs index 70907ea9..d83b36c9 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs @@ -2,7 +2,7 @@ namespace Bootsharp.Publish; -internal sealed class MemberDeclarationGenerator (Preferences prefs) +internal sealed class ModuleDeclarationGenerator (Preferences prefs) { private readonly StringBuilder bld = new(); private readonly TypeSyntaxBuilder ts = new(prefs); @@ -67,17 +67,15 @@ private void DeclareEvent (EventMeta evt) bld.Append(docs.BuildEvent(evt, 1)); var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; bld.Append($"\n export const {evt.JSName}: {type}<["); - bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); bld.Append("]>;"); } private void DeclareProperty (PropertyMeta prop) { - var value = prop.GetValue ?? prop.SetValue!; bld.Append(docs.BuildProperty(prop.Info, 1)); bld.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); - bld.Append(ts.Build(value.Type.Clr, value.Nullability)); - if (value.Nullable) bld.Append(" | undefined"); + bld.Append(ts.BuildVariable(prop.Info)); bld.Append(';'); } @@ -85,15 +83,15 @@ private void DeclareMethodExport (MethodMeta method) { bld.Append(docs.BuildFunction(method, 1)); bld.Append($"\n export function {method.JSName}("); - bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); - bld.Append($"): {ts.BuildReturn(method)};"); + bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); + bld.Append($"): {ts.BuildReturn(method.Info)};"); } private void DeclareMethodImport (MethodMeta method) { bld.Append(docs.BuildFunction(method, 1)); bld.Append($"\n export let {method.JSName}: ("); - bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); - bld.Append($") => {ts.BuildReturn(method)};"); + bld.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); + bld.Append($") => {ts.BuildReturn(method.Info)};"); } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 1a32db52..691753f1 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -79,24 +79,29 @@ private void DeclareEnum () private void DeclareSerialized () { bld.Append(docs.BuildType(type, indent)); - AppendLine($"export type {BuildTypeName(type)} = ", indent); + AppendLine($"export type {ts.BuildName(type)} = ", indent); if (type.BaseType is { } baseType && types.Contains(baseType)) - bld.Append(ts.Build(baseType, null)).Append(" & "); + bld.Append(ts.BuildFullName(baseType)).Append(" & "); bld.Append("Readonly<{"); var flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance; foreach (var prop in type.GetProperties(flags)) if (prop.GetMethod != null && prop.GetIndexParameters().Length == 0) - { - bld.Append(docs.BuildProperty(prop, indent + 1)); - AppendProperty(ToFirstLower(prop.Name), prop.PropertyType, GetNullability(prop)); - } + AppendProperty(prop); AppendLine("}>;", indent); + + void AppendProperty (PropertyInfo prop) + { + bld.Append(docs.BuildProperty(prop, indent + 1)); + AppendLine(ToFirstLower(prop.Name), indent + 1); + bld.Append(ts.BuildProperty(prop)); + bld.Append(';'); + } } private void DeclareInstanced (InstancedMeta it) { bld.Append(docs.BuildType(type, indent)); - AppendLine($"export interface {BuildTypeName(type)}", indent); + AppendLine($"export interface {ts.BuildName(type)}", indent); AppendExtensions(); bld.Append(" {"); foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Type.Clr)) @@ -111,7 +116,7 @@ void AppendExtensions () if (type.BaseType is { } baseType && types.Contains(baseType)) extTypes.Insert(0, baseType); if (extTypes.Count > 0) - bld.Append(" extends ").AppendJoin(", ", extTypes.Select(t => ts.Build(t, null))); + bld.Append(" extends ").AppendJoin(", ", extTypes.Select(ts.BuildFullName)); } void AppendEvent (EventMeta evt) @@ -120,7 +125,7 @@ void AppendEvent (EventMeta evt) AppendLine(evt.JSName, indent + 1); var type = evt.Interop == InteropKind.Export ? "EventSubscriber" : "EventBroadcaster"; bld.Append($": {type}<["); - bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.AppendJoin(", ", evt.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(evt.Info, a.Info)}")); bld.Append("]>;"); } @@ -128,8 +133,9 @@ void AppendProperty (PropertyMeta prop) { bld.Append(docs.BuildProperty(prop.Info, indent + 1)); var name = !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; - var value = prop.GetValue ?? prop.SetValue!; - this.AppendProperty(name, value.Type.Clr, value.Nullability); + AppendLine(name, indent + 1); + bld.Append(ts.BuildProperty(prop.Info)); + bld.Append(';'); } void AppendMethod (MethodMeta meta) @@ -137,23 +143,13 @@ void AppendMethod (MethodMeta meta) bld.Append(docs.BuildFunction(meta, indent + 1)); AppendLine(meta.JSName, indent + 1); bld.Append('('); - bld.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a)}")); + bld.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {ts.BuildArg(a.Info)}")); bld.Append("): "); - bld.Append(ts.BuildReturn(meta)); + bld.Append(ts.BuildReturn(meta.Info)); bld.Append(';'); } } - private void AppendProperty (string name, Type type, NullabilityInfo? nullability) - { - AppendLine(name, indent + 1); - if (IsNullable(type, nullability)) bld.Append('?'); - bld.Append(": "); - if (type.IsGenericTypeParameter) bld.Append(type.GetGenericTypeDefinition().Name); - else bld.Append(ts.Build(type, nullability)); - bld.Append(';'); - } - private void AppendLine (string content, int level) { bld.Append('\n'); @@ -167,15 +163,6 @@ private void Append (string content, int level) bld.Append(content); } - private string BuildTypeName (Type type) - { - if (!type.IsGenericType) return type.Name; - type = type.GetGenericTypeDefinition(); - var name = TrimGeneric(type.Name); - var args = string.Join(", ", type.GetGenericArguments().Select(BuildTypeName)); - return $"{name}<{args}>"; - } - private string GetNamespace (Type type) { return BuildJSSpace(type, prefs); diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index 08b87f4c..21967646 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -6,28 +6,78 @@ internal sealed class TypeSyntaxBuilder (Preferences prefs) { private NullabilityInfo? nullability; - public string BuildArg (ArgumentMeta arg) + public string BuildName (Type type) { - var nil = arg.Value.Nullable ? " | undefined" : ""; - return Build(arg.Value.Type.Clr, arg.Value.Nullability) + nil; + var full = BuildFullName(type); + var dotIdx = full.LastIndexOf('.'); + return dotIdx > 0 ? full[(dotIdx + 1)..] : full; } - public string BuildReturn (MethodMeta method) + public string BuildFullName (Type type) { - var nil = method.Return.Nullable ? " | null" : ""; - return Build(method.Return.Type.Clr, method.Return.Nullability) + nil; + if (type.IsGenericType) type = type.GetGenericTypeDefinition(); + return Build(type, null); } - public string Build (Type type, NullabilityInfo? nullability) + public string BuildArg (ParameterInfo param) + { + if (param.Member.DeclaringType!.IsGenericType) + param = param.Member.DeclaringType.GetGenericTypeDefinition() + .GetMethod(param.Member.Name)!.GetParameters()[param.Position]; + var nul = GetNullability(param); + var post = IsNullable(param.ParameterType, nul) ? " | undefined" : ""; + return Build(param.ParameterType, nul) + post; + } + + public string BuildArg (EventInfo evt, ParameterInfo param) + { + var nul = GetNullability(param); + if (evt.EventHandlerType!.IsGenericType) + { + var arg = evt.EventHandlerType.GetGenericTypeDefinition() + .GetMethod("Invoke")!.GetParameters()[param.Position].ParameterType; + if (arg.IsGenericParameter) nul = GetNullability(evt).GenericTypeArguments[arg.GenericParameterPosition]; + } + var post = IsNullable(param.ParameterType, nul) ? " | undefined" : ""; + return Build(param.ParameterType, nul) + post; + } + + public string BuildReturn (MethodInfo method) + { + if (method.DeclaringType!.IsGenericType) + method = method.DeclaringType.GetGenericTypeDefinition().GetMethod(method.Name)!; + var nul = GetNullability(method.ReturnParameter); + var post = IsNullable(method.ReturnType, nul) ? " | null" : ""; + return Build(method.ReturnType, nul) + post; + } + + public string BuildProperty (PropertyInfo prop) + { + if (prop.DeclaringType!.IsGenericType) + prop = prop.DeclaringType.GetGenericTypeDefinition().GetProperty(prop.Name)!; + var nul = GetNullability(prop); + var pre = IsNullable(prop.PropertyType, nul) ? "?: " : ": "; + return pre + Build(prop.PropertyType, nul); + } + + public string BuildVariable (PropertyInfo prop) + { + var nul = GetNullability(prop); + var post = IsNullable(prop.PropertyType, nul) ? " | undefined" : ""; + return Build(prop.PropertyType, nul) + post; + } + + private string Build (Type type, NullabilityInfo? nullability) { this.nullability = nullability; - // nullability of topmost declarations is handled upstream (?/undefined/null) + // nullability of topmost declarations is handled downstream (?/undefined/null) if (IsNullable(type, nullability, out var value)) type = value; return WithPrefs(prefs.Type, type.FullName!, Build(type)); } private string Build (Type type) { + if (type.IsGenericTypeParameter) return type.Name; if (IsNullable(type, out var nullValue)) return BuildNullable(nullValue); if (IsTaskLike(type)) return BuildTask(type); if (IsList(type, out var element)) return BuildList(type, element); @@ -80,7 +130,7 @@ private string BuildUser (Type type) var full = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; if (!type.IsGenericType) return full; EnterNullability(); - var args = string.Join(", ", type.GenericTypeArguments.Select(Build)); + var args = string.Join(", ", type.GetGenericArguments().Select(Build)); return $"{full}<{args}>"; } From fc135f1d20c875acd82bdd85d0bed67d095aa61d Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 4 May 2026 19:41:34 +0300 Subject: [PATCH 06/20] cover --- .../Emit/InteropTest.cs | 15 +++-- .../Common/Global/GlobalType.cs | 13 +++- .../SolutionInspector/MemberInspector.cs | 61 ++++++------------- .../Emit/InstanceGenerator.cs | 2 +- .../Emit/InteropGenerator.cs | 1 + .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 2 +- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 8 +-- src/cs/Directory.Build.props | 2 +- 8 files changed, 44 insertions(+), 60 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index fb77719b..0227baa2 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -320,15 +320,17 @@ public void DoesNotGenerateForUnsupportedProperties () public interface IExportedStatic { - int Ignored { get => 0; } - int IgnoredToo { set { } } + int DefaultGet { get => 0; } + int DefaultSet { set { } } + int BothDefault { get => 0; set { } } int this[int index] { get; set; } } public interface IExportedInstanced { - int Ignored { get => 0; } - int IgnoredToo { set { } } + int DefaultGet { get => 0; } + int DefaultSet { set { } } + int BothDefault { get => 0; set { } } int this[int index] { get; set; } } @@ -338,8 +340,9 @@ public class Class } """)); Execute(); - DoesNotContain("Ignored"); - DoesNotContain("IgnoredToo"); + DoesNotContain("DefaultGet"); + DoesNotContain("DefaultSet"); + DoesNotContain("BothDefault"); DoesNotContain("GetPropertyItem"); DoesNotContain("SetPropertyItem"); } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index faf5f9d1..956a3852 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -56,9 +56,20 @@ static bool IsDictionary (Type type) => type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } - public static NullabilityInfo GetNullability (EventInfo evt) => new NullabilityInfoContext().Create(evt); public static NullabilityInfo GetNullability (PropertyInfo prop) => new NullabilityInfoContext().Create(prop); public static NullabilityInfo GetNullability (ParameterInfo param) => new NullabilityInfoContext().Create(param); + public static NullabilityInfo GetNullability (EventInfo evt) => new NullabilityInfoContext().Create(evt); + public static NullabilityInfo GetNullability (EventInfo evt, ParameterInfo param) + { + if (evt.EventHandlerType!.IsGenericType) + { + var arg = evt.EventHandlerType.GetGenericTypeDefinition() + .GetMethod("Invoke")!.GetParameters()[param.Position].ParameterType; + if (arg.IsGenericParameter) + return GetNullability(evt).GenericTypeArguments[arg.GenericParameterPosition]; + } + return GetNullability(param); + } public static bool IsNullable (Type type) => IsNullable(type, out _); public static bool IsNullable (Type type, NullabilityInfo? info) => IsNullable(type, info, out _); diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index 01fa504b..f5848dc2 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -5,50 +5,25 @@ namespace Bootsharp.Publish; internal sealed class MemberInspector (Preferences prefs, TypeInspector types, SerializedInspector serde, InstancedInspector instanced) { - public EventMeta Inspect (EventInfo evt, InteropKind ik, InstancedMeta? host) - { - return new(evt) { - Interop = ik, - Space = BuildSpace(evt.DeclaringType!, host), - JSSpace = BuildJSSpace(evt.DeclaringType!), - Name = evt.Name, - JSName = WithPrefs(prefs.Function, evt.Name, ToFirstLower(evt.Name)), - Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() - .Select((p, i) => CreateArg(p, GetArgNullability(p, i), ik)).ToArray() - }; - - NullabilityInfo GetArgNullability (ParameterInfo param, int idx) - { - if (evt.EventHandlerType!.IsGenericType) - { - var genType = evt.EventHandlerType.GetGenericTypeDefinition() - .GetMethod("Invoke")!.GetParameters()[idx].ParameterType; - if (genType.IsGenericParameter) - return GetNullability(evt).GenericTypeArguments[genType.GenericParameterPosition]; - } - return GetNullability(param); - } - } - - public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik, InstancedMeta? host) - { - return new PropertyMeta(prop) { - Interop = ik, - Space = BuildSpace(prop.DeclaringType!, host), - JSSpace = BuildJSSpace(prop.DeclaringType!), - Name = prop.Name, - JSName = ToFirstLower(prop.Name), - GetValue = CreateValue(prop.GetMethod, ik), - SetValue = CreateValue(prop.SetMethod, ik.Invert()), - }; + public EventMeta Inspect (EventInfo evt, InteropKind ik, InstancedMeta? host) => new(evt) { + Interop = ik, + Space = BuildSpace(evt.DeclaringType!, host), + JSSpace = BuildJSSpace(evt.DeclaringType!), + Name = evt.Name, + JSName = ToFirstLower(evt.Name), + Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() + .Select(p => CreateArg(p, GetNullability(evt, p), ik)).ToArray() + }; - ValueMeta? CreateValue (MethodInfo? method, InteropKind ik) - { - if (method is null) return null; - if (prop.DeclaringType!.IsInterface && !method.IsAbstract) return null; - return this.CreateValue(prop.PropertyType, GetNullability(prop), ik); - } - } + public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik, InstancedMeta? host) => new(prop) { + Interop = ik, + Space = BuildSpace(prop.DeclaringType!, host), + JSSpace = BuildJSSpace(prop.DeclaringType!), + Name = prop.Name, + JSName = ToFirstLower(prop.Name), + GetValue = prop.GetMethod != null ? CreateValue(prop.PropertyType, GetNullability(prop), ik) : null, + SetValue = prop.SetMethod != null ? CreateValue(prop.PropertyType, GetNullability(prop), ik.Invert()) : null + }; public MethodMeta Inspect (MethodInfo method, InteropKind ik, InstancedMeta? host) => new(method) { Interop = ik, diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs index 75b4e76c..ed02302a 100644 --- a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -1,7 +1,7 @@ namespace Bootsharp.Publish; /// -/// Generates interop wrappers for imported instanced interfaces. +/// Generates interop wrappers for imported instances. /// internal sealed class InstanceGenerator { diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index be850a10..344cc734 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -40,6 +40,7 @@ internal static unsafe void Initialize () .Select(EmitMethodAssignment) ], 2)}} } + {{Fmt(spec.Static.SelectMany(m => EmitMember(m, null, null)))}} {{Fmt(spec.Modules.SelectMany(md => md.Members.SelectMany(m => EmitMember(m, null, md))))}} {{Fmt(spec.Instanced.SelectMany(it => it.Members.SelectMany(m => EmitMember(m, it, null))))}} diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs index f99a4c9e..207f71fe 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -1,7 +1,7 @@ namespace Bootsharp.Publish; /// -/// Generates implementations for interop modules and wrappers for instanced interfaces. +/// Generates implementations for interop modules. /// internal sealed class ModuleGenerator { diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index 21967646..a32df7f6 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -31,13 +31,7 @@ public string BuildArg (ParameterInfo param) public string BuildArg (EventInfo evt, ParameterInfo param) { - var nul = GetNullability(param); - if (evt.EventHandlerType!.IsGenericType) - { - var arg = evt.EventHandlerType.GetGenericTypeDefinition() - .GetMethod("Invoke")!.GetParameters()[param.Position].ParameterType; - if (arg.IsGenericParameter) nul = GetNullability(evt).GenericTypeArguments[arg.GenericParameterPosition]; - } + var nul = GetNullability(evt, param); var post = IsNullable(param.ParameterType, nul) ? " | undefined" : ""; return Build(param.ParameterType, nul) + post; } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index d354997d..79f98c76 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.161 + 0.8.0-alpha.164 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From caf878497d74ede25c78de5c0a7428fdec845680 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 5 May 2026 00:34:46 +0300 Subject: [PATCH 07/20] iter --- src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs | 6 ++++++ .../Common/SolutionInspector/MemberInspector.cs | 8 ++++---- .../Common/SolutionInspector/SerializedInspector.cs | 2 +- .../Pack/DeclarationGenerator/TypeDeclarationGenerator.cs | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 956a3852..12071116 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -94,6 +94,12 @@ public static string BuildJSSpace (Type type, Preferences prefs) return WithPrefs(prefs.Space, space, space); } + public static string BuildJSName (string name) + { + name = ToFirstLower(name); + return name == "function" ? "fn" : name; + } + public static string PrependIdArg (string args) { if (string.IsNullOrEmpty(args)) return "_id"; diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index f5848dc2..9467b63b 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -10,7 +10,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, Space = BuildSpace(evt.DeclaringType!, host), JSSpace = BuildJSSpace(evt.DeclaringType!), Name = evt.Name, - JSName = ToFirstLower(evt.Name), + JSName = BuildJSName(evt.Name), Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() .Select(p => CreateArg(p, GetNullability(evt, p), ik)).ToArray() }; @@ -20,7 +20,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, Space = BuildSpace(prop.DeclaringType!, host), JSSpace = BuildJSSpace(prop.DeclaringType!), Name = prop.Name, - JSName = ToFirstLower(prop.Name), + JSName = BuildJSName(prop.Name), GetValue = prop.GetMethod != null ? CreateValue(prop.PropertyType, GetNullability(prop), ik) : null, SetValue = prop.SetMethod != null ? CreateValue(prop.PropertyType, GetNullability(prop), ik.Invert()) : null }; @@ -30,7 +30,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, Space = BuildSpace(method.DeclaringType!, host), JSSpace = BuildJSSpace(method.DeclaringType!), Name = method.Name, - JSName = WithPrefs(prefs.Function, method.Name, ToFirstLower(method.Name)), + JSName = WithPrefs(prefs.Function, method.Name, BuildJSName(method.Name)), Arguments = method.GetParameters().Select(p => CreateArg(p, GetNullability(p), ik.Invert())).ToArray(), Return = CreateValue(method.ReturnParameter.ParameterType, GetNullability(method.ReturnParameter), ik), Void = IsVoid(method.ReturnParameter.ParameterType), @@ -39,7 +39,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { Name = param.Name!, - JSName = param.Name == "function" ? "fn" : param.Name!, + JSName = BuildJSName(param.Name!), Value = CreateValue(param.ParameterType, nil, ik) }; diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs index a96b3abc..16ffc43f 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -93,7 +93,7 @@ private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) var canSetField = !canInit && !canSet && IsAutoProperty(prop) && getter.IsPublic; return new(value.Type) { Name = prop.Name, - JSName = ToFirstLower(prop.Name), + JSName = BuildJSName(prop.Name), OmitWhenNull = !prop.PropertyType.IsValueType || IsNullable(prop.PropertyType), Required = prop.CustomAttributes .Any(a => a.AttributeType.FullName == typeof(RequiredMemberAttribute).FullName), diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 691753f1..5c477c75 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -92,7 +92,7 @@ private void DeclareSerialized () void AppendProperty (PropertyInfo prop) { bld.Append(docs.BuildProperty(prop, indent + 1)); - AppendLine(ToFirstLower(prop.Name), indent + 1); + AppendLine(BuildJSName(prop.Name), indent + 1); bld.Append(ts.BuildProperty(prop)); bld.Append(';'); } From d1ff3735f4b45f4f9c02848203ce24b67bdc57cf Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 5 May 2026 17:00:26 +0300 Subject: [PATCH 08/20] etc --- .../Bootsharp.Publish/Common/Meta/ValueMeta.cs | 2 +- .../SolutionInspector/MemberInspector.cs | 2 +- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index d13b91b3..cf8e74c4 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs @@ -24,7 +24,7 @@ internal sealed record ValueMeta /// /// Nullability context of the value. /// - public required NullabilityInfo Nullability { get; init; } + public required NullabilityInfo Nullity { get; init; } /// /// Serialization info when , null otherwise. /// diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index 9467b63b..258c0035 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -47,7 +47,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, Type = types.Inspect(type), TypeSyntax = BuildSyntax(type, nil), Nullable = IsNullable(type, nil), - Nullability = nil, + Nullity = nil, Serialized = serde.Inspect(type), Instanced = instanced.Inspect(type, ik, this) }; diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index a32df7f6..5102dc86 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -4,7 +4,7 @@ namespace Bootsharp.Publish; internal sealed class TypeSyntaxBuilder (Preferences prefs) { - private NullabilityInfo? nullability; + private NullabilityInfo? nullity; public string BuildName (Type type) { @@ -61,11 +61,11 @@ public string BuildVariable (PropertyInfo prop) return Build(prop.PropertyType, nul) + post; } - private string Build (Type type, NullabilityInfo? nullability) + private string Build (Type type, NullabilityInfo? nullity) { - this.nullability = nullability; + this.nullity = nullity; // nullability of topmost declarations is handled downstream (?/undefined/null) - if (IsNullable(type, nullability, out var value)) type = value; + if (IsNullable(type, nullity, out var value)) type = value; return WithPrefs(prefs.Type, type.FullName!, Build(type)); } @@ -147,14 +147,14 @@ TypeCode.Byte or TypeCode.SByte or TypeCode.UInt16 or TypeCode.UInt32 or TypeCod private bool EnterNullability (int idx = 0) { - if (nullability == null) return false; - if (nullability.GenericTypeArguments.Length > idx) nullability = nullability.GenericTypeArguments[idx]; - else if (nullability.ElementType != null) nullability = nullability.ElementType; + if (nullity == null) return false; + if (nullity.GenericTypeArguments.Length > idx) nullity = nullity.GenericTypeArguments[idx]; + else if (nullity.ElementType != null) nullity = nullity.ElementType; else { - nullability = null; + nullity = null; return false; } - return nullability.ReadState == NullabilityState.Nullable; + return nullity.ReadState == NullabilityState.Nullable; } } From 10639c2540dc983e029c45d34bb267986387f545 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 5 May 2026 17:13:51 +0300 Subject: [PATCH 09/20] iter --- .../Common/Meta/InstancedMeta.cs | 15 +++----- .../Common/Meta/SerializedMeta.cs | 34 +++++++------------ .../Bootsharp.Publish/Common/Meta/TypeMeta.cs | 5 +-- .../SolutionInspector/InspectionReporter.cs | 12 +++---- .../SolutionInspector/InstancedInspector.cs | 7 ++-- .../SolutionInspector/SerializedInspector.cs | 2 +- .../SolutionInspector/SolutionInspection.cs | 5 +-- .../SolutionInspector/SolutionInspector.cs | 7 ++-- .../Emit/InstanceGenerator.cs | 6 ++-- .../Emit/InteropGenerator.cs | 12 +++---- .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 14 ++++---- .../Emit/SerializerGenerator.cs | 14 ++++---- .../Pack/BindingGenerator/BindingGenerator.cs | 6 ++-- .../BindingSerializerGenerator.cs | 6 ++-- .../TypeDeclarationGenerator.cs | 4 +-- 15 files changed, 65 insertions(+), 84 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs index 19aa3031..7f8fd44e 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs @@ -1,22 +1,15 @@ namespace Bootsharp.Publish; /// -/// Interface supplied by user under either -/// or representing static interop API, or in -/// an interop method, representing instanced interop API. +/// Describes a mutable CLR type whose instances are passed by reference when crossing the interop boundary. /// -internal sealed record InstancedMeta +internal sealed record InstancedMeta (Type Clr) : TypeMeta(Clr) { /// - /// Whether the interface represents C# API consumed in - /// JavaScript (export) or vice versa (import). + /// Whether the type's instances are exported from C# or imported from JavaScript. /// public required InteropKind Interop { get; init; } /// - /// Type info of the instance. - /// - public required TypeMeta Type { get; init; } - /// /// C# namespace of the generated interop class implementation. /// public required string Namespace { get; init; } @@ -33,7 +26,7 @@ internal sealed record InstancedMeta /// public required string JSName { get; init; } /// - /// Members declared on the interface, representing the interop API. + /// Members declared on the instance. /// public required IReadOnlyCollection Members { get; init; } } diff --git a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs index 1962c337..ca408cb3 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs @@ -1,71 +1,63 @@ namespace Bootsharp.Publish; /// -/// Describes a CLR type that requires serialization to cross the interop boundary. +/// Describes an immutable CLR type that is serialized and copied by value when crossing the interop boundary. /// -internal abstract record SerializedMeta (Type Type) +internal abstract record SerializedMeta (Type Clr) : TypeMeta(Clr) { - /// - /// The serialized CLR type. - /// - public Type Type { get; } = Type; - /// - /// Fully qualified C# syntax of the serialized type. - /// - public string Syntax { get; } = BuildSyntax(Type); /// /// The identifier of the serializer factory associated with the type. /// - public string Id { get; } = BuildSerializedId(Type); + public string Id { get; } = BuildSerializedId(Clr); } /// /// Describes a serialized primitive (string, int, bool, etc). /// -internal sealed record SerializedPrimitiveMeta (Type Type) : SerializedMeta(Type); +internal sealed record SerializedPrimitiveMeta (Type Clr) : SerializedMeta(Clr); /// /// Describes a serialized . /// -internal sealed record SerializedEnumMeta (Type Type) : SerializedMeta(Type); +internal sealed record SerializedEnumMeta (Type Clr) : SerializedMeta(Clr); /// /// Describes a serialized . /// /// Describes a serialized . -internal sealed record SerializedNullableMeta (Type Type, SerializedMeta Value) : SerializedMeta(Type); +internal sealed record SerializedNullableMeta (Type Clr, SerializedMeta Value) : SerializedMeta(Clr); /// /// Describes a serialized . /// /// The array element. -internal sealed record SerializedArrayMeta (Type Type, SerializedMeta Element) : SerializedMeta(Type); +internal sealed record SerializedArrayMeta (Type Clr, SerializedMeta Element) : SerializedMeta(Clr); /// /// Describes a serialized linear collection type, such as generic lists and single-argument generic collections. /// /// The collection element. -internal sealed record SerializedListMeta (Type Type, SerializedMeta Element) : SerializedMeta(Type); +internal sealed record SerializedListMeta (Type Clr, SerializedMeta Element) : SerializedMeta(Clr); /// /// Describes a serialized generic key-value type, such as generic dictionaries. /// /// The dictionary key. /// The dictionary value. -internal sealed record SerializedDictionaryMeta (Type Type, - SerializedMeta Key, SerializedMeta Value) : SerializedMeta(Type); +internal sealed record SerializedDictionaryMeta (Type Clr, + SerializedMeta Key, SerializedMeta Value) : SerializedMeta(Clr); /// /// Describes a serialized user-defined object, such as a record or a struct. /// /// The properties of the object, pre-ordered for serialization. -internal sealed record SerializedObjectMeta (Type Type, - IReadOnlyList Properties) : SerializedMeta(Type); +internal sealed record SerializedObjectMeta (Type Clr, + IReadOnlyList Properties) : SerializedMeta(Clr); /// /// Describes a serializable property of a . /// -internal sealed record SerializedPropertyMeta (Type Type) : SerializedMeta(Type) +internal sealed record SerializedPropertyMeta (Type Clr) : SerializedMeta(Clr) { /// /// The name of the property. diff --git a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs index fc38b611..5322a3f1 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs @@ -1,9 +1,10 @@ namespace Bootsharp.Publish; /// -/// Describes a CLR type that crosses the interop boundary. +/// Describes a CLR type that either crosses the interop boundary directly, or is referenced by such a type. +/// Can be either or . /// -internal sealed record TypeMeta (Type Clr) +internal record TypeMeta (Type Clr) { /// /// The described CLR type. diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs index 199f37c4..d0161577 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs @@ -18,9 +18,10 @@ public void Report (SolutionInspection spec) private HashSet GetDiscoveredAssemblies (SolutionInspection spec) { - return spec.Static.Select(GetAssemblyName) - .Concat(spec.Modules.SelectMany(i => i.Members.Select(GetAssemblyName))) - .Concat(spec.Instanced.SelectMany(i => i.Members.Select(GetAssemblyName))) + return spec.Static + .Concat(spec.Modules.SelectMany(i => i.Members)) + .Concat(spec.Instanced.SelectMany(i => i.Members)) + .Select(m => m.Info.DeclaringType!.Assembly.GetName().Name!) .ToHashSet(); } @@ -32,9 +33,4 @@ private HashSet GetDiscoveredMembers (SolutionInspection spec) .Select(m => m.ToString()) .ToHashSet(); } - - private string GetAssemblyName (MemberMeta member) - { - return member.Info.DeclaringType!.Assembly.GetName().Name!; - } } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs index 1b00059e..4db715c9 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs @@ -2,7 +2,7 @@ namespace Bootsharp.Publish; -internal sealed class InstancedInspector (TypeInspector types) +internal sealed class InstancedInspector { private readonly Dictionary byType = []; @@ -23,9 +23,8 @@ public IReadOnlyCollection Collect () return byType.Values.ToArray(); } - private InstancedMeta InspectType (Type type, InteropKind ik) => new() { + private InstancedMeta InspectType (Type type, InteropKind ik) => new(type) { Interop = ik, - Type = types.Inspect(type), Namespace = BuildInstanceSpace(type, ik), Name = BuildInstanceName(type), JSName = BuildInstanceJSName(type), @@ -35,7 +34,7 @@ public IReadOnlyCollection Collect () private InstancedMeta CollectMembers (InstancedMeta it, MemberInspector members) { var ik = it.Interop; - var type = it.Type.Clr; + var type = it.Clr; var cl = (List)it.Members; cl.AddRange(type.GetEvents().Select(m => members.Inspect(m, ik, it))); cl.AddRange(type.GetProperties().Where(ShouldInspectProperty).Select(m => members.Inspect(m, ik, it))); diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs index 16ffc43f..716b1650 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -91,7 +91,7 @@ private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) var canSet = setter != null && setter.IsPublic && !initOnly; var canInit = setter != null && setter.IsPublic && initOnly; var canSetField = !canInit && !canSet && IsAutoProperty(prop) && getter.IsPublic; - return new(value.Type) { + return new(value.Clr) { Name = prop.Name, JSName = BuildJSName(prop.Name), OmitWhenNull = !prop.PropertyType.IsValueType || IsNullable(prop.PropertyType), diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index b7f922da..17a7857e 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -24,7 +24,8 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable /// public required IReadOnlyCollection Modules { get; init; } /// - /// All the types that either directly cross the interop boundary or are referenced by such types. + /// All the types that either directly cross the interop boundary via + /// or members, or types that are referenced by such types. /// public required IReadOnlyCollection Types { get; init; } /// @@ -32,7 +33,7 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable /// public required IReadOnlyCollection Serialized { get; init; } /// - /// All the mutable types that are passed by instance reference when crossing the interop boundary. + /// All the mutable types whose instances are passed by reference when crossing the interop boundary. /// public required IReadOnlyCollection Instanced { get; init; } /// diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs index 9c857c55..405eccda 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs @@ -11,12 +11,11 @@ internal sealed class SolutionInspector private readonly List warnings = []; private readonly TypeInspector types = new(); private readonly SerializedInspector serde = new(); - private readonly InstancedInspector instanced; + private readonly InstancedInspector instanced = new(); private readonly MemberInspector members; public SolutionInspector (Preferences prefs) { - instanced = new(types); members = new(prefs, types, serde, instanced); } @@ -53,7 +52,7 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { Static = statics.ToArray(), Modules = modules.ToArray(), - Types = types.Collect().Where(t => !modules.Any(m => m.Type == t)).ToArray(), + Types = types.Collect().Where(t => !modules.Any(m => m == t)).ToArray(), Instanced = instanced.Collect().Except(modules).ToArray(), Serialized = serde.Collect(), Documentation = docs.ToArray(), @@ -90,7 +89,7 @@ private void InspectModules (CustomAttributeData attr) if (ResolveInterop(attr) is not { } interop) return; foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) if (instanced.Inspect((Type)arg.Value!, interop, members) is { } it) - if (interop == InteropKind.Export || it.Type.Clr.IsInterface) + if (interop == InteropKind.Export || it.Clr.IsInterface) modules.Add(it); } diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs index ed02302a..7c44d567 100644 --- a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -21,7 +21,7 @@ private string EmitWrapper (InstancedMeta it) => $$""" namespace {{(this.it = it).Namespace}} { - public class {{it.Name}} (global::System.Int32 id) : {{it.Type.Syntax}} + public class {{it.Name}} (global::System.Int32 id) : {{it.Syntax}} { internal readonly global::System.Int32 _id = id; @@ -61,7 +61,7 @@ private string EmitPropertyImport (PropertyMeta prop) var setArgs = PrependIdArg("value"); return $$""" - {{type}} {{it.Type.Syntax}}.{{prop.Name}} + {{type}} {{it.Syntax}}.{{prop.Name}} { {{Fmt( prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, @@ -76,7 +76,7 @@ private string EmitMethodImport (MethodMeta method) var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = PrependIdArg(string.Join(", ", method.Arguments.Select(a => a.Name))); var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Return.TypeSyntax} {it.Type.Syntax}.{method.Name} ({args}) => " + + return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 344cc734..b5beb293 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -95,7 +95,7 @@ private IEnumerable EmitEventImport (EventMeta evt) var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; var invName = isIt ? $"Instances.Import(_id, static id => new global::{it.FullName}(id)).Invoke{evt.Name}" - : isMd ? $"((global::{evt.Space})Modules.Imports[typeof({md.Type.Syntax})].Instance).Invoke{evt.Name}" + : isMd ? $"((global::{evt.Space})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; var invArgs = string.Join(", ", evt.Arguments.Select(Deserialize)); yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; @@ -109,7 +109,7 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; var body = Serialize(prop.GetValue, isIt - ? $"Instances.Exported<{it.Type.Syntax}>(_id).{prop.Name}" + ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name}" : $"global::{prop.Space}.GetProperty{prop.Name}()"); yield return $"{attr}internal static {BuildValueSyntax(prop.GetValue)} {name} ({args}) => {body};"; } @@ -120,7 +120,7 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; var value = Deserialize(prop.SetValue, "value"); var body = isIt - ? $"Instances.Exported<{it.Type.Syntax}>(_id).{prop.Name} = {value}" + ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; yield return $"[JSExport] internal static void {name} ({args}) => {body};"; } @@ -168,7 +168,7 @@ private IEnumerable EmitMethodExport (MethodMeta method) if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); var invName = isIt - ? $"Instances.Exported<{it.Type.Syntax}>(_id).{method.Name}" + ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" : $"global::{method.Space}.{method.Name}"; var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; @@ -201,7 +201,7 @@ private IEnumerable EmitMethodImport (MethodMeta method) var events = it.Members.OfType().ToArray(); return $$""" - private static int Register ({{it.Type.Syntax}} instance) => Instances.Export(instance, static (_id, instance) => { + private static int Register ({{it.Syntax}} instance) => Instances.Export(instance, static (_id, instance) => { {{Fmt(events.Select(e => $"instance.{e.Name} += Handle{e.Name};"))}} return () => { {{Fmt(events.Select(e => $"instance.{e.Name} -= Handle{e.Name};"), 2)}} @@ -236,7 +236,7 @@ private string Deserialize (ValueMeta value, string exp) { if (value.Instanced is { } it) { - if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Type.Syntax}>({exp})"; + if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Syntax}>({exp})"; return $"Instances.Import({exp}, static id => new global::{it.FullName}(id))"; } if (Serialized(value, out var id)) return $"Serializer.Deserialize({exp}, {id})"; diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs index 207f71fe..b7224b6d 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -30,11 +30,11 @@ internal static void RegisterModules () private string EmitRegistration (InstancedMeta it) { var type = it.Interop == InteropKind.Import - ? $"typeof({it.Type.Syntax})" + ? $"typeof({it.Syntax})" : $"typeof({it.FullName})"; var factory = it.Interop == InteropKind.Import ? $"new ImportModule(new {it.FullName}())" - : $"new ExportModule(typeof({it.Type.Syntax}), handler => new {it.FullName}(({it.Type.Syntax})handler))"; + : $"new ExportModule(typeof({it.Syntax}), handler => new {it.FullName}(({it.Syntax})handler))"; return $"Modules.Register({type}, {factory});"; } @@ -51,9 +51,9 @@ namespace {{it.Namespace}} { public class {{it.Name}} { - private static {{it.Type.Syntax}} handler = null!; + private static {{it.Syntax}} handler = null!; - public {{it.Name}} ({{it.Type.Syntax}} handler) + public {{it.Name}} ({{it.Syntax}} handler) { {{Fmt([ $"{it.Name}.handler = handler;", @@ -70,7 +70,7 @@ private string EmitModuleImport () => $$""" namespace {{it.Namespace}} { - public class {{it.Name}} : {{it.Type.Syntax}} + public class {{it.Name}} : {{it.Syntax}} { {{Fmt(it.Members.Select(EmitMemberImport), 2)}} } @@ -121,7 +121,7 @@ private string EmitPropertyImport (PropertyMeta prop) var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; return $$""" - {{type}} {{it.Type.Syntax}}.{{prop.Name}} + {{type}} {{it.Syntax}}.{{prop.Name}} { {{Fmt( prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, @@ -144,7 +144,7 @@ private string EmitMethodImport (MethodMeta method) var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); var name = $"{it.FullName.Replace('.', '_')}_{method.Name}"; - return $"{method.Return.TypeSyntax} {it.Type.Syntax}.{method.Name} ({args}) => " + + return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index 9d9b3b90..516cb6ec 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -26,10 +26,10 @@ private string EmitFactory (SerializedMeta meta) SerializedEnumMeta => $"Serializer.Enum<{meta.Syntax}>()", SerializedNullableMeta nullable => $"Serializer.Nullable({nullable.Value.Id})", SerializedArrayMeta arr => $"Serializer.Array({arr.Element.Id})", - SerializedListMeta list => $"Serializer.{TrimGeneric(list.Type.Name)}({list.Element.Id})", - SerializedDictionaryMeta dic => $"Serializer.{TrimGeneric(dic.Type.Name)}({dic.Key.Id}, {dic.Value.Id})", + SerializedListMeta list => $"Serializer.{TrimGeneric(list.Clr.Name)}({list.Element.Id})", + SerializedDictionaryMeta dic => $"Serializer.{TrimGeneric(dic.Clr.Name)}({dic.Key.Id}, {dic.Value.Id})", SerializedObjectMeta => $"new(Write_{meta.Id}, Read_{meta.Id})", - _ => ResolvePrimitive(meta.Type) + _ => ResolvePrimitive(meta.Clr) }};"; static string ResolvePrimitive (Type type) @@ -61,7 +61,7 @@ private IEnumerable EmitHelpers (SerializedMeta meta) private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) { - if (!obj.Type.IsValueType) + if (!obj.Clr.IsValueType) { yield return "writer.WriteBool(value is not null);"; yield return "if (value is null) return;"; @@ -77,7 +77,7 @@ private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) private IEnumerable EmitObjectRead (SerializedObjectMeta obj) { - if (!obj.Type.IsValueType) yield return "if (!reader.ReadBool()) return null!;"; + if (!obj.Clr.IsValueType) yield return "if (!reader.ReadBool()) return null!;"; foreach (var p in obj.Properties) { var var = MangleLocal(p.Name); @@ -106,7 +106,7 @@ private string EmitObjectConstruction (SerializedObjectMeta obj) private static string EmitFieldAccessor (SerializedObjectMeta obj, SerializedPropertyMeta prop) { - var value = obj.Type.IsValueType ? $"ref {obj.Syntax} value" : $"{obj.Syntax} value"; + var value = obj.Clr.IsValueType ? $"ref {obj.Syntax} value" : $"{obj.Syntax} value"; return $""" [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "<{prop.Name}>k__BackingField")] private static extern ref {prop.Syntax} {prop.FieldAccessorName} ({value}); @@ -115,7 +115,7 @@ private static string EmitFieldAccessor (SerializedObjectMeta obj, SerializedPro private static string EmitFieldAssign (SerializedObjectMeta obj, SerializedPropertyMeta prop) { - var value = obj.Type.IsValueType ? "ref _value_" : "_value_"; + var value = obj.Clr.IsValueType ? "ref _value_" : "_value_"; return $"{prop.FieldAccessorName}({value}) = {MangleLocal(prop.Name)};"; } diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index d60e7b6f..2cd3b752 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -25,8 +25,8 @@ public string Generate (SolutionInspection spec) .Select(m => new Binding(m, null, null, m.JSSpace)))) .Concat(spec.Instanced.SelectMany(it => it.Members .Select(m => new Binding(m, null, it, m.JSSpace)))) - .Concat(spec.Serialized.Where(t => t.Type.IsEnum) - .Select(t => new Binding(null, t.Type, null, BuildJSSpace(t.Type, prefs)))) + .Concat(spec.Serialized.Where(t => t.Clr.IsEnum) + .Select(t => new Binding(null, t.Clr, null, BuildJSSpace(t.Clr, prefs)))) .OrderBy(m => m.Namespace).ToArray(); if (bindings.Length == 0) return ""; @@ -342,7 +342,7 @@ private string RegisterInstance (InstancedMeta it, string exp) private static string BuildRegistrarName (InstancedMeta it) { - return $"register_{it.Type.Clr.FullName!.Replace('.', '_').Replace('+', '_')}"; + return $"register_{it.Clr.FullName!.Replace('.', '_').Replace('+', '_')}"; } private bool ShouldWait (MethodMeta method) diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs index e486ce6f..93bc83ce 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs @@ -21,7 +21,7 @@ private string EmitFactory (SerializedMeta meta) SerializedListMeta list => $"types.List({list.Element.Id})", SerializedDictionaryMeta dic => $"types.Dictionary({dic.Key.Id}, {dic.Value.Id})", SerializedObjectMeta => $"binary(write_{meta.Id}, read_{meta.Id})", - _ => ResolvePrimitive(meta.Type) + _ => ResolvePrimitive(meta.Clr) }};"; static string ResolvePrimitive (Type type) @@ -49,7 +49,7 @@ private IEnumerable EmitHelpers (SerializedMeta meta) private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) { - if (!obj.Type.IsValueType) + if (!obj.Clr.IsValueType) { yield return "writer.writeBool(value != null);"; yield return "if (value == null) return;"; @@ -65,7 +65,7 @@ private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) private IEnumerable EmitObjectRead (SerializedObjectMeta obj) { - if (!obj.Type.IsValueType) yield return "if (!reader.readBool()) return null;"; + if (!obj.Clr.IsValueType) yield return "if (!reader.readBool()) return null;"; yield return "const value = {};"; foreach (var p in obj.Properties) if (p.OmitWhenNull) yield return $"if (reader.readBool()) value.{p.JSName} = {p.Id}.read(reader);"; diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 5c477c75..a02c7878 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -21,7 +21,7 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) public string Generate (SolutionInspection spec) { docs = new(spec.Documentation); - itByType = spec.Instanced.ToDictionary(it => it.Type.Clr); + itByType = spec.Instanced.ToDictionary(it => it.Clr); types = spec.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); @@ -104,7 +104,7 @@ private void DeclareInstanced (InstancedMeta it) AppendLine($"export interface {ts.BuildName(type)}", indent); AppendExtensions(); bld.Append(" {"); - foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Type.Clr)) + foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Clr)) if (member is EventMeta evt) AppendEvent(evt); else if (member is PropertyMeta prop) AppendProperty(prop); else AppendMethod((MethodMeta)member); From 80aa3a1e4666f93809908de98199bd3192fce05b Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Tue, 5 May 2026 20:36:34 +0300 Subject: [PATCH 10/20] iter --- .../Emit/InteropTest.cs | 6 +- .../Emit/SerializerTest.cs | 4 +- .../Pack/BindingTest.cs | 6 +- .../Pack/DeclarationTest.cs | 6 +- .../Common/Meta/SerializedMeta.cs | 6 ++ .../Bootsharp.Publish/Common/Meta/TypeMeta.cs | 1 - .../Common/Meta/ValueMeta.cs | 8 +-- .../SolutionInspector/InstancedInspector.cs | 16 +++-- .../SolutionInspector/MemberInspector.cs | 9 +-- .../SolutionInspector/SerializedInspector.cs | 1 + .../SolutionInspector/SolutionInspector.cs | 31 ++++++---- .../Common/SolutionInspector/TypeInspector.cs | 26 +++++--- .../TypeDeclarationGenerator.cs | 62 +++++++++---------- 13 files changed, 98 insertions(+), 84 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index 0227baa2..d73eef8c 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -147,7 +147,7 @@ public interface IImported { Info Fun (string str, Info info); } } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ @@ -212,7 +212,7 @@ public interface IImported } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -280,7 +280,7 @@ internal static unsafe void Initialize () } [Fact] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 5d58cc86..3303da12 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -445,7 +445,7 @@ public class Class } [Fact] - public void SerializesTypesFromInstancedInterfaces () + public void SerializesTypesFromInstanced () { AddAssembly(With( """ @@ -467,7 +467,7 @@ public class Class } [Fact] - public void DoesntSerializeInstancedInterfacesThemselves () + public void DoesntSerializeInstancedThemselves () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 4447fe47..f8f401cc 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -623,7 +623,7 @@ public interface IImported { Info Fun (string str, Info info); } } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ @@ -707,7 +707,7 @@ public interface IImported } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -796,7 +796,7 @@ public interface IImported { event Action Evt; } } [Fact] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 8b4f336a..611ad6d6 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -826,7 +826,7 @@ export namespace Imported { } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ @@ -916,7 +916,7 @@ export namespace ImportedStatic { } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -1003,7 +1003,7 @@ export namespace Imported { } [Fact] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ diff --git a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs index ca408cb3..ef2f441b 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs @@ -1,3 +1,5 @@ +using System.Reflection; + namespace Bootsharp.Publish; /// @@ -59,6 +61,10 @@ internal sealed record SerializedObjectMeta (Type Clr, /// internal sealed record SerializedPropertyMeta (Type Clr) : SerializedMeta(Clr) { + /// + /// The reflected info of the property. + /// + public required PropertyInfo Info { get; init; } /// /// The name of the property. /// diff --git a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs index 5322a3f1..fb94a3be 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs @@ -2,7 +2,6 @@ namespace Bootsharp.Publish; /// /// Describes a CLR type that either crosses the interop boundary directly, or is referenced by such a type. -/// Can be either or . /// internal record TypeMeta (Type Clr) { diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index cf8e74c4..0017bdcd 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs @@ -28,18 +28,18 @@ internal sealed record ValueMeta /// /// Serialization info when , null otherwise. /// - public required SerializedMeta? Serialized { get; init; } + public SerializedMeta? Serialized => Type as SerializedMeta; /// /// Instance info when , null otherwise. /// - public required InstancedMeta? Instanced { get; init; } + public InstancedMeta? Instanced => Type as InstancedMeta; /// - /// Whether the value has to be serialized to cross the interop boundary. + /// Whether the value is serialized and copied when crossing the interop boundary. /// [MemberNotNullWhen(true, nameof(Serialized))] public bool IsSerialized => Serialized != null; /// - /// Whether the value is an interop instance. + /// Whether the value instance is passed by reference when crossing the interop boundary. /// [MemberNotNullWhen(true, nameof(Instanced))] public bool IsInstanced => Instanced != null; diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs index 4db715c9..9d797c0d 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs @@ -2,20 +2,18 @@ namespace Bootsharp.Publish; -internal sealed class InstancedInspector +internal sealed class InstancedInspector (MemberInspector members) { private readonly Dictionary byType = []; - public InstancedMeta? Inspect (Type type, InteropKind ik, MemberInspector members) + public InstancedMeta? Inspect (Type type, InteropKind ik) { if (byType.TryGetValue(type, out var meta)) return meta; - if (IsTaskWithResult(type, out var result)) return Inspect(result, ik, members); - if (IsList(type, out var element)) return Inspect(element, ik, members); - if (IsDictionary(type, out _, out var value)) return Inspect(value, ik, members); + if (IsTaskWithResult(type, out var result)) return Inspect(result, ik); + if (IsList(type, out var element)) return Inspect(element, ik); + if (IsDictionary(type, out _, out var value)) return Inspect(value, ik); if (!IsInstancedType(type)) return null; - if (type.BaseType is { } b && Inspect(b, ik, members) is { } bm) byType[b] = bm; - // TODO: I dont like this crawling shit here, especially the base type. - return CollectMembers(byType[type] = InspectType(type, ik), members); + return CollectMembers(byType[type] = InspectType(type, ik)); } public IReadOnlyCollection Collect () @@ -31,7 +29,7 @@ public IReadOnlyCollection Collect () Members = new List() }; - private InstancedMeta CollectMembers (InstancedMeta it, MemberInspector members) + private InstancedMeta CollectMembers (InstancedMeta it) { var ik = it.Interop; var type = it.Clr; diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index 258c0035..d2458f7d 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -2,8 +2,7 @@ namespace Bootsharp.Publish; -internal sealed class MemberInspector (Preferences prefs, TypeInspector types, - SerializedInspector serde, InstancedInspector instanced) +internal sealed class MemberInspector (Preferences prefs, Func inspect) { public EventMeta Inspect (EventInfo evt, InteropKind ik, InstancedMeta? host) => new(evt) { Interop = ik, @@ -44,12 +43,10 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, }; private ValueMeta CreateValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { - Type = types.Inspect(type), + Type = inspect(type, ik), TypeSyntax = BuildSyntax(type, nil), Nullable = IsNullable(type, nil), - Nullity = nil, - Serialized = serde.Inspect(type), - Instanced = instanced.Inspect(type, ik, this) + Nullity = nil }; private string BuildSpace (Type decl, InstancedMeta? host) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs index 716b1650..d72c63c7 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -92,6 +92,7 @@ private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) var canInit = setter != null && setter.IsPublic && initOnly; var canSetField = !canInit && !canSet && IsAutoProperty(prop) && getter.IsPublic; return new(value.Clr) { + Info = prop, Name = prop.Name, JSName = BuildJSName(prop.Name), OmitWhenNull = !prop.PropertyType.IsValueType || IsNullable(prop.PropertyType), diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs index 405eccda..baa46557 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs @@ -9,14 +9,19 @@ internal sealed class SolutionInspector private readonly List modules = []; private readonly List docs = []; private readonly List warnings = []; - private readonly TypeInspector types = new(); + private readonly TypeInspector types; private readonly SerializedInspector serde = new(); - private readonly InstancedInspector instanced = new(); + private readonly InstancedInspector itd; private readonly MemberInspector members; public SolutionInspector (Preferences prefs) { - members = new(prefs, types, serde, instanced); + types = new(prefs); + members = new(prefs, (type, ik) => { + types.Crawl(type, ik); + return serde.Inspect(type) ?? itd!.Inspect(type, ik) ?? new TypeMeta(type); + }); + itd = new(members); } /// @@ -53,7 +58,7 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception Static = statics.ToArray(), Modules = modules.ToArray(), Types = types.Collect().Where(t => !modules.Any(m => m == t)).ToArray(), - Instanced = instanced.Collect().Except(modules).ToArray(), + Instanced = itd.Collect().Except(modules).ToArray(), Serialized = serde.Collect(), Documentation = docs.ToArray(), Warnings = warnings.ToArray() @@ -77,27 +82,27 @@ private void InspectStatic (Type type) { if (type.Namespace?.StartsWith("Bootsharp.Generated") ?? false) return; foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.Static)) - if (ResolveInterop(evt) is { } interop) - statics.Add(members.Inspect(evt, interop, null)); + if (ResolveInterop(evt) is { } ik) + statics.Add(members.Inspect(evt, ik, null)); foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - if (ResolveInterop(method) is { } interop) - statics.Add(members.Inspect(method, interop, null)); + if (ResolveInterop(method) is { } ik) + statics.Add(members.Inspect(method, ik, null)); } private void InspectModules (CustomAttributeData attr) { - if (ResolveInterop(attr) is not { } interop) return; + if (ResolveInterop(attr) is not { } ik) return; foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) - if (instanced.Inspect((Type)arg.Value!, interop, members) is { } it) - if (interop == InteropKind.Export || it.Clr.IsInterface) + if (itd.Inspect((Type)arg.Value!, ik) is { } it) + if (ik == InteropKind.Export || it.Clr.IsInterface) modules.Add(it); } private InteropKind? ResolveInterop (MemberInfo info) { foreach (var attr in info.CustomAttributes) - if (ResolveInterop(attr) is { } interop) - return interop; + if (ResolveInterop(attr) is { } ik) + return ik; return null; } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs index 17077608..19e5b222 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs @@ -3,21 +3,29 @@ namespace Bootsharp.Publish; internal sealed class TypeInspector { private readonly Dictionary byType = []; + private readonly SerializedInspector serde = new(); + private readonly InstancedInspector itd; + private InteropKind ik; - public TypeMeta Inspect (Type type) + public TypeInspector (Preferences prefs) { - return Crawl(type); + itd = new(new(prefs, Inspect)); + } + + public void Crawl (Type type, InteropKind ik) + { + this.ik = ik; + Crawl(type); } public IReadOnlyCollection Collect () { - return byType.Values.ToArray(); + return byType.Values.Distinct().Where(m => IsUserType(m.Clr)).ToArray(); } - private TypeMeta Crawl (Type type) + private void Crawl (Type type) { - if (byType.TryGetValue(type, out var meta)) return meta; - meta = byType[type] = new(type); + if (!byType.TryAdd(type, Inspect(type, ik))) return; if (IsNullable(type, out var nullValue)) Crawl(nullValue); else if (IsList(type, out var element)) Crawl(element); else if (IsDictionary(type, out var key, out var value)) @@ -30,7 +38,6 @@ private TypeMeta Crawl (Type type) CrawlProperties(type); CrawlBaseType(type); } - return meta; } private void CrawlProperties (Type type) @@ -44,4 +51,9 @@ private void CrawlBaseType (Type type) if (type.BaseType != null) Crawl(type.BaseType); } + + private TypeMeta Inspect (Type type, InteropKind ik) + { + return serde.Inspect(type) ?? itd.Inspect(type, ik) ?? new TypeMeta(type); + } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index a02c7878..57274c95 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -1,4 +1,3 @@ -using System.Reflection; using System.Text; namespace Bootsharp.Publish; @@ -8,21 +7,19 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) private readonly StringBuilder bld = new(); private readonly TypeSyntaxBuilder ts = new(prefs); - private Type type => types[index]; - private Type? prevType => index == 0 ? null : types[index - 1]; - private Type? nextType => index == types.Length - 1 ? null : types[index + 1]; + private TypeMeta type => types[index]; + private TypeMeta? prevType => index == 0 ? null : types[index - 1]; + private TypeMeta? nextType => index == types.Length - 1 ? null : types[index + 1]; private int indent => !string.IsNullOrEmpty(GetNamespace(type)) ? 1 : 0; private DocumentationBuilder docs = null!; - private Dictionary itByType = null!; - private Type[] types = null!; + private TypeMeta[] types = null!; private int index; public string Generate (SolutionInspection spec) { docs = new(spec.Documentation); - itByType = spec.Instanced.ToDictionary(it => it.Clr); - types = spec.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); + types = spec.Types.OrderBy(GetNamespace).ToArray(); for (index = 0; index < types.Length; index++) DeclareType(); return bld.ToString(); @@ -31,9 +28,9 @@ public string Generate (SolutionInspection spec) private void DeclareType () { if (ShouldOpenNamespace()) OpenNamespace(); - if (itByType.TryGetValue(type, out var it)) DeclareInstanced(it); - else if (type.IsEnum) DeclareEnum(); - else DeclareSerialized(); + if (type is InstancedMeta it) DeclareInstanced(it); + else if (type is SerializedEnumMeta enu) DeclareEnum(enu); + else if (type is SerializedObjectMeta obj) DeclareSerialized(obj); if (ShouldCloseNamespace()) CloseNamespace(); } @@ -62,46 +59,45 @@ private void CloseNamespace () AppendLine("}", 0); } - private void DeclareEnum () + private void DeclareEnum (SerializedEnumMeta enu) { - bld.Append(docs.BuildType(type, indent)); - AppendLine($"export enum {type.Name} {{", indent); - var names = Enum.GetNames(type); + bld.Append(docs.BuildType(enu.Clr, indent)); + AppendLine($"export enum {enu.Clr.Name} {{", indent); + var names = Enum.GetNames(enu.Clr); for (int i = 0; i < names.Length; i++) { - bld.Append(docs.BuildProperty(type.GetField(names[i])!, indent + 1)); + bld.Append(docs.BuildProperty(enu.Clr.GetField(names[i])!, indent + 1)); if (i == names.Length - 1) AppendLine(names[i], indent + 1); else AppendLine($"{names[i]},", indent + 1); } AppendLine("}", indent); } - private void DeclareSerialized () + private void DeclareSerialized (SerializedObjectMeta obj) { - bld.Append(docs.BuildType(type, indent)); - AppendLine($"export type {ts.BuildName(type)} = ", indent); - if (type.BaseType is { } baseType && types.Contains(baseType)) + bld.Append(docs.BuildType(obj.Clr, indent)); + AppendLine($"export type {ts.BuildName(obj.Clr)} = ", indent); + if (obj.Clr.BaseType is { } baseType && IsUserType(baseType)) bld.Append(ts.BuildFullName(baseType)).Append(" & "); bld.Append("Readonly<{"); - var flags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance; - foreach (var prop in type.GetProperties(flags)) - if (prop.GetMethod != null && prop.GetIndexParameters().Length == 0) + foreach (var prop in obj.Properties) + if (prop.Info.DeclaringType == obj.Clr) AppendProperty(prop); AppendLine("}>;", indent); - void AppendProperty (PropertyInfo prop) + void AppendProperty (SerializedPropertyMeta prop) { - bld.Append(docs.BuildProperty(prop, indent + 1)); - AppendLine(BuildJSName(prop.Name), indent + 1); - bld.Append(ts.BuildProperty(prop)); + bld.Append(docs.BuildProperty(prop.Info, indent + 1)); + AppendLine(prop.JSName, indent + 1); + bld.Append(ts.BuildProperty(prop.Info)); bld.Append(';'); } } private void DeclareInstanced (InstancedMeta it) { - bld.Append(docs.BuildType(type, indent)); - AppendLine($"export interface {ts.BuildName(type)}", indent); + bld.Append(docs.BuildType(it.Clr, indent)); + AppendLine($"export interface {ts.BuildName(it.Clr)}", indent); AppendExtensions(); bld.Append(" {"); foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Clr)) @@ -112,8 +108,8 @@ private void DeclareInstanced (InstancedMeta it) void AppendExtensions () { - var extTypes = new List(type.GetInterfaces().Where(types.Contains)); - if (type.BaseType is { } baseType && types.Contains(baseType)) + var extTypes = new List(it.Clr.GetInterfaces().Where(IsUserType)); + if (it.Clr.BaseType is { } baseType && IsUserType(baseType)) extTypes.Insert(0, baseType); if (extTypes.Count > 0) bld.Append(" extends ").AppendJoin(", ", extTypes.Select(ts.BuildFullName)); @@ -163,8 +159,8 @@ private void Append (string content, int level) bld.Append(content); } - private string GetNamespace (Type type) + private string GetNamespace (TypeMeta type) { - return BuildJSSpace(type, prefs); + return BuildJSSpace(type.Clr, prefs); } } From 6df988375513ab662f3be3d29ed71f67d7d91c98 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 00:13:30 +0300 Subject: [PATCH 11/20] iter --- .../Common/{SolutionInspector => Inspector}/InspectionReporter.cs | 0 .../Common/{SolutionInspector => Inspector}/InstancedInspector.cs | 0 .../Common/{SolutionInspector => Inspector}/MemberInspector.cs | 0 .../{SolutionInspector => Inspector}/SerializedInspector.cs | 0 .../Common/{SolutionInspector => Inspector}/SolutionInspection.cs | 0 .../Common/{SolutionInspector => Inspector}/SolutionInspector.cs | 0 .../Common/{SolutionInspector => Inspector}/TypeInspector.cs | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/InspectionReporter.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/InstancedInspector.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/MemberInspector.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/SerializedInspector.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/SolutionInspection.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/SolutionInspector.cs (100%) rename src/cs/Bootsharp.Publish/Common/{SolutionInspector => Inspector}/TypeInspector.cs (100%) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/InstancedInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs similarity index 100% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs From e9c8dab9d4c0a05e16179bfcc8994f993b94ecbc Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 15:39:49 +0300 Subject: [PATCH 12/20] remove type inspector --- .../Pack/DeclarationTest.cs | 93 ++++++++++--------- .../Common/Inspector/InstancedInspector.cs | 2 - .../Common/Inspector/SerializedInspector.cs | 10 +- .../Common/Inspector/SolutionInspection.cs | 5 - .../Common/Inspector/SolutionInspector.cs | 11 +-- .../Common/Inspector/TypeInspector.cs | 59 ------------ .../Common/Meta/SerializedMeta.cs | 5 + .../TypeDeclarationGenerator.cs | 74 ++++++++++----- 8 files changed, 115 insertions(+), 144 deletions(-) delete mode 100644 src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 611ad6d6..85f5eb09 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -47,12 +47,12 @@ public void WhenNoNamespaceDeclaresUnderRoot () Execute(); Contains( """ - export type Record = Readonly<{ - }>; export enum Enum { A, B } + export type Record = Readonly<{ + }>; export namespace Class { export function inv(r: Record): Enum; @@ -658,56 +658,65 @@ public void CanCrawlCustomTypes () AddAssembly( With("Space", """ - public struct Struct { public double A { get; set; } } + public class Nya { public bool Mew() => default; } + public struct Struct { public double A { get; set; } public Nya Mew { get; } } public readonly struct ReadonlyStruct { public double A { get; init; } } public readonly record struct ReadonlyRecordStruct(double A); - public record class RecordClassA (double A, ReadonlyRecordStruct Str); - public record class RecordClassB (double B) : RecordClassA(42, new(24)); + public record class RecordClass (ReadonlyRecordStruct Str); + public record class RecordClassA (double A) : RecordClass(new ReadonlyRecordStruct(42)); + public record class RecordClassB (RecordClassA B) : RecordClassA(24); public enum Enum { A, B } public class Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } } public class Bar : Foo { public Dictionary Rc { get; } } - public class Baz { public List Bars { get; } public Enum E { get; } } - public class Class { [Export] public static Dictionary GetBaz () => default; } + public class Baz : Bar { public List Bars { get; } public Enum E { get; } } + public class Key : Baz { } + public class Class { [Export] public static Dictionary GetBaz () => default; } """)); Execute(); + // 'Foo' and 'RecordClass' are not declared, because they don't directly appear on the interop boundary; + // instead, their members are merged into 'Bar' and 'RecordClassA', who directly inherit (extend) them. Contains( """ export namespace Space { - export interface Baz { + export interface Key extends Space.Baz { + } + export interface Bar { + readonly rc: Map; + readonly s: Space.Struct; + readonly rs: Space.ReadonlyStruct; + } + export interface Nya { + mew(): boolean; + } + export interface Baz extends Space.Bar { readonly bars: Array; readonly e: Space.Enum; } - export interface Bar extends Space.Foo { - readonly rc: Map; + export enum Enum { + A, + B } - export type RecordClassB = Space.RecordClassA & Readonly<{ - b: number; - }>; export type ReadonlyRecordStruct = Readonly<{ a: number; }>; + export type ReadonlyStruct = Readonly<{ + a: number; + }>; export type RecordClassA = Readonly<{ a: number; str: Space.ReadonlyRecordStruct; }>; - export type Struct = Readonly<{ - a: number; + export type RecordClassB = Space.RecordClassA & Readonly<{ + b: Space.RecordClassA; }>; - export type ReadonlyStruct = Readonly<{ + export type Struct = Readonly<{ a: number; + mew: Space.Nya; }>; - export interface Foo { - readonly s: Space.Struct; - readonly rs: Space.ReadonlyStruct; - } - export enum Enum { - A, - B - } } export namespace Space.Class { - export function getBaz(): Map; + export function getBaz(): Map; } """); } @@ -847,13 +856,13 @@ public class Class export interface IImported { fun(info: Info, str: string): Info; } - export type Info = Readonly<{ - value: string; - }>; export interface IExported { inv(str: string, info: Info): Info; reset(): void; } + export type Info = Readonly<{ + value: string; + }>; export namespace Class { export function getExported(inst: IImported): Promise; @@ -893,13 +902,13 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export type Info = Readonly<{ - value: string; - }>; export interface IExportedInstanced { } export interface IImportedInstanced { } + export type Info = Readonly<{ + value: string; + }>; export namespace ExportedStatic { export let state: Info; @@ -950,14 +959,14 @@ export interface IImported { readonly imported: IImported; exported: IExported; } - export type Info = Readonly<{ - value: string; - }>; export interface IExported { state: Info; readonly exported: IExported; imported: IImported; } + export type Info = Readonly<{ + value: string; + }>; export namespace Class { export function getExported(inst: IImported): IExported; @@ -985,13 +994,13 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export type Info = Readonly<{ - value: string; - }>; export interface IExportedInstanced { } export interface IImportedInstanced { } + export type Info = Readonly<{ + value: string; + }>; export namespace Exported { export const evt: EventSubscriber<[arg1: string, arg2: Info, arg3: IExportedInstanced]>; @@ -1024,13 +1033,13 @@ public class Class export interface IImported { changed: EventBroadcaster<[arg1: IImported, arg2: Info, arg3: string]>; } - export type Info = Readonly<{ - value: string; - }>; export interface IExported { changed: EventSubscriber<[obj: Info]>; done: EventSubscriber<[]>; } + export type Info = Readonly<{ + value: string; + }>; export namespace Class { export function getExported(inst: IImported): IExported; @@ -1252,10 +1261,10 @@ [Export] public static void Inv (Record r, Generic g) {} Execute(); Contains( """ - export type Foo = Readonly<{ - }>; export type Bar = Readonly<{ }>; + export type Foo = Readonly<{ + }>; export namespace Class { export function inv(r: Foo, g: Bar): void; diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index 9d797c0d..9b9d0e7c 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -10,8 +10,6 @@ internal sealed class InstancedInspector (MemberInspector members) { if (byType.TryGetValue(type, out var meta)) return meta; if (IsTaskWithResult(type, out var result)) return Inspect(result, ik); - if (IsList(type, out var element)) return Inspect(element, ik); - if (IsDictionary(type, out _, out var value)) return Inspect(value, ik); if (!IsInstancedType(type)) return null; return CollectMembers(byType[type] = InspectType(type, ik)); } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs index d72c63c7..92939fd2 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs @@ -5,11 +5,11 @@ namespace Bootsharp.Publish; /// -/// Remember that the serialization is only required for the values that cross the interop boundary +/// Remember that the serialization is only required for the values that directly cross the interop boundary /// and whose types are not natively supported by System.Runtime.InteropServices.JavaScript. /// The types that are referenced by these top-level interop types are crawled by this inspector. /// -internal sealed class SerializedInspector +internal sealed class SerializedInspector (InstancedInspector itd) { private record Discard (Type Type) : SerializedMeta(Type); @@ -24,9 +24,11 @@ private record Discard (Type Type) : SerializedMeta(Type); private readonly Dictionary byId = []; private readonly HashSet cycle = []; + private InteropKind ik; - public SerializedMeta? Inspect (Type type) + public SerializedMeta? Inspect (Type type, InteropKind ik) { + this.ik = ik; return ShouldSerialize(type) ? Build(type) : null; } @@ -40,7 +42,6 @@ private static bool ShouldSerialize (Type type) if (IsVoid(type)) return false; if (IsNullable(type, out var value)) return ShouldSerialize(value); if (IsTaskWithResult(type, out var result)) return ShouldSerialize(result); - if (IsInstancedType(type)) return false; return !native.Contains(type.FullName!); } @@ -57,6 +58,7 @@ private SerializedMeta Build (Type type) type.IsArray ? new SerializedArrayMeta(type, Build(type.GetElementType()!)) : IsList(type, out var element) ? new SerializedListMeta(type, Build(element)) : IsDictionary(type, out var k, out var v) ? new SerializedDictionaryMeta(type, Build(k), Build(v)) : + IsInstancedType(type) ? new SerializedInstanceMeta(itd.Inspect(type, ik)!.Clr) : BuildObject(type); cycle.Remove(type); return meta; diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs index 17a7857e..97f4ffd8 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs @@ -24,11 +24,6 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable /// public required IReadOnlyCollection Modules { get; init; } /// - /// All the types that either directly cross the interop boundary via - /// or members, or types that are referenced by such types. - /// - public required IReadOnlyCollection Types { get; init; } - /// /// All the immutable types that are serialized and copied by value when crossing the interop boundary. /// public required IReadOnlyCollection Serialized { get; init; } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs index baa46557..cc5df2ed 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs @@ -9,19 +9,15 @@ internal sealed class SolutionInspector private readonly List modules = []; private readonly List docs = []; private readonly List warnings = []; - private readonly TypeInspector types; - private readonly SerializedInspector serde = new(); + private readonly SerializedInspector serde; private readonly InstancedInspector itd; private readonly MemberInspector members; public SolutionInspector (Preferences prefs) { - types = new(prefs); - members = new(prefs, (type, ik) => { - types.Crawl(type, ik); - return serde.Inspect(type) ?? itd!.Inspect(type, ik) ?? new TypeMeta(type); - }); + members = new(prefs, (type, ik) => itd!.Inspect(type, ik) ?? serde!.Inspect(type, ik) ?? new TypeMeta(type)); itd = new(members); + serde = new(itd); } /// @@ -57,7 +53,6 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { Static = statics.ToArray(), Modules = modules.ToArray(), - Types = types.Collect().Where(t => !modules.Any(m => m == t)).ToArray(), Instanced = itd.Collect().Except(modules).ToArray(), Serialized = serde.Collect(), Documentation = docs.ToArray(), diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs deleted file mode 100644 index 19e5b222..00000000 --- a/src/cs/Bootsharp.Publish/Common/Inspector/TypeInspector.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class TypeInspector -{ - private readonly Dictionary byType = []; - private readonly SerializedInspector serde = new(); - private readonly InstancedInspector itd; - private InteropKind ik; - - public TypeInspector (Preferences prefs) - { - itd = new(new(prefs, Inspect)); - } - - public void Crawl (Type type, InteropKind ik) - { - this.ik = ik; - Crawl(type); - } - - public IReadOnlyCollection Collect () - { - return byType.Values.Distinct().Where(m => IsUserType(m.Clr)).ToArray(); - } - - private void Crawl (Type type) - { - if (!byType.TryAdd(type, Inspect(type, ik))) return; - if (IsNullable(type, out var nullValue)) Crawl(nullValue); - else if (IsList(type, out var element)) Crawl(element); - else if (IsDictionary(type, out var key, out var value)) - { - Crawl(key); - Crawl(value); - } - else - { - CrawlProperties(type); - CrawlBaseType(type); - } - } - - private void CrawlProperties (Type type) - { - foreach (var prop in type.GetProperties()) - Crawl(prop.PropertyType); - } - - private void CrawlBaseType (Type type) - { - if (type.BaseType != null) - Crawl(type.BaseType); - } - - private TypeMeta Inspect (Type type, InteropKind ik) - { - return serde.Inspect(type) ?? itd.Inspect(type, ik) ?? new TypeMeta(type); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs index ef2f441b..09b6a987 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs @@ -49,6 +49,11 @@ internal sealed record SerializedListMeta (Type Clr, SerializedMeta Element) : S internal sealed record SerializedDictionaryMeta (Type Clr, SerializedMeta Key, SerializedMeta Value) : SerializedMeta(Clr); +/// +/// Describes an instance reference () under a serialized object. +/// +internal sealed record SerializedInstanceMeta (Type Clr) : SerializedMeta(Clr); + /// /// Describes a serialized user-defined object, such as a record or a struct. /// diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 57274c95..ff90f6a5 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -1,3 +1,5 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; using System.Text; namespace Bootsharp.Publish; @@ -7,51 +9,62 @@ internal sealed class TypeDeclarationGenerator (Preferences prefs) private readonly StringBuilder bld = new(); private readonly TypeSyntaxBuilder ts = new(prefs); - private TypeMeta type => types[index]; - private TypeMeta? prevType => index == 0 ? null : types[index - 1]; - private TypeMeta? nextType => index == types.Length - 1 ? null : types[index + 1]; - private int indent => !string.IsNullOrEmpty(GetNamespace(type)) ? 1 : 0; + private TypeMeta meta => metas[index]; + private TypeMeta? prevMeta => index == 0 ? null : metas[index - 1]; + private TypeMeta? nextMeta => index == metas.Count - 1 ? null : metas[index + 1]; + private int indent => !string.IsNullOrEmpty(GetNamespace(meta)) ? 1 : 0; private DocumentationBuilder docs = null!; - private TypeMeta[] types = null!; + private readonly Dictionary metaByClr = []; + private readonly List metas = []; private int index; public string Generate (SolutionInspection spec) { docs = new(spec.Documentation); - types = spec.Types.OrderBy(GetNamespace).ToArray(); - for (index = 0; index < types.Length; index++) + CollectMetas(spec); + for (index = 0; index < metas.Count; index++) DeclareType(); return bld.ToString(); } + private void CollectMetas (SolutionInspection spec) + { + foreach (var meta in spec.Instanced) + metas.Add(metaByClr[meta.Clr] = meta); + foreach (var meta in spec.Serialized) + if (metaByClr.TryAdd(meta.Clr, meta) && IsUserType(meta.Clr)) + metas.Add(meta); + metas.Sort((a, b) => string.Compare(GetNamespace(a), GetNamespace(b), StringComparison.Ordinal)); + } + private void DeclareType () { if (ShouldOpenNamespace()) OpenNamespace(); - if (type is InstancedMeta it) DeclareInstanced(it); - else if (type is SerializedEnumMeta enu) DeclareEnum(enu); - else if (type is SerializedObjectMeta obj) DeclareSerialized(obj); + if (meta is InstancedMeta it) DeclareInstanced(it); + else if (meta is SerializedEnumMeta enu) DeclareEnum(enu); + else if (meta is SerializedObjectMeta obj) DeclareSerialized(obj); if (ShouldCloseNamespace()) CloseNamespace(); } private bool ShouldOpenNamespace () { - if (string.IsNullOrEmpty(GetNamespace(type))) return false; - if (prevType == null) return true; - return GetNamespace(prevType) != GetNamespace(type); + if (string.IsNullOrEmpty(GetNamespace(meta))) return false; + if (prevMeta == null) return true; + return GetNamespace(prevMeta) != GetNamespace(meta); } private void OpenNamespace () { - var space = GetNamespace(type); + var space = GetNamespace(meta); AppendLine($"export namespace {space} {{", 0); } private bool ShouldCloseNamespace () { - if (string.IsNullOrEmpty(GetNamespace(type))) return false; - if (nextType is null) return true; - return GetNamespace(nextType) != GetNamespace(type); + if (string.IsNullOrEmpty(GetNamespace(meta))) return false; + if (nextMeta is null) return true; + return GetNamespace(nextMeta) != GetNamespace(meta); } private void CloseNamespace () @@ -77,11 +90,11 @@ private void DeclareSerialized (SerializedObjectMeta obj) { bld.Append(docs.BuildType(obj.Clr, indent)); AppendLine($"export type {ts.BuildName(obj.Clr)} = ", indent); - if (obj.Clr.BaseType is { } baseType && IsUserType(baseType)) + if (TryGetBase(obj.Clr, out var baseType)) bld.Append(ts.BuildFullName(baseType)).Append(" & "); bld.Append("Readonly<{"); foreach (var prop in obj.Properties) - if (prop.Info.DeclaringType == obj.Clr) + if (ShouldDeclareOn(obj.Clr, prop.Info)) AppendProperty(prop); AppendLine("}>;", indent); @@ -100,8 +113,9 @@ private void DeclareInstanced (InstancedMeta it) AppendLine($"export interface {ts.BuildName(it.Clr)}", indent); AppendExtensions(); bld.Append(" {"); - foreach (var member in it.Members.Where(m => m.Info.DeclaringType == it.Clr)) - if (member is EventMeta evt) AppendEvent(evt); + foreach (var member in it.Members) + if (!ShouldDeclareOn(it.Clr, member.Info)) continue; + else if (member is EventMeta evt) AppendEvent(evt); else if (member is PropertyMeta prop) AppendProperty(prop); else AppendMethod((MethodMeta)member); AppendLine("}", indent); @@ -109,7 +123,7 @@ private void DeclareInstanced (InstancedMeta it) void AppendExtensions () { var extTypes = new List(it.Clr.GetInterfaces().Where(IsUserType)); - if (it.Clr.BaseType is { } baseType && IsUserType(baseType)) + if (TryGetBase(it.Clr, out var baseType)) extTypes.Insert(0, baseType); if (extTypes.Count > 0) bld.Append(" extends ").AppendJoin(", ", extTypes.Select(ts.BuildFullName)); @@ -159,8 +173,20 @@ private void Append (string content, int level) bld.Append(content); } - private string GetNamespace (TypeMeta type) + private string GetNamespace (TypeMeta meta) + { + return BuildJSSpace(meta.Clr, prefs); + } + + private bool TryGetBase (Type clr, [NotNullWhen(true)] out Type? baseType) + { + if ((baseType = clr.BaseType) == null || !IsUserType(baseType)) return false; + return metaByClr.ContainsKey(baseType); + } + + private bool ShouldDeclareOn (Type host, MemberInfo member) { - return BuildJSSpace(type.Clr, prefs); + if (member.DeclaringType == host) return true; + return !TryGetBase(member.DeclaringType!, out _) && !TryGetBase(host, out _); } } From 4e964de67ee294ec68d578e4b19f9ed438cac928 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 17:09:03 +0300 Subject: [PATCH 13/20] etc --- .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs index b7224b6d..2aac6aad 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -5,7 +5,7 @@ namespace Bootsharp.Publish; /// internal sealed class ModuleGenerator { - private InstancedMeta it = null!; + private InstancedMeta md = null!; public string Generate (SolutionInspection spec) => $$""" @@ -27,52 +27,52 @@ internal static void RegisterModules () {{Fmt(spec.Modules.Select(EmitModule), 0, "\n\n")}} """; - private string EmitRegistration (InstancedMeta it) + private string EmitRegistration (InstancedMeta md) { - var type = it.Interop == InteropKind.Import - ? $"typeof({it.Syntax})" - : $"typeof({it.FullName})"; - var factory = it.Interop == InteropKind.Import - ? $"new ImportModule(new {it.FullName}())" - : $"new ExportModule(typeof({it.Syntax}), handler => new {it.FullName}(({it.Syntax})handler))"; + var type = md.Interop == InteropKind.Import + ? $"typeof({md.Syntax})" + : $"typeof({md.FullName})"; + var factory = md.Interop == InteropKind.Import + ? $"new ImportModule(new {md.FullName}())" + : $"new ExportModule(typeof({md.Syntax}), handler => new {md.FullName}(({md.Syntax})handler))"; return $"Modules.Register({type}, {factory});"; } - private string EmitModule (InstancedMeta it) + private string EmitModule (InstancedMeta md) { - this.it = it; - if (it.Interop == InteropKind.Export) return EmitModuleExport(); + this.md = md; + if (md.Interop == InteropKind.Export) return EmitModuleExport(); return EmitModuleImport(); } private string EmitModuleExport () => $$""" - namespace {{it.Namespace}} + namespace {{md.Namespace}} { - public class {{it.Name}} + public class {{md.Name}} { - private static {{it.Syntax}} handler = null!; + private static {{md.Syntax}} handler = null!; - public {{it.Name}} ({{it.Syntax}} handler) + public {{md.Name}} ({{md.Syntax}} handler) { {{Fmt([ - $"{it.Name}.handler = handler;", - ..it.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") + $"{md.Name}.handler = handler;", + ..md.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") ], 3)}} } - {{Fmt(it.Members.Select(EmitMemberExport), 2)}} + {{Fmt(md.Members.Select(EmitMemberExport), 2)}} } } """; private string EmitModuleImport () => $$""" - namespace {{it.Namespace}} + namespace {{md.Namespace}} { - public class {{it.Name}} : {{it.Syntax}} + public class {{md.Name}} : {{md.Syntax}} { - {{Fmt(it.Members.Select(EmitMemberImport), 2)}} + {{Fmt(md.Members.Select(EmitMemberImport), 2)}} } } """; @@ -117,11 +117,11 @@ private string EmitPropertyExport (PropertyMeta prop) private string EmitPropertyImport (PropertyMeta prop) { - var space = $"global::Bootsharp.Generated.Interop.{it.FullName.Replace('.', '_')}"; + var space = $"global::Bootsharp.Generated.Interop.{md.FullName.Replace('.', '_')}"; var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; return $$""" - {{type}} {{it.Syntax}}.{{prop.Name}} + {{type}} {{md.Syntax}}.{{prop.Name}} { {{Fmt( prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, @@ -143,8 +143,8 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); - var name = $"{it.FullName.Replace('.', '_')}_{method.Name}"; - return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + + var name = $"{md.FullName.Replace('.', '_')}_{method.Name}"; + return $"{method.Return.TypeSyntax} {md.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } From f5ee725b5e58e7568e297ed80be91c013002e5a6 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 18:06:34 +0300 Subject: [PATCH 14/20] refactor member space --- .../Common/Inspector/InstancedInspector.cs | 22 ++++----- .../Common/Inspector/MemberInspector.cs | 18 +++----- .../Common/Inspector/SolutionInspector.cs | 4 +- .../Emit/InstanceGenerator.cs | 4 +- .../Emit/InteropGenerator.cs | 46 +++++++++++-------- .../Pack/BindingGenerator/BindingGenerator.cs | 45 +++++++++--------- src/cs/Directory.Build.props | 2 +- 7 files changed, 72 insertions(+), 69 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index 9b9d0e7c..12589ea0 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -21,20 +21,18 @@ public IReadOnlyCollection Collect () private InstancedMeta InspectType (Type type, InteropKind ik) => new(type) { Interop = ik, - Namespace = BuildInstanceSpace(type, ik), - Name = BuildInstanceName(type), - JSName = BuildInstanceJSName(type), + Namespace = BuildSpace(type, ik), + Name = BuildName(type), + JSName = BuildJSName(type), Members = new List() }; private InstancedMeta CollectMembers (InstancedMeta it) { - var ik = it.Interop; - var type = it.Clr; var cl = (List)it.Members; - cl.AddRange(type.GetEvents().Select(m => members.Inspect(m, ik, it))); - cl.AddRange(type.GetProperties().Where(ShouldInspectProperty).Select(m => members.Inspect(m, ik, it))); - cl.AddRange(type.GetMethods().Where(ShouldInspectMethod).Select(m => members.Inspect(m, ik, it))); + cl.AddRange(it.Clr.GetEvents().Select(m => members.Inspect(m, it.Interop))); + cl.AddRange(it.Clr.GetProperties().Where(ShouldInspectProperty).Select(m => members.Inspect(m, it.Interop))); + cl.AddRange(it.Clr.GetMethods().Where(ShouldInspectMethod).Select(m => members.Inspect(m, it.Interop))); return it; } @@ -55,22 +53,22 @@ private bool ShouldInspectMethod (MethodInfo method) return !method.IsStatic; } - private string BuildInstanceSpace (Type type, InteropKind ik) + private string BuildSpace (Type type, InteropKind ik) { var space = "Bootsharp.Generated." + (ik == InteropKind.Export ? "Exports" : "Imports"); if (type.Namespace != null) space += $".{type.Namespace}"; return space; } - private string BuildInstanceName (Type type) + private string BuildName (Type type) { var trimmed = type.IsInterface ? type.Name[1..] : type.Name; return "JS" + trimmed; } - private string BuildInstanceJSName (Type type) + private string BuildJSName (Type type) { - var name = BuildInstanceName(type); + var name = BuildName(type); if (type.Namespace == null) return name; return $"{type.Namespace}.{name}".Replace(".", "_"); } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs index d2458f7d..8fde9045 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs @@ -4,9 +4,9 @@ namespace Bootsharp.Publish; internal sealed class MemberInspector (Preferences prefs, Func inspect) { - public EventMeta Inspect (EventInfo evt, InteropKind ik, InstancedMeta? host) => new(evt) { + public EventMeta Inspect (EventInfo evt, InteropKind ik) => new(evt) { Interop = ik, - Space = BuildSpace(evt.DeclaringType!, host), + Space = evt.DeclaringType!.FullName!, JSSpace = BuildJSSpace(evt.DeclaringType!), Name = evt.Name, JSName = BuildJSName(evt.Name), @@ -14,9 +14,9 @@ internal sealed class MemberInspector (Preferences prefs, Func CreateArg(p, GetNullability(evt, p), ik)).ToArray() }; - public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik, InstancedMeta? host) => new(prop) { + public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik) => new(prop) { Interop = ik, - Space = BuildSpace(prop.DeclaringType!, host), + Space = prop.DeclaringType!.FullName!, JSSpace = BuildJSSpace(prop.DeclaringType!), Name = prop.Name, JSName = BuildJSName(prop.Name), @@ -24,9 +24,9 @@ internal sealed class MemberInspector (Preferences prefs, Func new(method) { + public MethodMeta Inspect (MethodInfo method, InteropKind ik) => new(method) { Interop = ik, - Space = BuildSpace(method.DeclaringType!, host), + Space = method.DeclaringType!.FullName!, JSSpace = BuildJSSpace(method.DeclaringType!), Name = method.Name, JSName = WithPrefs(prefs.Function, method.Name, BuildJSName(method.Name)), @@ -49,12 +49,6 @@ internal sealed class MemberInspector (Preferences prefs, Func $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = PrependIdArg(string.Join(", ", method.Arguments.Select(a => a.Name))); - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var name = $"{it.FullName.Replace('.', '_')}_{method.Name}"; return $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index b5beb293..5df3ad37 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -7,10 +7,14 @@ namespace Bootsharp.Publish; /// internal sealed class InteropGenerator { + [MemberNotNullWhen(true, nameof(it))] + private bool isIt => it != null; + [MemberNotNullWhen(true, nameof(md))] + private bool isMd => md != null; + private readonly HashSet registered = []; + private string id = null!, space = null!; private InstancedMeta? it, md; - [MemberNotNullWhen(true, nameof(it))] private bool isIt => it != null; - [MemberNotNullWhen(true, nameof(md))] private bool isMd => md != null; public string Generate (SolutionInspection spec) => $$""" @@ -32,9 +36,11 @@ internal static unsafe void Initialize () { {{Fmt([ ..spec.Static.OfType() - .Concat(spec.Modules.SelectMany(i => i.Members.OfType())) .Where(e => e.Interop == InteropKind.Export) - .Select(EmitEventSubscription), + .Select(e => EmitEventSubscription(e, e.Space)), + ..spec.Modules.SelectMany(md => md.Members.OfType() + .Where(e => e.Interop == InteropKind.Export) + .Select(e => EmitEventSubscription(e, md.FullName))), ..spec.Static.OfType() .Where(m => m.Interop == InteropKind.Import) .Select(EmitMethodAssignment) @@ -47,10 +53,10 @@ internal static unsafe void Initialize () } """; - private static string EmitEventSubscription (EventMeta evt) + private static string EmitEventSubscription (EventMeta evt, string space) { - var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; - return $"global::{evt.Space}.{evt.Name} += {handler};"; + var handler = $"Handle_{space.Replace('.', '_')}_{evt.Name}"; + return $"global::{space}.{evt.Name} += {handler};"; } private static string EmitMethodAssignment (MethodMeta method) @@ -63,6 +69,8 @@ private static string EmitMethodAssignment (MethodMeta method) { this.it = it; this.md = md; + space = (it ?? md)?.FullName ?? member.Space; + id = space.Replace('.', '_'); return member switch { EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), EventMeta { Interop: InteropKind.Import } e => EmitEventImport(e), @@ -83,7 +91,7 @@ private static string EmitMethodAssignment (MethodMeta method) if (isIt) yield return EmitInstanceRegistrar(it); if (isIt) yield break; // instanced export event handlers are emitted in the registrar - var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; + var handler = $"Handle_{id}_{evt.Name}"; var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); yield return $"private static void {handler} ({sigArgs}) => {name}({invArgs});"; @@ -91,11 +99,11 @@ private static string EmitMethodAssignment (MethodMeta method) private IEnumerable EmitEventImport (EventMeta evt) { - var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; + var name = $"{id}_Invoke{evt.Name}"; var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; var invName = isIt ? $"Instances.Import(_id, static id => new global::{it.FullName}(id)).Invoke{evt.Name}" - : isMd ? $"((global::{evt.Space})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" + : isMd ? $"((global::{md.FullName})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; var invArgs = string.Join(", ", evt.Arguments.Select(Deserialize)); yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; @@ -106,22 +114,22 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) if (prop.CanGet) { var attr = $"[JSExport] {MarshalAmbiguous(prop.GetValue, true)}"; - var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var name = $"{id}_GetProperty{prop.Name}"; var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; var body = Serialize(prop.GetValue, isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name}" - : $"global::{prop.Space}.GetProperty{prop.Name}()"); + : $"global::{space}.GetProperty{prop.Name}()"); yield return $"{attr}internal static {BuildValueSyntax(prop.GetValue)} {name} ({args}) => {body};"; } if (prop.CanSet) { - var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var name = $"{id}_SetProperty{prop.Name}"; var args = BuildParameter(prop.SetValue, "value"); if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; var value = Deserialize(prop.SetValue, "value"); var body = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" - : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; + : $"global::{space}.SetProperty{prop.Name}({value})"; yield return $"[JSExport] internal static void {name} ({args}) => {body};"; } } @@ -136,7 +144,7 @@ private IEnumerable EmitPropertyImport (PropertyMeta prop) var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; yield return $"{attr}internal static partial {BuildValueSyntax(prop.GetValue)} {serdeName} ({args});"; - var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var name = $"{id}_GetProperty{prop.Name}"; var body = Deserialize(prop.GetValue, isIt ? $"{serdeName}(_id)" : $"{serdeName}()"); yield return $"public static {prop.GetValue.TypeSyntax} {name}({args}) => {body};"; } @@ -148,7 +156,7 @@ private IEnumerable EmitPropertyImport (PropertyMeta prop) if (isIt) serdeArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(serdeArgs)}"; yield return $"{attr}internal static partial void {serdeName} ({serdeArgs});"; - var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var name = $"{id}_SetProperty{prop.Name}"; var args = $"{prop.SetValue.TypeSyntax} value"; if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; var value = Serialize(prop.SetValue, "value"); @@ -161,7 +169,7 @@ private IEnumerable EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var name = $"{id}_{method.Name}"; var @return = BuildValueSyntax(method.Return); if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); @@ -169,7 +177,7 @@ private IEnumerable EmitMethodExport (MethodMeta method) var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); var invName = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" - : $"global::{method.Space}.{method.Name}"; + : $"global::{space}.{method.Name}"; var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; } @@ -178,7 +186,7 @@ private IEnumerable EmitMethodImport (MethodMeta method) { var marshalAs = MarshalAmbiguous(method.Return, true); var attr = $"""[JSImport("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")] {marshalAs}"""; - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var name = $"{id}_{method.Name}"; var @return = BuildValueSyntax(method.Return); if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; var args = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 2cd3b752..25b16cd9 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -5,29 +5,32 @@ namespace Bootsharp.Publish; internal sealed class BindingGenerator (Preferences prefs, bool debug) { - private record Binding (MemberMeta? Member, Type? Enum, InstancedMeta? It, string Namespace); + private record Binding (MemberMeta? Member, Type? Enum, InstancedMeta? It, string Id, string Space); private Binding binding => bindings[index]; private Binding? prevBinding => index == 0 ? null : bindings[index - 1]; private Binding? nextBinding => index == bindings.Length - 1 ? null : bindings[index + 1]; + [MemberNotNullWhen(true, nameof(it))] + private bool isIt => it != null; + private InstancedMeta? it => binding.It; + private string space => binding.Space; + private string id => binding.Id; private readonly StringBuilder bld = new(); - [MemberNotNullWhen(true, nameof(it))] private bool isIt => it != null; - private InstancedMeta? it => binding.It; private Binding[] bindings = []; private int index, level; public string Generate (SolutionInspection spec) { bindings = spec.Static - .Select(m => new Binding(m, null, null, m.JSSpace)) - .Concat(spec.Modules.SelectMany(it => it.Members - .Select(m => new Binding(m, null, null, m.JSSpace)))) + .Select(m => new Binding(m, null, null, m.Space.Replace('.', '_'), m.JSSpace)) + .Concat(spec.Modules.SelectMany(md => md.Members + .Select(m => new Binding(m, null, null, md.FullName.Replace('.', '_'), m.JSSpace)))) .Concat(spec.Instanced.SelectMany(it => it.Members - .Select(m => new Binding(m, null, it, m.JSSpace)))) + .Select(m => new Binding(m, null, it, it.FullName.Replace('.', '_'), m.JSSpace)))) .Concat(spec.Serialized.Where(t => t.Clr.IsEnum) - .Select(t => new Binding(null, t.Clr, null, BuildJSSpace(t.Clr, prefs)))) - .OrderBy(m => m.Namespace).ToArray(); + .Select(t => new Binding(null, t.Clr, null, "", BuildJSSpace(t.Clr, prefs)))) + .OrderBy(m => m.Space).ToArray(); if (bindings.Length == 0) return ""; EmitImports(); @@ -120,14 +123,14 @@ private void EmitBinding () private bool ShouldOpenNamespace () { if (prevBinding is null) return true; - return prevBinding.Namespace != binding.Namespace; + return prevBinding.Space != binding.Space; } private void OpenNamespace () { level = 0; - var prevParts = prevBinding?.Namespace.Split('.') ?? []; - var parts = binding.Namespace.Split('.'); + var prevParts = prevBinding?.Space.Split('.') ?? []; + var parts = binding.Space.Split('.'); while (prevParts.ElementAtOrDefault(level) == parts[level]) level++; for (var i = level; i < parts.Length; level = i, i++) if (i == 0) bld.Append($"\nexport const {parts[i]} = {{"); @@ -137,7 +140,7 @@ private void OpenNamespace () private bool ShouldCloseNamespace () { if (nextBinding is null) return true; - return nextBinding.Namespace != binding.Namespace; + return nextBinding.Space != binding.Space; } private void CloseNamespace () @@ -151,8 +154,8 @@ int GetCloseLevel () { if (nextBinding is null) return 0; var closeLevel = 0; - var parts = binding.Namespace.Split('.'); - var nextParts = nextBinding.Namespace.Split('.'); + var parts = binding.Space.Split('.'); + var nextParts = nextBinding.Space.Split('.'); for (var i = 0; i < parts.Length; i++) if (parts[i] == nextParts[i]) closeLevel++; else break; @@ -197,7 +200,7 @@ private void EmitEventExport (EventMeta evt) private void EmitEventImport (EventMeta evt) { if (isIt) return; // instanced import event handlers are emitted in the registrar - var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; + var name = $"{id}_Invoke{evt.Name}"; var invName = debug ? $"""getExport("{name}")""" : $"exports.{name}"; var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); @@ -208,7 +211,7 @@ private void EmitPropertyExport (PropertyMeta prop) { if (prop.CanGet) { - var fnName = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; + var fnName = $"{id}_GetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var body = Deserialize(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); if (prop.GetValue.Nullable && !prop.GetValue.IsInstanced) body += " ?? undefined"; @@ -217,7 +220,7 @@ private void EmitPropertyExport (PropertyMeta prop) } if (prop.CanSet) { - var fnName = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; + var fnName = $"{id}_SetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var value = Serialize(prop.SetValue, "value"); var body = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; @@ -248,7 +251,7 @@ private void EmitPropertyImport (PropertyMeta prop) private void EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); - var fnName = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var fnName = $"{id}_{method.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); @@ -272,7 +275,7 @@ private void EmitMethodImport (MethodMeta method) else { var serde = $"this.{name}SerializedHandler"; - var serdeExp = debug ? $"getImport({invName}, {serde}, \"{binding.Namespace}.{name}\")" : serde; + var serdeExp = debug ? $"getImport({invName}, {serde}, \"{space}.{name}\")" : serde; bld.Append($"{Br}get {name}() {{ return {invName}; }}"); bld.Append($"{Br}set {name}(handler) {{ {invName} = handler; {serde} = {serdeHandler}; }}"); bld.Append($"{Br}get {name}Serialized() {{ return {serdeExp}; }}"); @@ -301,7 +304,7 @@ private void EmitInstanceRegistrar (InstancedMeta instance) }; {{Fmt(events.Select(e => { - var fnName = $"{e.Space.Replace('.', '_')}_Invoke{e.Name}"; + var fnName = $"{instance.FullName.Replace('.', '_')}_Invoke{e.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", e.Arguments.Select(a => a.JSName)); var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(Serialize))); diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 79f98c76..e0ae2e86 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.164 + 0.8.0-alpha.165 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From d3b1caccf3d4c34103ed005b22a75d038dcd4b95 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 18:48:37 +0300 Subject: [PATCH 15/20] etc --- .../Common/Global/GlobalInspection.cs | 2 ++ .../Common/Inspector/MemberInspector.cs | 2 +- .../Common/Inspector/SolutionInspector.cs | 11 ++++++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index a8108306..78050b44 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -5,6 +5,8 @@ namespace Bootsharp.Publish; +internal delegate TypeMeta InspectType (Type type, InteropKind ik); + internal static class GlobalInspection { public static MetadataLoadContext CreateLoadContext (string directory) diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs index 8fde9045..0b5e2935 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs @@ -2,7 +2,7 @@ namespace Bootsharp.Publish; -internal sealed class MemberInspector (Preferences prefs, Func inspect) +internal sealed class MemberInspector (Preferences prefs, InspectType inspect) { public EventMeta Inspect (EventInfo evt, InteropKind ik) => new(evt) { Interop = ik, diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs index 6641a288..9dd930b6 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs @@ -9,13 +9,13 @@ internal sealed class SolutionInspector private readonly List modules = []; private readonly List docs = []; private readonly List warnings = []; - private readonly SerializedInspector serde; - private readonly InstancedInspector itd; private readonly MemberInspector members; + private readonly InstancedInspector itd; + private readonly SerializedInspector serde; public SolutionInspector (Preferences prefs) { - members = new(prefs, (type, ik) => itd!.Inspect(type, ik) ?? serde!.Inspect(type, ik) ?? new TypeMeta(type)); + members = new(prefs, InspectType); itd = new(members); serde = new(itd); } @@ -34,6 +34,11 @@ public SolutionInspection Inspect (string directory, IEnumerable paths) return CreateInspection(ctx); } + private TypeMeta InspectType (Type type, InteropKind ik) + { + return itd.Inspect(type, ik) ?? serde.Inspect(type, ik) ?? new TypeMeta(type); + } + private void InspectAssemblyFile (string assemblyPath, MetadataLoadContext ctx) { var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); From 443714f365b30841719ec36fb7ebf3e0be9e693e Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 6 May 2026 19:26:00 +0300 Subject: [PATCH 16/20] refactor modules --- .../Common/Global/GlobalInspection.cs | 5 +--- .../Common/Global/GlobalType.cs | 11 +++++++ .../Common/Inspector/InstancedInspector.cs | 7 +++++ .../Common/Inspector/SolutionInspection.cs | 2 +- .../Common/Inspector/SolutionInspector.cs | 9 +++--- .../Common/Meta/InstancedMeta.cs | 10 +++---- .../Common/Meta/ModuleMeta.cs | 29 +++++++++++++++++++ .../Emit/InteropGenerator.cs | 7 +++-- .../Bootsharp.Publish/Emit/ModuleGenerator.cs | 6 ++-- 9 files changed, 65 insertions(+), 21 deletions(-) create mode 100644 src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 78050b44..533a4d6c 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -38,10 +38,7 @@ public static bool IsInstancedType (Type type) // interop boundary (as opposed to serialized immutable types, which are copied by value). if (!IsUserType(type)) return false; if (type.IsInterface) return true; - var isRecord = type.GetMethod("$", // records are immutable by convention - BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) != null; - var isStatic = type.IsAbstract && type.IsSealed; - return type.IsClass && !isStatic && !isRecord; + return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention } public static bool IsAutoProperty (PropertyInfo prop) diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 12071116..0ed64fe0 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -7,6 +7,17 @@ namespace Bootsharp.Publish; internal static class GlobalType { + public static bool IsStatic (Type type) + { + return type.IsAbstract && type.IsSealed; + } + + public static bool IsRecord (Type type) + { + var flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + return type.GetMethod("$", flags) != null; + } + public static bool IsTaskLike (Type type) { return type.GetMethod(nameof(Task.GetAwaiter)) != null; diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index 12589ea0..75c91a0e 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -14,6 +14,13 @@ internal sealed class InstancedInspector (MemberInspector members) return CollectMembers(byType[type] = InspectType(type, ik)); } + public ModuleMeta? InspectModule (Type type, InteropKind ik) + { + if (ik == InteropKind.Import && !type.IsInterface || IsStatic(type)) return null; + var it = CollectMembers(InspectType(type, ik)); + return new(type) { Interop = ik, Namespace = it.Namespace, Name = it.Name, Members = it.Members }; + } + public IReadOnlyCollection Collect () { return byType.Values.ToArray(); diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs index 97f4ffd8..8cb4b7e3 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs @@ -22,7 +22,7 @@ internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable /// Interop API surfaces specified under assembly-level /// or attributes. /// - public required IReadOnlyCollection Modules { get; init; } + public required IReadOnlyCollection Modules { get; init; } /// /// All the immutable types that are serialized and copied by value when crossing the interop boundary. /// diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs index 9dd930b6..09358ad1 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs @@ -6,7 +6,7 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspector { private readonly List statics = []; - private readonly List modules = []; + private readonly List modules = []; private readonly List docs = []; private readonly List warnings = []; private readonly MemberInspector members; @@ -58,7 +58,7 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { Static = statics.ToArray(), Modules = modules.ToArray(), - Instanced = itd.Collect().Except(modules).ToArray(), + Instanced = itd.Collect(), Serialized = serde.Collect(), Documentation = docs.ToArray(), Warnings = warnings.ToArray() @@ -93,9 +93,8 @@ private void InspectModules (CustomAttributeData attr) { if (ResolveInterop(attr) is not { } ik) return; foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) - if (itd.Inspect((Type)arg.Value!, ik) is { } it) - if (ik == InteropKind.Export || it.Clr.IsInterface) - modules.Add(it); + if (itd.InspectModule((Type)arg.Value!, ik) is { } md) + modules.Add(md); } private InteropKind? ResolveInterop (MemberInfo info) diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs index 7f8fd44e..9e691e49 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs @@ -3,26 +3,26 @@ namespace Bootsharp.Publish; /// /// Describes a mutable CLR type whose instances are passed by reference when crossing the interop boundary. /// -internal sealed record InstancedMeta (Type Clr) : TypeMeta(Clr) +internal record InstancedMeta (Type Clr) : TypeMeta(Clr) { /// /// Whether the type's instances are exported from C# or imported from JavaScript. /// public required InteropKind Interop { get; init; } /// - /// C# namespace of the generated interop class implementation. + /// Namespace of the generated C# bindings wrapper. /// public required string Namespace { get; init; } /// - /// C# name of the generated interop class implementation. + /// Name of the generated C# bindings wrapper. /// public required string Name { get; init; } /// - /// Full C# type name of the generated interop class implementation. + /// Full type name of the generated C# bindings wrapper. /// public string FullName => $"{Namespace}.{Name}"; /// - /// JS name of the generated interop class implementation. + /// Name of the generated JavaScript bindings wrapper. /// public required string JSName { get; init; } /// diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs new file mode 100644 index 00000000..41806759 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Meta/ModuleMeta.cs @@ -0,0 +1,29 @@ +namespace Bootsharp.Publish; + +/// +/// Describes a CLR type specified as interop surface under an assembly-level +/// or attribute. +/// +internal record ModuleMeta (Type Clr) : TypeMeta(Clr) +{ + /// + /// Whether the module is exported from C# or imported from JavaScript. + /// + public required InteropKind Interop { get; init; } + /// + /// Namespace of the generated C# bindings wrapper. + /// + public required string Namespace { get; init; } + /// + /// Name of the generated C# bindings wrapper. + /// + public required string Name { get; init; } + /// + /// Full type name of the generated C# bindings wrapper. + /// + public string FullName => $"{Namespace}.{Name}"; + /// + /// Members declared on the module. + /// + public required IReadOnlyCollection Members { get; init; } +} diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 5df3ad37..397119d7 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -14,7 +14,8 @@ internal sealed class InteropGenerator private readonly HashSet registered = []; private string id = null!, space = null!; - private InstancedMeta? it, md; + private InstancedMeta? it; + private ModuleMeta? md; public string Generate (SolutionInspection spec) => $$""" @@ -65,11 +66,11 @@ private static string EmitMethodAssignment (MethodMeta method) return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; } - private IEnumerable EmitMember (MemberMeta member, InstancedMeta? it, InstancedMeta? md) + private IEnumerable EmitMember (MemberMeta member, InstancedMeta? it, ModuleMeta? md) { this.it = it; this.md = md; - space = (it ?? md)?.FullName ?? member.Space; + space = it?.FullName ?? md?.FullName ?? member.Space; id = space.Replace('.', '_'); return member switch { EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), diff --git a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs index 2aac6aad..ed0966a3 100644 --- a/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -5,7 +5,7 @@ namespace Bootsharp.Publish; /// internal sealed class ModuleGenerator { - private InstancedMeta md = null!; + private ModuleMeta md = null!; public string Generate (SolutionInspection spec) => $$""" @@ -27,7 +27,7 @@ internal static void RegisterModules () {{Fmt(spec.Modules.Select(EmitModule), 0, "\n\n")}} """; - private string EmitRegistration (InstancedMeta md) + private string EmitRegistration (ModuleMeta md) { var type = md.Interop == InteropKind.Import ? $"typeof({md.Syntax})" @@ -38,7 +38,7 @@ private string EmitRegistration (InstancedMeta md) return $"Modules.Register({type}, {factory});"; } - private string EmitModule (InstancedMeta md) + private string EmitModule (ModuleMeta md) { this.md = md; if (md.Interop == InteropKind.Export) return EmitModuleExport(); From 5fc7667dbbcfe7b0f8734805f7e085624e73add6 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 7 May 2026 14:47:27 +0300 Subject: [PATCH 17/20] implement serialized instanced --- .../Emit/InstancesTest.cs | 56 ++++++++- .../Emit/InteropTest.cs | 61 ++-------- .../Emit/SerializerTest.cs | 86 +++++++------ .../Pack/BindingTest.cs | 42 +++---- .../Pack/DeclarationTest.cs | 24 ++-- .../Common/Global/GlobalType.cs | 45 +++++++ .../Common/Inspector/InstancedInspector.cs | 16 ++- .../Common/Inspector/MemberInspector.cs | 3 +- .../Common/Inspector/SerializedInspector.cs | 6 +- .../Common/Meta/InstancedMeta.cs | 8 ++ .../Common/Meta/SerializedMeta.cs | 5 +- .../Common/Meta/ValueMeta.cs | 5 - .../Emit/InstanceGenerator.cs | 65 ++++++++-- .../Emit/InteropGenerator.cs | 93 +++----------- .../Emit/SerializerGenerator.cs | 49 +++++--- .../Pack/BindingGenerator/BindingGenerator.cs | 73 ++++------- .../BindingSerializerGenerator.cs | 39 ++++-- src/cs/Directory.Build.props | 2 +- src/js/src/exports.ts | 5 +- src/js/src/instances.ts | 2 +- .../Interfaces/ExportedInnerInstanced.cs | 12 ++ .../Interfaces/ExportedInstanced.cs | 1 + .../{ExportedStatic.cs => ExportedModule.cs} | 4 +- .../Interfaces/IExportedInstanced.cs | 1 + ...{IExportedStatic.cs => IExportedModule.cs} | 2 +- .../Interfaces/IImportedInnerInstanced.cs | 12 ++ .../Interfaces/IImportedInstanced.cs | 1 + ...{IImportedStatic.cs => IImportedModule.cs} | 2 +- .../cs/Test.Types/Interfaces/Interfaces.cs | 40 +++++-- .../cs/Test.Types/Registries/IRegistry.cs | 9 ++ .../Registries/IRegistryProvider.cs | 10 ++ .../Registry.cs => Registries/Registries.cs} | 19 +-- .../{Vehicle => Registries}/TrackType.cs | 0 .../{Vehicle => Registries}/Tracked.cs | 2 +- .../{Vehicle => Registries}/Vehicle.cs | 2 +- .../{Vehicle => Registries}/Wheeled.cs | 2 +- .../Test.Types/Vehicle/IRegistryProvider.cs | 10 -- src/js/test/cs/Test/Program.cs | 8 +- src/js/test/spec/boot.spec.ts | 2 +- src/js/test/spec/interop.spec.ts | 113 +++++++++++------- src/js/test/spec/serialization.spec.ts | 8 +- 41 files changed, 547 insertions(+), 398 deletions(-) create mode 100644 src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs rename src/js/test/cs/Test.Types/Interfaces/{ExportedStatic.cs => ExportedModule.cs} (73%) rename src/js/test/cs/Test.Types/Interfaces/{IExportedStatic.cs => IExportedModule.cs} (88%) create mode 100644 src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs rename src/js/test/cs/Test.Types/Interfaces/{IImportedStatic.cs => IImportedModule.cs} (88%) create mode 100644 src/js/test/cs/Test.Types/Registries/IRegistry.cs create mode 100644 src/js/test/cs/Test.Types/Registries/IRegistryProvider.cs rename src/js/test/cs/Test.Types/{Vehicle/Registry.cs => Registries/Registries.cs} (73%) rename src/js/test/cs/Test.Types/{Vehicle => Registries}/TrackType.cs (100%) rename src/js/test/cs/Test.Types/{Vehicle => Registries}/Tracked.cs (69%) rename src/js/test/cs/Test.Types/{Vehicle => Registries}/Vehicle.cs (83%) rename src/js/test/cs/Test.Types/{Vehicle => Registries}/Wheeled.cs (67%) delete mode 100644 src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs index ce1edf86..201150f7 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs @@ -37,11 +37,7 @@ public class JSImported (global::System.Int32 id) : global::IImported { internal readonly global::System.Int32 _id = id; - ~JSImported() - { - global::Bootsharp.Instances.DisposeImported(_id); - global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - } + ~JSImported() => Instances.DisposeImported(_id); public event global::System.Action OnRecordChanged; internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); @@ -103,4 +99,54 @@ public class Class Execute(); DoesNotContain("Foo"); } + + [Fact] + public void GeneratesSpecializedExportsForInstancesWithEvents () + { + AddAssembly(With( + """ + public record Record; + + public interface IExported { event Action Changed; } + public interface IImported { event Action Changed; } + + public partial class Class + { + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; + } + """)); + Execute(); + Contains( + """ + internal static int Export (global::IExported instance) => Export(instance, static (_id, instance) => { + instance.Changed += HandleChanged; + return () => { + instance.Changed -= HandleChanged; + }; + + void HandleChanged (global::Record arg1, global::IExported arg2) => Interop.Exported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Instances.Export(arg2)); + }); + """); + } + + [Fact] + public void DoesNotGenerateDuplicateSpecializedExports () + { + AddAssembly(With( + """ + public interface IExported + { + event Action? Changed; + event Action? Done; + } + + public class Class + { + [Export] public static IExported GetExported () => default; + } + """)); + Execute(); + Once(@"internal static int Export \(global::IExported instance\)"); + } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index d73eef8c..dde22fa7 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -17,14 +17,6 @@ public static partial class Interop """); } - [Fact] - public void GeneratesDisposeInstanceBindings () - { - Execute(); - Contains("[JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id);"); - Contains("""[JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id);"""); - } - [Fact] public void GeneratesInitializersForEntryAndLibraryAssemblies () { @@ -153,21 +145,21 @@ public void GeneratesForMethodsInInstanced () """ public record Info (string Value); - public interface IExported { Info Inv (IExported inst, Info info); } - public interface IImported { Info Fun (IImported inst, Info info); } + public interface IExported { Info Inv (IExported it, Info info); } + public interface IImported { Info Fun (IImported it, Info info); } public partial class Class { - [Export] public static Task GetExported (IImported inst) => default; - [Import] public static Task GetImported (IExported inst) => default; + [Export] public static Task GetExported (IImported it) => default; + [Import] public static Task GetImported (IExported it) => default; } """)); Execute(); - Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExported_Inv (global::System.Int32 _id, global::System.Int32 inst, [JSMarshalAs] global::System.Int64 info) => Serializer.Serialize(Instances.Exported(_id).Inv(Instances.Exported(inst), Serializer.Deserialize(info, SerializerContext.Info)), SerializerContext.Info);"); - Contains("""[JSImport("Imported.funSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImported_Fun_Serialized (global::System.Int32 _id, global::System.Int32 inst, [JSMarshalAs] global::System.Int64 info);"""); - Contains("public static global::Info Bootsharp_Generated_Imports_JSImported_Fun (global::System.Int32 _id, global::IImported inst, global::Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImported_Fun_Serialized(_id, ((global::Bootsharp.Generated.Imports.JSImported)inst)._id, Serializer.Serialize(info, SerializerContext.Info)), SerializerContext.Info);"); - Contains("[JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (global::System.Int32 inst) => Instances.Export(await global::Class.GetExported(Instances.Import(inst, static id => new global::Bootsharp.Generated.Imports.JSImported(id))));"); - Contains("""[JSImport("Class.getImportedSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Class_GetImported_Serialized (global::System.Int32 inst);"""); + Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExported_Inv (global::System.Int32 _id, global::System.Int32 it, [JSMarshalAs] global::System.Int64 info) => Serializer.Serialize(Instances.Exported(_id).Inv(Instances.Exported(it), Serializer.Deserialize(info, SerializerContext.Info)), SerializerContext.Info);"); + Contains("""[JSImport("Imported.funSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImported_Fun_Serialized (global::System.Int32 _id, global::System.Int32 it, [JSMarshalAs] global::System.Int64 info);"""); + Contains("public static global::Info Bootsharp_Generated_Imports_JSImported_Fun (global::System.Int32 _id, global::IImported it, global::Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImported_Fun_Serialized(_id, ((global::Bootsharp.Generated.Imports.JSImported)it)._id, Serializer.Serialize(info, SerializerContext.Info)), SerializerContext.Info);"); + Contains("[JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (global::System.Int32 it) => Instances.Export(await global::Class.GetExported(Instances.Import(it, static id => new global::Bootsharp.Generated.Imports.JSImported(id))));"); + Contains("""[JSImport("Class.getImportedSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Class_GetImported_Serialized (global::System.Int32 it);"""); } [Fact] @@ -291,22 +283,11 @@ public interface IImported { event Action Changed; } public partial class Class { - [Export] public static IExported GetExported (IImported inst) => default; - [Import] public static IImported GetImported (IExported inst) => default; + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; } """)); Execute(); - Contains( - """ - private static int Register (global::IExported instance) => Instances.Export(instance, static (_id, instance) => { - instance.Changed += HandleChanged; - return () => { - instance.Changed -= HandleChanged; - }; - - void HandleChanged (global::Record arg1, global::IExported arg2) => Exported_BroadcastChanged_Serialized(_id, Serializer.Serialize(arg1, SerializerContext.Record), Register(arg2)); - }); - """); Contains("""[JSImport("Exported.broadcastChangedSerialized", "Bootsharp")] internal static partial void Exported_BroadcastChanged_Serialized (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2);"""); Contains("[JSExport] internal static void Bootsharp_Generated_Imports_JSImported_InvokeChanged (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2) => Instances.Import(_id, static id => new global::Bootsharp.Generated.Imports.JSImported(id)).InvokeChanged(Serializer.Deserialize(arg1, SerializerContext.Record), Instances.Import(arg2, static id => new global::Bootsharp.Generated.Imports.JSImported(id)));"); } @@ -347,26 +328,6 @@ public class Class DoesNotContain("SetPropertyItem"); } - [Fact] - public void DoesNotEmitDuplicateModuleRegistrations () - { - AddAssembly(With( - """ - public interface IExported - { - event Action? Changed; - event Action? Done; - } - - public class Class - { - [Export] public static IExported GetExported () => default; - } - """)); - Execute(); - Once(@"private static int Register \(global::IExported instance\)"); - } - [Fact] public void IgnoresImplementedInterfaceMethods () { diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 3303da12..6505a439 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -436,12 +436,12 @@ public class Class } """)); Execute(); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); } [Fact] @@ -467,7 +467,7 @@ public class Class } [Fact] - public void DoesntSerializeInstancedThemselves () + public void DoesntSerializeTopLevelInstances () { AddAssembly(With( """ @@ -490,6 +490,51 @@ public class Class DoesNotContain("Binary<"); } + [Fact] + public void SerializesInstancesUnderSerialized () + { + AddAssembly(With( + """ + public record Data (IExported Foo); + + public interface IExported { void Inv (); } + + public class Class + { + [Export] public static Data? GetData () => default; + } + """)); + Execute(); + Contains("Binary"); + } + + [Fact] + public void SerializesAllTheCrawledSerializableTypes () + { + AddAssembly( + With("y", "public enum Enum { A, B }"), + With("y", "public record Struct (double A, ReadonlyStruct[]? B);"), + With("y", "public record ReadonlyStruct (Enum e);"), + With("n", "public struct Struct { public y.Struct S { get; set; } public ReadonlyStruct[]? A { get; set; } }"), + With("n", "public readonly struct ReadonlyStruct { public double A { get; init; } }"), + With("n", "public readonly record struct ReadonlyRecordStruct(double A);"), + With("n", "public record class RecordClass(double A);"), + With("n", "public enum Enum { A, B }"), + With("n", "public record Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"), + WithClass("n", "public record Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"), + With("n", "public record Baz { public List Bars { get; } }"), + WithClass("n", "[Export] public static Task GetBaz (Enum e) => default;")); + Execute(); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + Contains("Binary"); + } + [Fact] public void DoesntGenerateDuplicateSerializers () { @@ -525,31 +570,4 @@ public class Class Once("Binary>"); Once("Binary>"); } - - [Fact] - public void SerializesAllTheCrawledSerializableTypes () - { - AddAssembly( - With("y", "public enum Enum { A, B }"), - With("y", "public record Struct (double A, ReadonlyStruct[]? B);"), - With("y", "public record ReadonlyStruct (Enum e);"), - With("n", "public struct Struct { public y.Struct S { get; set; } public ReadonlyStruct[]? A { get; set; } }"), - With("n", "public readonly struct ReadonlyStruct { public double A { get; init; } }"), - With("n", "public readonly record struct ReadonlyRecordStruct(double A);"), - With("n", "public record class RecordClass(double A);"), - With("n", "public enum Enum { A, B }"), - With("n", "public record Foo { public Struct S { get; } public ReadonlyStruct Rs { get; } }"), - WithClass("n", "public record Bar : Foo { public ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } }"), - With("n", "public record Baz { public List Bars { get; } }"), - WithClass("n", "[Export] public static Task GetBaz (Enum e) => default;")); - Execute(); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - Contains("Binary "); - } } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index f8f401cc..8826c13a 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -26,7 +26,7 @@ public partial class Class { [Import] public static event Action? Evt; [Export] public static Task InvAsync () => Task.FromResult(0); - [Export] public static void UseImported (IImportedInstanced inst) {} + [Export] public static void UseImported (IImportedInstanced it) {} [Import] public static void Fun () {} } """)); @@ -629,13 +629,13 @@ public void GeneratesForMethodsInInstanced () """ public record Info (string Value); - public interface IExported { Info Inv (IExported inst, Info info); } - public interface IImported { Info Fun (IImported inst, Info info); } + public interface IExported { Info Inv (IExported it, Info info); } + public interface IImported { Info Fun (IImported it, Info info); } public partial class Class { - [Export] public static Task GetExported (IImported inst) => default; - [Import] public static Task GetImported (IExported inst) => default; + [Export] public static Task GetExported (IImported it) => default; + [Import] public static Task GetImported (IExported it) => default; } """)); Execute(); @@ -643,20 +643,20 @@ public partial class Class """ class JSExported { constructor(_id) { this._id = _id; } - inv(inst, info) { return Exported.inv(this._id, inst, info); } + inv(it, info) { return Exported.inv(this._id, it, info); } } export const Class = { - getExported: async (inst) => instances.export(await exports.Class_GetExported(instances.import(inst)), id => new JSExported(id)), + getExported: async (it) => instances.export(await exports.Class_GetExported(instances.import(it)), id => new JSExported(id)), get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = async (inst) => instances.import(await this.getImportedHandler(instances.export(inst, id => new JSExported(id)))); }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = async (it) => instances.import(await this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, get getImportedSerialized() { return this.getImportedSerializedHandler; } }; export const Exported = { - inv: (_id, inst, info) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_Inv(_id, inst._id, serialize(info, Info)), Info) + inv: (_id, it, info) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_Inv(_id, it._id, serialize(info, Info)), Info) }; export const Imported = { - funSerialized: (_id, inst, info) => serialize(instances.imported(_id).fun(instances.imported(inst), deserialize(info, Info)), Info) + funSerialized: (_id, it, info) => serialize(instances.imported(_id).fun(instances.imported(it), deserialize(info, Info)), Info) }; """); } @@ -729,8 +729,8 @@ public interface IImported public partial class Class { - [Export] public static IExported GetExported (IImported inst) => default; - [Import] public static IImported GetImported (IExported inst) => default; + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; } """)); Execute(); @@ -745,9 +745,9 @@ class JSExported { } export const Class = { - getExported: (inst) => instances.export(exports.Class_GetExported(instances.import(inst)), id => new JSExported(id)), + getExported: (it) => instances.export(exports.Class_GetExported(instances.import(it)), id => new JSExported(id)), get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => instances.import(this.getImportedHandler(instances.export(inst, id => new JSExported(id)))); }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => instances.import(this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, get getImportedSerialized() { return this.getImportedSerializedHandler; } }; export const Exported = { @@ -807,21 +807,21 @@ public interface IImported { event Action? Changed; } public partial class Class { - [Export] public static IExported GetExported (IImported inst) => default; - [Import] public static IImported GetImported (IExported inst) => default; + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; } """)); Execute(); Contains( """ - function register_IImported(instance) { + function import_IImported(instance) { return instances.import(instance, _id => { instance.changed.subscribe(handleChanged); return () => { instance.changed.unsubscribe(handleChanged); }; - function handleChanged(arg1, arg2) { exports.Bootsharp_Generated_Imports_JSImported_InvokeChanged(_id, register_IImported(arg1), serialize(arg2, Info)); } + function handleChanged(arg1, arg2) { exports.Bootsharp_Generated_Imports_JSImported_InvokeChanged(_id, import_IImported(arg1), serialize(arg2, Info)); } }); } """); @@ -834,9 +834,9 @@ class JSExported { } export const Class = { - getExported: (inst) => instances.export(exports.Class_GetExported(register_IImported(inst)), id => new JSExported(id)), + getExported: (it) => instances.export(exports.Class_GetExported(import_IImported(it)), id => new JSExported(id)), get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => register_IImported(this.getImportedHandler(instances.export(inst, id => new JSExported(id)))); }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (it) => import_IImported(this.getImportedHandler(instances.export(it, id => new JSExported(id)))); }, get getImportedSerialized() { return this.getImportedSerializedHandler; } }; export const Exported = { @@ -862,7 +862,7 @@ [Export] public static void UseImported (IImported instance) {} } """)); Execute(); - Once("function register_IImported"); + Once("function import_IImported"); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 85f5eb09..d86429e9 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -846,8 +846,8 @@ public interface IImported { Info Fun (Info info, string str); } public class Class { - [Export] public static Task GetExported (IImported inst) => default; - [Import] public static Task GetImported (IExported inst) => default; + [Export] public static Task GetExported (IImported it) => default; + [Import] public static Task GetImported (IExported it) => default; } """)); Execute(); @@ -865,8 +865,8 @@ export interface IExported { }>; export namespace Class { - export function getExported(inst: IImported): Promise; - export let getImported: (inst: IExported) => Promise; + export function getExported(it: IImported): Promise; + export let getImported: (it: IExported) => Promise; } """); } @@ -947,8 +947,8 @@ public interface IImported public class Class { - [Export] public static IExported GetExported (IImported inst) => default; - [Import] public static IImported GetImported (IExported inst) => default; + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; } """)); Execute(); @@ -969,8 +969,8 @@ export interface IExported { }>; export namespace Class { - export function getExported(inst: IImported): IExported; - export let getImported: (inst: IExported) => IImported; + export function getExported(it: IImported): IExported; + export let getImported: (it: IExported) => IImported; } """); } @@ -1023,8 +1023,8 @@ public interface IImported { event Action? Changed; } public class Class { - [Export] public static IExported GetExported (IImported inst) => default; - [Import] public static IImported GetImported (IExported inst) => default; + [Export] public static IExported GetExported (IImported it) => default; + [Import] public static IImported GetImported (IExported it) => default; } """)); Execute(); @@ -1042,8 +1042,8 @@ export interface IExported { }>; export namespace Class { - export function getExported(inst: IImported): IExported; - export let getImported: (inst: IExported) => IImported; + export function getExported(it: IImported): IExported; + export let getImported: (it: IExported) => IImported; } """); } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 0ed64fe0..2c817849 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -160,4 +160,49 @@ public static string TrimGeneric (string typeName) if (delimiterIndex < 0) return typeName; return typeName[..delimiterIndex]; } + + public static string Export (ArgumentMeta arg) => Export(arg.Value, arg.Name); + public static string Export (ValueMeta value, string exp) => Export(value.Type, exp); + public static string Export (TypeMeta type, string exp) + { + if (type is InstancedMeta it) + if (it.Interop == InteropKind.Export) return $"Instances.Export({exp})"; + else return $"((global::{it.FullName}){exp})._id"; + if (type is SerializedMeta sm) return $"Serializer.Serialize({exp}, SerializerContext.{sm.Id})"; + return exp; + } + + public static string Import (ArgumentMeta arg) => Import(arg.Value, arg.Name); + public static string Import (ValueMeta value, string exp) => Import(value.Type, exp); + public static string Import (TypeMeta type, string exp) + { + if (type is InstancedMeta it) + if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Syntax}>({exp})"; + else return $"Instances.Import({exp}, static id => new global::{it.FullName}(id))"; + if (type is SerializedMeta sm) return $"Serializer.Deserialize({exp}, SerializerContext.{sm.Id})"; + return exp; + } + + public static string ExportJS (ArgumentMeta arg) => ExportJS(arg.Value, arg.JSName); + public static string ExportJS (ValueMeta value, string exp) => ExportJS(value.Type, exp); + public static string ExportJS (TypeMeta type, string exp) + { + if (type is InstancedMeta it) + if (it.Interop == InteropKind.Export) return $"{exp}._id"; + else if (it.Importer is { } importer) return $"{importer}({exp})"; + else return $"instances.import({exp})"; + if (type is SerializedMeta sm) return $"serialize({exp}, {sm.Id})"; + return exp; + } + + public static string ImportJS (ArgumentMeta arg) => ImportJS(arg.Value, arg.JSName); + public static string ImportJS (ValueMeta value, string exp) => ImportJS(value.Type, exp); + public static string ImportJS (TypeMeta type, string exp) + { + if (type is InstancedMeta it) + if (it.Interop == InteropKind.Import) return $"instances.imported({exp})"; + else return $"instances.export({exp}, id => new {it.JSName}(id))"; + if (type is SerializedMeta sm) return $"deserialize({exp}, {sm.Id})"; + return exp; + } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index 75c91a0e..6a17a26d 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -31,7 +31,9 @@ public IReadOnlyCollection Collect () Namespace = BuildSpace(type, ik), Name = BuildName(type), JSName = BuildJSName(type), - Members = new List() + Members = new List(), + Exporter = BuildExporter(type, ik), + Importer = BuildImporter(type, ik) }; private InstancedMeta CollectMembers (InstancedMeta it) @@ -79,4 +81,16 @@ private string BuildJSName (Type type) if (type.Namespace == null) return name; return $"{type.Namespace}.{name}".Replace(".", "_"); } + + private string? BuildExporter (Type type, InteropKind ik) + { + if (ik != InteropKind.Export || type.GetEvents().Length == 0) return null; + return "Export"; // we're using method overloads instead of unique names + } + + private string? BuildImporter (Type type, InteropKind ik) + { + if (ik != InteropKind.Import || type.GetEvents().Length == 0) return null; + return $"import_{type.FullName!.Replace('.', '_').Replace('+', '_')}"; + } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs index 0b5e2935..42d1d09c 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs @@ -45,8 +45,7 @@ internal sealed class MemberInspector (Preferences prefs, InspectType inspect) private ValueMeta CreateValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { Type = inspect(type, ik), TypeSyntax = BuildSyntax(type, nil), - Nullable = IsNullable(type, nil), - Nullity = nil + Nullable = IsNullable(type, nil) }; private string BuildJSSpace (Type decl) diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs index 92939fd2..46aabb9a 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs @@ -58,7 +58,7 @@ private SerializedMeta Build (Type type) type.IsArray ? new SerializedArrayMeta(type, Build(type.GetElementType()!)) : IsList(type, out var element) ? new SerializedListMeta(type, Build(element)) : IsDictionary(type, out var k, out var v) ? new SerializedDictionaryMeta(type, Build(k), Build(v)) : - IsInstancedType(type) ? new SerializedInstanceMeta(itd.Inspect(type, ik)!.Clr) : + IsInstancedType(type) ? new SerializedInstanceMeta(itd.Inspect(type, ik)!) : BuildObject(type); cycle.Remove(type); return meta; @@ -167,8 +167,8 @@ private static IReadOnlyList OrderByDependencyGraph (IEnumerable static int GetInitOrder (SerializedMeta meta) => meta switch { SerializedPrimitiveMeta or SerializedEnumMeta => 0, - SerializedObjectMeta => 2, - _ => 3 + SerializedInstanceMeta or SerializedObjectMeta => 1, + _ => 2 }; static IEnumerable GetInitDependencies (SerializedMeta meta) diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs index 9e691e49..c8558210 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs @@ -29,4 +29,12 @@ internal record InstancedMeta (Type Clr) : TypeMeta(Clr) /// Members declared on the instance. /// public required IReadOnlyCollection Members { get; init; } + /// + /// Name of the specialized C# exporter method or null when is sufficient. + /// + public string? Exporter { get; init; } + /// + /// Name of the specialized JS importer function or null when is sufficient. + /// + public string? Importer { get; init; } } diff --git a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs index 09b6a987..c46b18e8 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs @@ -50,9 +50,10 @@ internal sealed record SerializedDictionaryMeta (Type Clr, SerializedMeta Key, SerializedMeta Value) : SerializedMeta(Clr); /// -/// Describes an instance reference () under a serialized object. +/// Describes an instance reference under a serialized object. /// -internal sealed record SerializedInstanceMeta (Type Clr) : SerializedMeta(Clr); +/// The associated instanced type info. +internal sealed record SerializedInstanceMeta (InstancedMeta Instance) : SerializedMeta(Instance.Clr); /// /// Describes a serialized user-defined object, such as a record or a struct. diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index 0017bdcd..c02b1433 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Reflection; namespace Bootsharp.Publish; @@ -22,10 +21,6 @@ internal sealed record ValueMeta /// public required bool Nullable { get; init; } /// - /// Nullability context of the value. - /// - public required NullabilityInfo Nullity { get; init; } - /// /// Serialization info when , null otherwise. /// public SerializedMeta? Serialized => Type as SerializedMeta; diff --git a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs index b0011933..b826f237 100644 --- a/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -1,21 +1,64 @@ namespace Bootsharp.Publish; /// -/// Generates interop wrappers for imported instances. +/// Generates binding wrappers for imported instances and instance-specific export handlers. /// internal sealed class InstanceGenerator { private InstancedMeta it = null!; public string Generate (SolutionInspection spec) => - $""" - #nullable enable - #pragma warning disable + $$""" + #nullable enable + #pragma warning disable + + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices.JavaScript; + + namespace Bootsharp.Generated + { + public static partial class Instances + { + internal static int Export (T instance, global::System.Func? factory = null) where T : class => global::Bootsharp.Instances.Export(instance, factory); + internal static T Exported (int id) where T : class => global::Bootsharp.Instances.Exported(id); + internal static T Import (int id, global::System.Func factory) where T : class => global::Bootsharp.Instances.Import(id, factory); + + internal static void DisposeImported (int id) + { + NotifyImportedDisposed(id); + global::Bootsharp.Instances.DisposeImported(id); + } + + {{Fmt(spec.Instanced.Where(i => i.Exporter != null).Select(EmitExporter), 2, "\n\n")}} + + [JSExport] private static void DisposeExported (int id) => global::Bootsharp.Instances.DisposeExported(id); + [JSImport("instances.disposeImported", "Bootsharp")] private static partial void NotifyImportedDisposed (int id); + } + } + + {{Fmt(spec.Instanced.Where(i => i.Interop == InteropKind.Import).Select(EmitWrapper), 0, "\n\n")}} + """; - {Fmt(spec.Instanced - .Where(i => i.Interop == InteropKind.Import) - .Select(EmitWrapper), 0, "\n\n")} - """; + private static string EmitExporter (InstancedMeta it) + { + var evt = it.Members.OfType().ToArray(); + return + $$""" + internal static int {{it.Exporter}} ({{it.Syntax}} instance) => Export(instance, static (_id, instance) => { + {{Fmt(evt.Select(e => $"instance.{e.Name} += Handle{e.Name};"))}} + return () => { + {{Fmt(evt.Select(e => $"instance.{e.Name} -= Handle{e.Name};"), 2)}} + }; + + {{Fmt(evt.Select(e => { + var args = string.Join(", ", e.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(Export))); + var name = $"{e.JSSpace.Replace('.', '_')}_Broadcast{e.Name}_Serialized"; + return $"void Handle{e.Name} ({args}) => Interop.{name}({invArgs});"; + }))}} + }); + """; + } private string EmitWrapper (InstancedMeta it) => $$""" @@ -25,11 +68,7 @@ public class {{it.Name}} (global::System.Int32 id) : {{it.Syntax}} { internal readonly global::System.Int32 _id = id; - ~{{it.Name}}() - { - global::Bootsharp.Instances.DisposeImported(_id); - global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - } + ~{{it.Name}}() => Instances.DisposeImported(_id); {{Fmt(it.Members.Select(EmitMemberImport), 2)}} } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 397119d7..af078795 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -12,7 +12,6 @@ internal sealed class InteropGenerator [MemberNotNullWhen(true, nameof(md))] private bool isMd => md != null; - private readonly HashSet registered = []; private string id = null!, space = null!; private InstancedMeta? it; private ModuleMeta? md; @@ -29,22 +28,19 @@ namespace Bootsharp.Generated; public static partial class Interop { - [JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id); - [JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); - [ModuleInitializer] internal static unsafe void Initialize () { {{Fmt([ ..spec.Static.OfType() .Where(e => e.Interop == InteropKind.Export) - .Select(e => EmitEventSubscription(e, e.Space)), + .Select(e => EmitStaticEventSubscription(e, e.Space)), ..spec.Modules.SelectMany(md => md.Members.OfType() .Where(e => e.Interop == InteropKind.Export) - .Select(e => EmitEventSubscription(e, md.FullName))), + .Select(e => EmitStaticEventSubscription(e, md.FullName))), ..spec.Static.OfType() .Where(m => m.Interop == InteropKind.Import) - .Select(EmitMethodAssignment) + .Select(EmitStaticMethodAssignment) ], 2)}} } @@ -54,13 +50,13 @@ internal static unsafe void Initialize () } """; - private static string EmitEventSubscription (EventMeta evt, string space) + private static string EmitStaticEventSubscription (EventMeta evt, string space) { var handler = $"Handle_{space.Replace('.', '_')}_{evt.Name}"; return $"global::{space}.{evt.Name} += {handler};"; } - private static string EmitMethodAssignment (MethodMeta method) + private static string EmitStaticMethodAssignment (MethodMeta method) { var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; @@ -90,11 +86,10 @@ private static string EmitMethodAssignment (MethodMeta method) if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; yield return $"{attr}internal static partial void {name} ({args});"; - if (isIt) yield return EmitInstanceRegistrar(it); - if (isIt) yield break; // instanced export event handlers are emitted in the registrar + if (isIt) yield break; // instanced export event handlers are emitted by InstanceGenerator var handler = $"Handle_{id}_{evt.Name}"; var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); + var invArgs = string.Join(", ", evt.Arguments.Select(Export)); yield return $"private static void {handler} ({sigArgs}) => {name}({invArgs});"; } @@ -106,7 +101,7 @@ private IEnumerable EmitEventImport (EventMeta evt) var invName = isIt ? $"Instances.Import(_id, static id => new global::{it.FullName}(id)).Invoke{evt.Name}" : isMd ? $"((global::{md.FullName})Modules.Imports[typeof({md.Syntax})].Instance).Invoke{evt.Name}" : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; - var invArgs = string.Join(", ", evt.Arguments.Select(Deserialize)); + var invArgs = string.Join(", ", evt.Arguments.Select(Import)); yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; } @@ -117,7 +112,7 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) var attr = $"[JSExport] {MarshalAmbiguous(prop.GetValue, true)}"; var name = $"{id}_GetProperty{prop.Name}"; var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; - var body = Serialize(prop.GetValue, isIt + var body = Export(prop.GetValue, isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name}" : $"global::{space}.GetProperty{prop.Name}()"); yield return $"{attr}internal static {BuildValueSyntax(prop.GetValue)} {name} ({args}) => {body};"; @@ -127,7 +122,7 @@ private IEnumerable EmitPropertyExport (PropertyMeta prop) var name = $"{id}_SetProperty{prop.Name}"; var args = BuildParameter(prop.SetValue, "value"); if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Deserialize(prop.SetValue, "value"); + var value = Import(prop.SetValue, "value"); var body = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" : $"global::{space}.SetProperty{prop.Name}({value})"; @@ -146,7 +141,7 @@ private IEnumerable EmitPropertyImport (PropertyMeta prop) yield return $"{attr}internal static partial {BuildValueSyntax(prop.GetValue)} {serdeName} ({args});"; var name = $"{id}_GetProperty{prop.Name}"; - var body = Deserialize(prop.GetValue, isIt ? $"{serdeName}(_id)" : $"{serdeName}()"); + var body = Import(prop.GetValue, isIt ? $"{serdeName}(_id)" : $"{serdeName}()"); yield return $"public static {prop.GetValue.TypeSyntax} {name}({args}) => {body};"; } if (prop.CanSet) @@ -160,7 +155,7 @@ private IEnumerable EmitPropertyImport (PropertyMeta prop) var name = $"{id}_SetProperty{prop.Name}"; var args = $"{prop.SetValue.TypeSyntax} value"; if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; - var value = Serialize(prop.SetValue, "value"); + var value = Export(prop.SetValue, "value"); var body = isIt ? $"{serdeName}(_id, {value})" : $"{serdeName}({value})"; yield return $"public static void {name}({args}) => {body};"; } @@ -175,11 +170,11 @@ private IEnumerable EmitMethodExport (MethodMeta method) if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; - var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); + var invArgs = string.Join(", ", method.Arguments.Select(Import)); var invName = isIt ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" : $"global::{space}.{method.Name}"; - var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var body = Export(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; } @@ -198,60 +193,18 @@ private IEnumerable EmitMethodImport (MethodMeta method) @return = $"{(wait ? "async " : "")}{method.Return.TypeSyntax}"; var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; - var invArgs = string.Join(", ", method.Arguments.Select(Serialize)); + var invArgs = string.Join(", ", method.Arguments.Select(Export)); if (isIt) invArgs = PrependIdArg(invArgs); - var body = Deserialize(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); + var body = Import(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); yield return $"public static {@return} {name} ({sigArgs}) => {body};"; } - private string? EmitInstanceRegistrar (InstancedMeta it) - { - if (!registered.Add(it)) return null; - var events = it.Members.OfType().ToArray(); - return - $$""" - private static int Register ({{it.Syntax}} instance) => Instances.Export(instance, static (_id, instance) => { - {{Fmt(events.Select(e => $"instance.{e.Name} += Handle{e.Name};"))}} - return () => { - {{Fmt(events.Select(e => $"instance.{e.Name} -= Handle{e.Name};"), 2)}} - }; - - {{Fmt(events.Select(e => { - var args = string.Join(", ", e.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(Serialize))); - var name = $"{e.JSSpace.Replace('.', '_')}_Broadcast{e.Name}_Serialized"; - return $"void Handle{e.Name} ({args}) => {name}({invArgs});"; - }))}} - }); - """; - } - private string BuildParameter (ValueMeta value, string name) { var type = BuildValueSyntax(value); return $"{MarshalAmbiguous(value, false)}{type} {name}"; } - private string Serialize (ArgumentMeta arg) => Serialize(arg.Value, arg.Name); - private string Serialize (ValueMeta value, string exp) - { - if (value.IsInstanced) return RegisterInstance(value.Instanced, exp); - if (Serialized(value, out var id)) return $"Serializer.Serialize({exp}, {id})"; - return exp; - } - - private string Deserialize (ArgumentMeta arg) => Deserialize(arg.Value, arg.Name); - private string Deserialize (ValueMeta value, string exp) - { - if (value.Instanced is { } it) - { - if (it.Interop == InteropKind.Export) return $"Instances.Exported<{it.Syntax}>({exp})"; - return $"Instances.Import({exp}, static id => new global::{it.FullName}(id))"; - } - if (Serialized(value, out var id)) return $"Serializer.Deserialize({exp}, {id})"; - return exp; - } - private string BuildValueSyntax (ValueMeta value) { var nil = value.Nullable && !value.IsSerialized ? "?" : ""; @@ -276,20 +229,6 @@ private static string MarshalAmbiguous (ValueMeta value, bool @return) return $"[JSMarshalAs<{result}>] "; } - private static bool Serialized (ValueMeta meta, [NotNullWhen(true)] out string? id) - { - if (!meta.IsSerialized) id = null; - else id = $"SerializerContext.{meta.Serialized.Id}"; - return id != null; - } - - private string RegisterInstance (InstancedMeta it, string exp) - { - if (it.Interop == InteropKind.Import) return $"((global::{it.FullName}){exp})._id"; - if (it.Members.OfType().Any()) return $"Register({exp})"; - return $"Instances.Export({exp})"; - } - private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index 516cb6ec..31b6e4a1 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -28,7 +28,7 @@ private string EmitFactory (SerializedMeta meta) SerializedArrayMeta arr => $"Serializer.Array({arr.Element.Id})", SerializedListMeta list => $"Serializer.{TrimGeneric(list.Clr.Name)}({list.Element.Id})", SerializedDictionaryMeta dic => $"Serializer.{TrimGeneric(dic.Clr.Name)}({dic.Key.Id}, {dic.Value.Id})", - SerializedObjectMeta => $"new(Write_{meta.Id}, Read_{meta.Id})", + SerializedObjectMeta or SerializedInstanceMeta => $"new(Write_{meta.Id}, Read_{meta.Id})", _ => ResolvePrimitive(meta.Clr) }};"; @@ -42,21 +42,38 @@ static string ResolvePrimitive (Type type) private IEnumerable EmitHelpers (SerializedMeta meta) { - if (meta is not SerializedObjectMeta obj) yield break; - yield return $$""" - private static void Write_{{obj.Id}} (ref Writer writer, {{obj.Syntax}} value) - { - {{Fmt(EmitObjectWrite(obj))}} - } - """; - yield return $$""" - private static {{obj.Syntax}} Read_{{obj.Id}} (ref Reader reader) - { - {{Fmt(EmitObjectRead(obj))}} - } - """; - foreach (var prop in obj.Properties.Where(p => p.Kind == SerializedPropertyKind.Field)) - yield return EmitFieldAccessor(obj, prop); + if (meta is SerializedInstanceMeta it) + { + yield return + $$""" + private static void Write_{{it.Id}} (ref Writer writer, {{it.Syntax}} value) + { + writer.WriteInt32({{Export(it.Instance, "value")}}); + } + + private static {{it.Syntax}} Read_{{it.Id}} (ref Reader reader) + { + return {{Import(it.Instance, "reader.ReadInt32()")}}; + } + """; + } + if (meta is SerializedObjectMeta obj) + { + yield return + $$""" + private static void Write_{{obj.Id}} (ref Writer writer, {{obj.Syntax}} value) + { + {{Fmt(EmitObjectWrite(obj))}} + } + + private static {{obj.Syntax}} Read_{{obj.Id}} (ref Reader reader) + { + {{Fmt(EmitObjectRead(obj))}} + } + """; + foreach (var prop in obj.Properties.Where(p => p.Kind == SerializedPropertyKind.Field)) + yield return EmitFieldAccessor(obj, prop); + } } private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 25b16cd9..496b74cc 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -48,9 +48,8 @@ public string Generate (SolutionInspection spec) bld.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); bld.Append("\n\n"); - foreach (var instance in spec.Instanced - .Where(i => i.Interop == InteropKind.Import && i.Members.OfType().Any())) - EmitInstanceRegistrar(instance); + foreach (var it in spec.Instanced.Where(i => i.Importer != null)) + EmitImporter(it); bld.Append("\n\n"); if (spec.Instanced.Count > 0) @@ -183,7 +182,7 @@ private void EmitEventExport (EventMeta evt) var invArgs = string.Join(", ", evt.Arguments.Select(arg => // By default, we use 'null' for missing collection items, but here the event args array // represents args specified to the event's 'broadcast' function, so user expects 'undefined'. - $"{Deserialize(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); + $"{ImportJS(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); if (isIt) { var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; @@ -203,7 +202,7 @@ private void EmitEventImport (EventMeta evt) var name = $"{id}_Invoke{evt.Name}"; var invName = debug ? $"""getExport("{name}")""" : $"exports.{name}"; var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); - var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); + var invArgs = string.Join(", ", evt.Arguments.Select(ExportJS)); bld.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); } @@ -213,7 +212,7 @@ private void EmitPropertyExport (PropertyMeta prop) { var fnName = $"{id}_GetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var body = Deserialize(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); + var body = ImportJS(prop.GetValue, isIt ? $"{invName}(_id)" : $"{invName}()"); if (prop.GetValue.Nullable && !prop.GetValue.IsInstanced) body += " ?? undefined"; if (isIt) bld.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); else bld.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); @@ -222,7 +221,7 @@ private void EmitPropertyExport (PropertyMeta prop) { var fnName = $"{id}_SetProperty{prop.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var value = Serialize(prop.SetValue, "value"); + var value = ExportJS(prop.SetValue, "value"); var body = isIt ? $"{invName}(_id, {value})" : $"{invName}({value})"; if (isIt) bld.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); else bld.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); @@ -235,13 +234,13 @@ private void EmitPropertyImport (PropertyMeta prop) { if (!isIt) bld.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); var args = isIt ? "_id" : ""; - var body = Serialize(prop.GetValue, isIt ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}"); + var body = ExportJS(prop.GetValue, isIt ? $"instances.imported(_id).{prop.JSName}" : $"this.{prop.JSName}"); bld.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); } if (prop.CanSet) { if (!isIt) bld.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); - var value = Deserialize(prop.SetValue, "value"); + var value = ImportJS(prop.SetValue, "value"); var args = isIt ? "_id, value" : "value"; var body = isIt ? $"instances.imported(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; bld.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); @@ -255,9 +254,9 @@ private void EmitMethodExport (MethodMeta method) var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); - var invArgs = string.Join(", ", method.Arguments.Select(Serialize)); + var invArgs = string.Join(", ", method.Arguments.Select(ExportJS)); if (isIt) invArgs = PrependIdArg(invArgs); - var body = Deserialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var body = ImportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); bld.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); } @@ -267,9 +266,9 @@ private void EmitMethodImport (MethodMeta method) var name = method.JSName; var args = string.Join(", ", method.Arguments.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); - var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); + var invArgs = string.Join(", ", method.Arguments.Select(ImportJS)); var invName = isIt ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; - var body = Serialize(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + var body = ExportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; if (isIt) bld.Append($"{Br}{name}Serialized: {serdeHandler}"); else @@ -291,23 +290,23 @@ private void EmitEnum (Type @enum) bld.Append($"{Br}{@enum.Name}: {{ {fields} }}"); } - private void EmitInstanceRegistrar (InstancedMeta instance) + private void EmitImporter (InstancedMeta it) { - var events = instance.Members.OfType().ToArray(); + var evt = it.Members.OfType().ToArray(); bld.Append( $$""" - function {{BuildRegistrarName(instance)}}(instance) { + function {{it.Importer}}(instance) { return instances.import(instance, _id => { - {{Fmt(events.Select(e => $"instance.{e.JSName}.subscribe(handle{e.Name});"))}} + {{Fmt(evt.Select(e => $"instance.{e.JSName}.subscribe(handle{e.Name});"))}} return () => { - {{Fmt(events.Select(e => $"instance.{e.JSName}.unsubscribe(handle{e.Name});"), 2)}} + {{Fmt(evt.Select(e => $"instance.{e.JSName}.unsubscribe(handle{e.Name});"), 2)}} }; - {{Fmt(events.Select(e => { - var fnName = $"{instance.FullName.Replace('.', '_')}_Invoke{e.Name}"; + {{Fmt(evt.Select(e => { + var fnName = $"{it.FullName.Replace('.', '_')}_Invoke{e.Name}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", e.Arguments.Select(a => a.JSName)); - var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(Serialize))); + var invArgs = PrependIdArg(string.Join(", ", e.Arguments.Select(ExportJS))); return $"function handle{e.Name}({args}) {{ {invName}({invArgs}); }}"; }))}} }); @@ -316,38 +315,6 @@ private void EmitInstanceRegistrar (InstancedMeta instance) ); } - private string Serialize (ArgumentMeta arg) => Serialize(arg.Value, arg.JSName); - private string Serialize (ValueMeta value, string exp) - { - if (value.IsInstanced) return RegisterInstance(value.Instanced, exp); - if (value.IsSerialized) return $"serialize({exp}, {value.Serialized.Id})"; - return exp; - } - - private string Deserialize (ArgumentMeta arg) => Deserialize(arg.Value, arg.JSName); - private string Deserialize (ValueMeta value, string exp) - { - if (value.IsInstanced) - { - if (value.Instanced.Interop == InteropKind.Import) return $"instances.imported({exp})"; - return $"instances.export({exp}, id => new {value.Instanced.JSName}(id))"; - } - if (value.IsSerialized) return $"deserialize({exp}, {value.Serialized.Id})"; - return exp; - } - - private string RegisterInstance (InstancedMeta it, string exp) - { - if (it.Interop == InteropKind.Export) return $"{exp}._id"; - if (it.Members.OfType().Any()) return $"{BuildRegistrarName(it)}({exp})"; - return $"instances.import({exp})"; - } - - private static string BuildRegistrarName (InstancedMeta it) - { - return $"register_{it.Clr.FullName!.Replace('.', '_').Replace('+', '_')}"; - } - private bool ShouldWait (MethodMeta method) { if (!method.Async) return false; diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs index 93bc83ce..867f40fb 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs @@ -20,7 +20,7 @@ private string EmitFactory (SerializedMeta meta) SerializedArrayMeta arr => $"types.Array({arr.Element.Id})", SerializedListMeta list => $"types.List({list.Element.Id})", SerializedDictionaryMeta dic => $"types.Dictionary({dic.Key.Id}, {dic.Value.Id})", - SerializedObjectMeta => $"binary(write_{meta.Id}, read_{meta.Id})", + SerializedObjectMeta or SerializedInstanceMeta => $"binary(write_{meta.Id}, read_{meta.Id})", _ => ResolvePrimitive(meta.Clr) }};"; @@ -34,17 +34,32 @@ static string ResolvePrimitive (Type type) private IEnumerable EmitHelpers (SerializedMeta meta) { - if (meta is not SerializedObjectMeta obj) yield break; - yield return $$""" - function write_{{obj.Id}}(writer, value) { - {{Fmt(EmitObjectWrite(obj))}} - } - """; - yield return $$""" - function read_{{obj.Id}}(reader) { - {{Fmt(EmitObjectRead(obj))}} - } - """; + if (meta is SerializedInstanceMeta it) + { + yield return + $$""" + function write_{{it.Id}}(writer, value) { + writer.writeInt32({{ExportJS(it.Instance, "value")}}); + } + + function read_{{it.Id}}(reader) { + return {{ImportJS(it.Instance, "reader.readInt32()")}}; + } + """; + } + if (meta is SerializedObjectMeta obj) + { + yield return + $$""" + function write_{{obj.Id}}(writer, value) { + {{Fmt(EmitObjectWrite(obj))}} + } + + function read_{{obj.Id}}(reader) { + {{Fmt(EmitObjectRead(obj))}} + } + """; + } } private IEnumerable EmitObjectWrite (SerializedObjectMeta obj) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e0ae2e86..681b0b9f 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.165 + 0.8.0-alpha.170 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/src/exports.ts b/src/js/src/exports.ts index 9d2ed966..90524187 100644 --- a/src/js/src/exports.ts +++ b/src/js/src/exports.ts @@ -1,8 +1,9 @@ import type { RuntimeAPI } from "./modules"; -export let exports: unknown; +export let exports: Record; export async function bindExports(runtime: RuntimeAPI, assembly: string) { const asm = await runtime.getAssemblyExports(assembly); - exports = asm["Bootsharp"]?.["Generated"]["Interop"]; + exports = asm["Bootsharp"]["Generated"]["Interop"] ?? {}; + exports.disposeExported = asm["Bootsharp"]["Generated"]["Instances"].DisposeExported; } diff --git a/src/js/src/instances.ts b/src/js/src/instances.ts index 758c1da7..d04a7353 100644 --- a/src/js/src/instances.ts +++ b/src/js/src/instances.ts @@ -49,6 +49,6 @@ export const instances = { /* v8 ignore start -- @preserve */ // Uncoverable, as finalization in Node is not controllable. function finalizeExported(id: number) { exportedById.delete(id); - (<{ DisposeExportedInstance: (id: number) => void }>exports).DisposeExportedInstance(id); + (<{ disposeExported: (id: number) => void }>exports).disposeExported(id); } /* v8 ignore stop -- @preserve */ diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs new file mode 100644 index 00000000..5ab7b6ca --- /dev/null +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs @@ -0,0 +1,12 @@ +using System; + +namespace Test.Types; + +public class ExportedInnerInstanced +{ + public event Action OnCountChanged = delegate { }; + + public int Count { get; set => OnCountChanged(field = value); } + + public void Increment () => Count++; +} diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs index 24f22421..5f71ef67 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs @@ -8,6 +8,7 @@ public class ExportedInstanced (string instanceArg) : IExportedInstanced public Record? Record { get; set => OnRecordChanged?.Invoke(this, field = value); } + public ExportedInnerInstanced Inner { get; } = new(); public string GetInstanceArg () => instanceArg; public async Task GetRecordIdAsync (Record record) diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedModule.cs similarity index 73% rename from src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs rename to src/js/test/cs/Test.Types/Interfaces/ExportedModule.cs index c9eebc73..2b82ecf1 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedModule.cs @@ -2,9 +2,9 @@ namespace Test.Types; -public class ExportedStatic : IExportedStatic +public class ExportedModule : IExportedModule { - public event IExportedStatic.RecordChanged? OnRecordChanged; + public event IExportedModule.RecordChanged? OnRecordChanged; public Record? Record { get; set => OnRecordChanged?.Invoke(field = value); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs index 52e0d86a..ebb821ad 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs @@ -7,6 +7,7 @@ public interface IExportedInstanced event RecordChanged OnRecordChanged; Record? Record { get; set; } + ExportedInnerInstanced Inner { get; } string GetInstanceArg (); Task GetRecordIdAsync (Record record); diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedModule.cs similarity index 88% rename from src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs rename to src/js/test/cs/Test.Types/Interfaces/IExportedModule.cs index 67a526d5..7c67718c 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedModule.cs @@ -2,7 +2,7 @@ namespace Test.Types; -public interface IExportedStatic +public interface IExportedModule { delegate void RecordChanged (Record? record); diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs new file mode 100644 index 00000000..a09d2cb6 --- /dev/null +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs @@ -0,0 +1,12 @@ +using System; + +namespace Test.Types; + +public interface IImportedInnerInstanced +{ + event Action OnCountChanged; + + int Count { get; set; } + + void Increment (); +} diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs index 0d9b77fc..20b4c28e 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs @@ -7,6 +7,7 @@ public interface IImportedInstanced event RecordChanged OnRecordChanged; Record? Record { get; set; } + IImportedInnerInstanced Inner { get; } string GetInstanceArg (); Task GetRecordIdAsync (Record record); diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedModule.cs similarity index 88% rename from src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs rename to src/js/test/cs/Test.Types/Interfaces/IImportedModule.cs index 9fdccc22..624d8a2f 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedModule.cs @@ -2,7 +2,7 @@ namespace Test.Types; -public interface IImportedStatic +public interface IImportedModule { delegate void RecordChanged (Record? record); diff --git a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs index 0aab97ba..23034b5d 100644 --- a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs +++ b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs @@ -6,20 +6,20 @@ namespace Test.Types; public static class Interfaces { - [Export] public static event Action? OnImportedStaticRecordEchoed; + [Export] public static event Action? OnImportedModuleRecordEchoed; [Export] public static event Action? OnImportedInstanceRecordEchoed; [Export] public static async Task GetImportedArgAndRecordIdAsync (Record record, string arg) { - var instance = await GetImportedStatic().GetInstanceAsync(arg); + var instance = await GetImportedModule().GetInstanceAsync(arg); return await instance.GetRecordIdAsync(record) + instance.GetInstanceArg(); } [Export] - public static string GetImportedStaticRecordIdAndSet (Record record) + public static string GetImportedModuleRecordIdAndSet (Record record) { - var imported = GetImportedStatic(); + var imported = GetImportedModule(); var currentRecordId = imported.Record?.Id ?? ""; imported.Record = record; return currentRecordId; @@ -28,7 +28,7 @@ public static string GetImportedStaticRecordIdAndSet (Record record) [Export] public static async Task GetImportedInstanceArgAndRecordIdAsync (Record record, string arg) { - var instance = await GetImportedStatic().GetInstanceAsync(arg); + var instance = await GetImportedModule().GetInstanceAsync(arg); var currentRecordId = instance.Record?.Id ?? ""; instance.Record = record; return instance.GetInstanceArg() + currentRecordId + instance.Record.Id; @@ -37,7 +37,7 @@ public static async Task GetImportedInstanceArgAndRecordIdAsync (Record [Export] public static async Task GetImportedArgsAndFinalize (string arg1, string arg2) { - var imported = GetImportedStatic(); + var imported = GetImportedModule(); var instance1 = await imported.GetInstanceAsync(arg1); var instance2 = await imported.GetInstanceAsync(arg2); var result = new[] { instance1.GetInstanceArg(), instance2.GetInstanceArg() }; @@ -50,9 +50,9 @@ public static async Task GetImportedArgsAndFinalize (string arg1, stri } [Export] - public static Task EchoImportedStaticRecordEventAsync () + public static Task EchoImportedModuleRecordEventAsync () { - var imported = GetImportedStatic(); + var imported = GetImportedModule(); var tcs = new TaskCompletionSource(); imported.OnRecordChanged += Handle; return tcs.Task; @@ -60,7 +60,7 @@ public static Task EchoImportedStaticRecordEventAsync () void Handle (Record? record) { imported.OnRecordChanged -= Handle; - OnImportedStaticRecordEchoed?.Invoke(record); + OnImportedModuleRecordEchoed?.Invoke(record); tcs.SetResult(); } } @@ -80,8 +80,26 @@ void Handle (IImportedInstanced caller, Record? record) } } - private static IImportedStatic GetImportedStatic () + [Export] + public static string? CanInteropWithImportedInnerInstances (IImportedInstanced imported) + { + var inner = imported.Inner; + var currentCount = -1; + inner.OnCountChanged += HandleCountChanged; + inner.Count = 0; + if (currentCount != 0) return $"Set test failed. Expected count '0', but was '{currentCount}'."; + inner.Increment(); + if (currentCount != 1) return $"Increment test failed. Expected count '1', but was '{currentCount}'."; + inner.Increment(); + if (inner.Count != 2) return $"Get test failed. Expected count '2', but was '{currentCount}'."; + inner.OnCountChanged -= HandleCountChanged; + return null; + + void HandleCountChanged (int count) => currentCount = count; + } + + private static IImportedModule GetImportedModule () { - return (IImportedStatic)Modules.Imports[typeof(IImportedStatic)].Instance; + return (IImportedModule)Modules.Imports[typeof(IImportedModule)].Instance; } } diff --git a/src/js/test/cs/Test.Types/Registries/IRegistry.cs b/src/js/test/cs/Test.Types/Registries/IRegistry.cs new file mode 100644 index 00000000..332d5048 --- /dev/null +++ b/src/js/test/cs/Test.Types/Registries/IRegistry.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace Test.Types; + +public interface IRegistry +{ + IReadOnlyList Wheeled { get; set; } + IReadOnlyList Tracked { get; set; } +} diff --git a/src/js/test/cs/Test.Types/Registries/IRegistryProvider.cs b/src/js/test/cs/Test.Types/Registries/IRegistryProvider.cs new file mode 100644 index 00000000..d382f1d6 --- /dev/null +++ b/src/js/test/cs/Test.Types/Registries/IRegistryProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Test.Types; + +public interface IRegistryProvider +{ + IRegistry GetRegistry (); + IReadOnlyList GetRegistries (); + IReadOnlyDictionary GetRegistryMap (); +} diff --git a/src/js/test/cs/Test.Types/Vehicle/Registry.cs b/src/js/test/cs/Test.Types/Registries/Registries.cs similarity index 73% rename from src/js/test/cs/Test.Types/Vehicle/Registry.cs rename to src/js/test/cs/Test.Types/Registries/Registries.cs index 683e8691..5050823d 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Registry.cs +++ b/src/js/test/cs/Test.Types/Registries/Registries.cs @@ -6,15 +6,20 @@ namespace Test.Types; -public partial class Registry +public partial class Registries { [Export] public static event Action? OnVehicleBroadcast; public static IRegistryProvider Provider { get; set; } = null!; - public List Wheeled { get; set; } = null!; - public List Tracked { get; set; } = null!; - [Export] public static Registry EchoRegistry (Registry registry) => registry; + [Export] + public static IRegistry EchoRegistry (IRegistry registry) + { + registry.Wheeled = registry.Wheeled; + registry.Tracked = registry.Tracked; + return registry; + } + [Export] public static Vehicle?[]? EchoVehicles (Vehicle?[]? value) => value; [Export] public static Record?[]? EchoRecords (Record?[]? value) => value; @@ -27,15 +32,15 @@ public static float CountTotalSpeed () } [Export] - public static async Task> ConcatRegistriesAsync (IReadOnlyList registries) + public static async Task> ConcatRegistriesAsync (IReadOnlyList registries) { await Task.Delay(1); return registries.Concat(Provider.GetRegistries()).ToArray(); } [Export] - public static async Task> MapRegistriesAsync - (IReadOnlyDictionary map) + public static async Task> MapRegistriesAsync + (IReadOnlyDictionary map) { await Task.Delay(1); return map.Concat(Provider.GetRegistryMap()).ToDictionary(kv => kv.Key, kv => kv.Value); diff --git a/src/js/test/cs/Test.Types/Vehicle/TrackType.cs b/src/js/test/cs/Test.Types/Registries/TrackType.cs similarity index 100% rename from src/js/test/cs/Test.Types/Vehicle/TrackType.cs rename to src/js/test/cs/Test.Types/Registries/TrackType.cs diff --git a/src/js/test/cs/Test.Types/Vehicle/Tracked.cs b/src/js/test/cs/Test.Types/Registries/Tracked.cs similarity index 69% rename from src/js/test/cs/Test.Types/Vehicle/Tracked.cs rename to src/js/test/cs/Test.Types/Registries/Tracked.cs index f0541b47..4b85ef32 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Tracked.cs +++ b/src/js/test/cs/Test.Types/Registries/Tracked.cs @@ -1,6 +1,6 @@ namespace Test.Types; -public class Tracked : Vehicle +public record Tracked : Vehicle { public TrackType TrackType { get; set; } } diff --git a/src/js/test/cs/Test.Types/Vehicle/Vehicle.cs b/src/js/test/cs/Test.Types/Registries/Vehicle.cs similarity index 83% rename from src/js/test/cs/Test.Types/Vehicle/Vehicle.cs rename to src/js/test/cs/Test.Types/Registries/Vehicle.cs index bd5910d6..392d845d 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Vehicle.cs +++ b/src/js/test/cs/Test.Types/Registries/Vehicle.cs @@ -1,6 +1,6 @@ namespace Test.Types; -public class Vehicle +public record Vehicle { public string Id { get; set; } = null!; public float MaxSpeed { get; set; } diff --git a/src/js/test/cs/Test.Types/Vehicle/Wheeled.cs b/src/js/test/cs/Test.Types/Registries/Wheeled.cs similarity index 67% rename from src/js/test/cs/Test.Types/Vehicle/Wheeled.cs rename to src/js/test/cs/Test.Types/Registries/Wheeled.cs index 2a87d4c3..f70e0450 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Wheeled.cs +++ b/src/js/test/cs/Test.Types/Registries/Wheeled.cs @@ -1,6 +1,6 @@ namespace Test.Types; -public class Wheeled : Vehicle +public record Wheeled : Vehicle { public int WheelCount { get; set; } } diff --git a/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs b/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs deleted file mode 100644 index de6aa90e..00000000 --- a/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Collections.Generic; - -namespace Test.Types; - -public interface IRegistryProvider -{ - Registry GetRegistry (); - IReadOnlyList GetRegistries (); - IReadOnlyDictionary GetRegistryMap (); -} diff --git a/src/js/test/cs/Test/Program.cs b/src/js/test/cs/Test/Program.cs index 24ebc03a..7b397474 100644 --- a/src/js/test/cs/Test/Program.cs +++ b/src/js/test/cs/Test/Program.cs @@ -4,8 +4,8 @@ using Microsoft.Extensions.DependencyInjection; using Test.Types; -[assembly: Export(typeof(IExportedStatic))] -[assembly: Import(typeof(IImportedStatic), typeof(IRegistryProvider))] +[assembly: Export(typeof(IExportedModule))] +[assembly: Import(typeof(IImportedModule), typeof(IRegistryProvider))] namespace Test; @@ -16,11 +16,11 @@ public static partial class Program public static void Main () { services = new ServiceCollection() - .AddSingleton() + .AddSingleton() .AddBootsharp() .BuildServiceProvider() .RunBootsharp(); - Registry.Provider = services.GetRequiredService(); + Registries.Provider = services.GetRequiredService(); OnMainInvoked(); } diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index dd537feb..4c81cffc 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -216,7 +216,7 @@ describe("boot", () => { const cfg = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); const dotnet = (await bootsharp.dotnet.getMain(root)).dotnet; const runtime = await dotnet.withConfig(cfg).create(); - runtime.getAssemblyExports = () => Promise.resolve({}); + runtime.getAssemblyExports = () => Promise.resolve({ Bootsharp: { Generated: { Instances: { DisposeExported: () => {} } } } }); return runtime; }), root diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 543ebc8d..dbcd172b 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -4,9 +4,10 @@ import { Event, Test, bootSideload } from "../cs"; const TrackType = Test.Types.TrackType; class Imported implements Test.Types.IImportedInstanced { - constructor(private arg: string) { } - record: Test.Types.Record = { id: "foo" }; onRecordChanged = new Event<[Test.Types.IImportedInstanced, Test.Types.Record | undefined]>(); + record: Test.Types.Record = { id: "foo" }; + inner = new ImportedInner(); + constructor(private arg: string) { } getInstanceArg() { return this.arg; } async getRecordIdAsync(record: Test.Types.Record) { await new Promise(res => setTimeout(res, 1)); @@ -14,6 +15,14 @@ class Imported implements Test.Types.IImportedInstanced { } } +class ImportedInner implements Test.Types.IImportedInnerInstanced { + onCountChanged = new Event<[number]>(); + #count = 0; + get count() { return this.#count; } + set count(value) { this.onCountChanged.broadcast(this.#count = value); } + increment() { this.count++; } +} + describe("while bootsharp is not booted", () => { it("throws when attempting to invoke C# APIs", () => { expect(Test.Invokable.invokeVoid).throw(/Boot the runtime before invoking C# APIs/); @@ -28,11 +37,11 @@ describe("while bootsharp is booted", () => { expect(Test.Functions.getStringAsync).toBeUndefined(); expect(Test.Functions.getBytes).toBeUndefined(); expect(Test.Platform.throwJS).toBeUndefined(); - expect(Test.Types.Registry.createVehicle).toBeUndefined(); + expect(Test.Types.Registries.createVehicle).toBeUndefined(); expect(Test.Types.RegistryProvider.getRegistry).toBeUndefined(); expect(Test.Types.RegistryProvider.getRegistries).toBeUndefined(); expect(Test.Types.RegistryProvider.getRegistryMap).toBeUndefined(); - expect(Test.Types.ImportedStatic.getInstanceAsync).toBeUndefined(); + expect(Test.Types.ImportedModule.getInstanceAsync).toBeUndefined(); }); it("errs when invoking unassigned JS function", () => { expect(() => Test.Functions.invokeJSFunction()) @@ -98,11 +107,11 @@ describe("while bootsharp is booted", () => { { id: "tractor", trackType: TrackType.Rubber, maxSpeed: Math.fround(15.9) } ] }; - const actual = Test.Types.Registry.echoRegistry(expected); + const actual = Test.Types.Registries.echoRegistry(expected); expect(actual).toStrictEqual(expected); }); it("empty string of a struct is transferred correctly", () => { - const id = Test.Types.Registry.getVehicleWithEmptyId().id; + const id = Test.Types.Registries.getVehicleWithEmptyId().id; expect(id).not.toBeNull(); expect(id).not.toBeUndefined(); expect(id).toStrictEqual(""); @@ -112,7 +121,7 @@ describe("while bootsharp is booted", () => { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] }]; - const result = await Test.Types.Registry.concatRegistriesAsync([ + const result = await Test.Types.Registries.concatRegistriesAsync([ { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }], tracked: [] }, { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] } ]); @@ -127,7 +136,7 @@ describe("while bootsharp is booted", () => { ["foo", { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] }], ["bar", { wheeled: [{ id: "bar", maxSpeed: 15, wheelCount: 5 }], tracked: [] }] ]); - const result = await Test.Types.Registry.mapRegistriesAsync(new Map([ + const result = await Test.Types.Registries.mapRegistriesAsync(new Map([ ["baz", { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] }] ])); expect(result).toStrictEqual(new Map([ @@ -157,11 +166,11 @@ describe("while bootsharp is booted", () => { wheeled: [{ id: "", maxSpeed: 1, wheelCount: 0 }], tracked: [{ id: "", maxSpeed: 2, trackType: TrackType.Chain }] }); - expect(Test.Types.Registry.countTotalSpeed()).toStrictEqual(3); + expect(Test.Types.Registries.countTotalSpeed()).toStrictEqual(3); }); it("can invoke assigned JS functions from library assembly", () => { - Test.Types.Registry.createVehicle = (id, maxSpeed) => ({ id, maxSpeed }); - expect(Test.Types.Registry.getVehicle("foo", 42)).toStrictEqual({ id: "foo", maxSpeed: 42 }); + Test.Types.Registries.createVehicle = (id, maxSpeed) => ({ id, maxSpeed }); + expect(Test.Types.Registries.getVehicle("foo", 42)).toStrictEqual({ id: "foo", maxSpeed: 42 }); }); it("can subscribe to exported events", () => { const handler = vi.fn(); @@ -184,10 +193,10 @@ describe("while bootsharp is booted", () => { }); it("can subscribe to events from library assembly", () => { const handler = vi.fn(); - Test.Types.Registry.onVehicleBroadcast.subscribe(handler); - Test.Types.Registry.broadcastVehicle({ id: "foo", maxSpeed: 42 }); + Test.Types.Registries.onVehicleBroadcast.subscribe(handler); + Test.Types.Registries.broadcastVehicle({ id: "foo", maxSpeed: 42 }); expect(handler).toHaveBeenCalledWith({ id: "foo", maxSpeed: 42 }); - Test.Types.Registry.broadcastVehicle(undefined); + Test.Types.Registries.broadcastVehicle(undefined); expect(handler).toHaveBeenCalledWith(undefined); }); it("can un-subscribe from events", () => { @@ -224,25 +233,40 @@ describe("while bootsharp is booted", () => { expect(Test.Invokable.getIdxEnumOne() === Test.IdxEnum.Two).not.toBeTruthy(); }); it("can interop with imported modules", async () => { - Test.Types.ImportedStatic.record = { id: "baz" }; - expect(Test.Types.Interfaces.getImportedStaticRecordIdAndSet({ id: "qux" })).toStrictEqual("baz"); - expect(Test.Types.ImportedStatic.record).toStrictEqual({ id: "qux" }); - Test.Types.ImportedStatic.record = undefined; - expect(Test.Types.ImportedStatic.record).toBeUndefined(); + Test.Types.ImportedModule.record = { id: "baz" }; + expect(Test.Types.Interfaces.getImportedModuleRecordIdAndSet({ id: "qux" })).toStrictEqual("baz"); + expect(Test.Types.ImportedModule.record).toStrictEqual({ id: "qux" }); + Test.Types.ImportedModule.record = undefined; + expect(Test.Types.ImportedModule.record).toBeUndefined(); const handler = vi.fn(); - Test.Types.Interfaces.onImportedStaticRecordEchoed.subscribe(handler); - let echo = Test.Types.Interfaces.echoImportedStaticRecordEventAsync(); - Test.Types.ImportedStatic.onRecordChanged.broadcast({ id: "static" }); + Test.Types.Interfaces.onImportedModuleRecordEchoed.subscribe(handler); + let echo = Test.Types.Interfaces.echoImportedModuleRecordEventAsync(); + Test.Types.ImportedModule.onRecordChanged.broadcast({ id: "static" }); await echo; expect(handler).toHaveBeenCalledWith({ id: "static" }); - echo = Test.Types.Interfaces.echoImportedStaticRecordEventAsync(); - Test.Types.ImportedStatic.onRecordChanged.broadcast(undefined); + echo = Test.Types.Interfaces.echoImportedModuleRecordEventAsync(); + Test.Types.ImportedModule.onRecordChanged.broadcast(undefined); await echo; expect(handler).toHaveBeenCalledWith(undefined); - Test.Types.Interfaces.onImportedStaticRecordEchoed.unsubscribe(handler); + Test.Types.Interfaces.onImportedModuleRecordEchoed.unsubscribe(handler); + }); + it("can interop with exported modules", () => { + const record = { id: "foo" }; + const handler = vi.fn(); + Test.Types.ExportedModule.onRecordChanged.subscribe(handler); + Test.Types.ExportedModule.record = record; + expect(Test.Types.ExportedModule.record).toStrictEqual(record); + expect(handler).toHaveBeenCalledWith(record); + Test.Types.ExportedModule.record = { id: "bar" }; + expect(Test.Types.ExportedModule.record).toStrictEqual({ id: "bar" }); + expect(handler).toHaveBeenCalledWith({ id: "bar" }); + Test.Types.ExportedModule.record = undefined; + expect(Test.Types.ExportedModule.record).toBeUndefined(); + expect(handler).toHaveBeenCalledWith(undefined); + Test.Types.ExportedModule.onRecordChanged.unsubscribe(handler); }); it("can interop with imported instances", async () => { - Test.Types.ImportedStatic.getInstanceAsync = async (arg) => { + Test.Types.ImportedModule.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); return new Imported(arg); }; @@ -265,23 +289,8 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(undefined); Test.Types.Interfaces.onImportedInstanceRecordEchoed.unsubscribe(handler); }); - it("can interop with exported modules", () => { - const record = { id: "foo" }; - const handler = vi.fn(); - Test.Types.ExportedStatic.onRecordChanged.subscribe(handler); - Test.Types.ExportedStatic.record = record; - expect(Test.Types.ExportedStatic.record).toStrictEqual(record); - expect(handler).toHaveBeenCalledWith(record); - Test.Types.ExportedStatic.record = { id: "bar" }; - expect(Test.Types.ExportedStatic.record).toStrictEqual({ id: "bar" }); - expect(handler).toHaveBeenCalledWith({ id: "bar" }); - Test.Types.ExportedStatic.record = undefined; - expect(Test.Types.ExportedStatic.record).toBeUndefined(); - expect(handler).toHaveBeenCalledWith(undefined); - Test.Types.ExportedStatic.onRecordChanged.unsubscribe(handler); - }); it("can interop with exported instances", async () => { - const exported = await Test.Types.ExportedStatic.getInstanceAsync("bar"); + const exported = await Test.Types.ExportedModule.getInstanceAsync("bar"); const handler = vi.fn(); expect(exported.getInstanceArg()).toStrictEqual("bar"); expect(await exported.getRecordIdAsync({ id: "foo" })).toStrictEqual("foo"); @@ -295,8 +304,24 @@ describe("while bootsharp is booted", () => { expect(handler).toHaveBeenCalledWith(exported, undefined); exported.onRecordChanged.unsubscribe(handler); }); - it("releases interface instances after use", async () => { - Test.Types.ImportedStatic.getInstanceAsync = async (arg) => new Imported(arg); + it("can interop with imported inner instances", async () => { + const imported = new Imported(""); + const error = Test.Types.Interfaces.canInteropWithImportedInnerInstances(imported); + expect(error).toBeNull(); + }); + it("can interop with exported inner instances", async () => { + const handler = vi.fn(); + const inner = (await Test.Types.ExportedModule.getInstanceAsync("bar")).inner; + inner.onCountChanged.subscribe(handler); + inner.count = 0; + expect(handler).toHaveBeenCalledWith(0); + inner.increment(); + expect(handler).toHaveBeenCalledWith(1); + inner.increment(); + expect(inner.count).toStrictEqual(2); + }); + it("releases instances after use", async () => { + Test.Types.ImportedModule.getInstanceAsync = async (arg) => new Imported(arg); expect(await Test.Types.Interfaces.getImportedArgsAndFinalize("qux", "fox")).toStrictEqual(["qux", "fox"]); expect(await Test.Types.Interfaces.getImportedArgsAndFinalize("zip", "zap")).toStrictEqual(["zip", "zap"]); }); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 6befe0ed..c5bc919c 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -55,11 +55,11 @@ describe("serialization", () => { }); it("can echo vehicles", async () => { - expect(Test.Types.Registry.echoRecords([{ id: "foo" }, null])) + expect(Test.Types.Registries.echoRecords([{ id: "foo" }, null])) .toStrictEqual([{ id: "foo" }, null]); - expect(Test.Types.Registry.echoVehicles([{ id: "foo", maxSpeed: 1 }, null])) + expect(Test.Types.Registries.echoVehicles([{ id: "foo", maxSpeed: 1 }, null])) .toStrictEqual([{ id: "foo", maxSpeed: 1 }, null]); - expect(Test.Types.Registry.echoRegistry({ + expect(Test.Types.Registries.echoRegistry({ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 2 }, null], tracked: [{ id: "bar", maxSpeed: 2, trackType: Test.Types.TrackType.Chain }, null] })).toStrictEqual({ @@ -67,7 +67,7 @@ describe("serialization", () => { tracked: [{ id: "bar", maxSpeed: 2, trackType: Test.Types.TrackType.Chain }, null] }); Test.Types.RegistryProvider.getRegistries = () => []; - await expect(Test.Types.Registry.concatRegistriesAsync([null])).resolves.toStrictEqual([null]); + await expect(Test.Types.Registries.concatRegistriesAsync([null])).resolves.toStrictEqual([null]); }); it("can echo arrays", () => { From f8d78155be21ccf226f18bd63482a3e2ab8a8d12 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 7 May 2026 21:04:06 +0300 Subject: [PATCH 18/20] ffs --- .../Bootsharp.Publish/Common/Inspector/InstancedInspector.cs | 3 ++- src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs | 2 +- .../test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs | 2 +- src/js/test/cs/Test.Types/Registries/IRegistry.cs | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index 6a17a26d..ee715043 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -16,7 +16,8 @@ internal sealed class InstancedInspector (MemberInspector members) public ModuleMeta? InspectModule (Type type, InteropKind ik) { - if (ik == InteropKind.Import && !type.IsInterface || IsStatic(type)) return null; + if (ik == InteropKind.Import && !type.IsInterface) return null; + if (IsStatic(type)) return null; var it = CollectMembers(InspectType(type, ik)); return new(type) { Interop = ik, Namespace = it.Namespace, Name = it.Name, Members = it.Members }; } diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs index 5ab7b6ca..45e07f59 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedInnerInstanced.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Test.Types; diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs index a09d2cb6..894ff4d7 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedInnerInstanced.cs @@ -1,4 +1,4 @@ -using System; +using System; namespace Test.Types; diff --git a/src/js/test/cs/Test.Types/Registries/IRegistry.cs b/src/js/test/cs/Test.Types/Registries/IRegistry.cs index 332d5048..e4180fcf 100644 --- a/src/js/test/cs/Test.Types/Registries/IRegistry.cs +++ b/src/js/test/cs/Test.Types/Registries/IRegistry.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Test.Types; From 0a0ec884569b048c378622e39b771a0366659d25 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 7 May 2026 21:19:04 +0300 Subject: [PATCH 19/20] etc --- .../Pack/DeclarationGenerator/TypeDeclarationGenerator.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index ff90f6a5..c84fbe88 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -122,11 +122,9 @@ private void DeclareInstanced (InstancedMeta it) void AppendExtensions () { - var extTypes = new List(it.Clr.GetInterfaces().Where(IsUserType)); - if (TryGetBase(it.Clr, out var baseType)) - extTypes.Insert(0, baseType); - if (extTypes.Count > 0) - bld.Append(" extends ").AppendJoin(", ", extTypes.Select(ts.BuildFullName)); + var ext = new List(it.Clr.GetInterfaces().Where(IsUserType)); + if (TryGetBase(it.Clr, out var baseType)) ext.Insert(0, baseType); + if (ext.Count > 0) bld.Append(" extends ").AppendJoin(", ", ext.Select(ts.BuildFullName)); } void AppendEvent (EventMeta evt) From 9c16cbbf05c570160d59eac79499fc2b59ea29df Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 7 May 2026 23:09:10 +0300 Subject: [PATCH 20/20] dedup modules --- .../Bootsharp.Publish.Test/Emit/ModulesTest.cs | 16 ++++++++++++++++ .../Common/Inspector/InstancedInspector.cs | 2 ++ src/cs/Directory.Build.props | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs index e36d406f..bc591a55 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs @@ -320,4 +320,20 @@ public void Inst () {} Execute(); DoesNotContain("StaticMethod"); } + + [Fact] + public void IgnoresDuplicateModules () + { + AddAssembly("Library.dll", With( + """ + [assembly:Import(typeof(IShared))] + public interface IShared { void Inv (); } + """)); + AddAssembly("Entry.dll", With( + """ + [assembly:Import(typeof(IShared))] + """)); + Execute(); + Once("class JSShared"); + } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs index ee715043..d29202a1 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -5,6 +5,7 @@ namespace Bootsharp.Publish; internal sealed class InstancedInspector (MemberInspector members) { private readonly Dictionary byType = []; + private readonly HashSet modules = []; public InstancedMeta? Inspect (Type type, InteropKind ik) { @@ -16,6 +17,7 @@ internal sealed class InstancedInspector (MemberInspector members) public ModuleMeta? InspectModule (Type type, InteropKind ik) { + if (!modules.Add(type)) return null; if (ik == InteropKind.Import && !type.IsInterface) return null; if (IsStatic(type)) return null; var it = CollectMembers(InspectType(type, ik)); diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 681b0b9f..13f7408f 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.170 + 0.8.0-alpha.172 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com