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/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/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..201150f7 --- /dev/null +++ b/src/cs/Bootsharp.Publish.Test/Emit/InstancesTest.cs @@ -0,0 +1,152 @@ +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() => Instances.DisposeImported(_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"); + } + + [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 3a65a631..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 () { @@ -126,7 +118,7 @@ public void IgnoresEventsWithoutImportExportAttributes () } [Fact] - public void GeneratesForMethodsInStaticInterfaces () + public void GeneratesForMethodsInModules () { AddAssembly(With( """ @@ -147,31 +139,31 @@ public interface IImported { Info Fun (string str, Info info); } } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ 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] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -212,7 +204,7 @@ public interface IImported } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -251,7 +243,7 @@ public class Class } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -276,11 +268,11 @@ 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] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ @@ -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)));"); } @@ -320,15 +301,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,32 +321,13 @@ public class Class } """)); Execute(); - DoesNotContain("Ignored"); - DoesNotContain("IgnoredToo"); + DoesNotContain("DefaultGet"); + DoesNotContain("DefaultSet"); + DoesNotContain("BothDefault"); DoesNotContain("GetPropertyItem"); DoesNotContain("SetPropertyItem"); } - [Fact] - public void DoesNotEmitDuplicateInterfaceRegistrations () - { - 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/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs similarity index 58% rename from src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs rename to src/cs/Bootsharp.Publish.Test/Emit/ModulesTest.cs index f41ee662..bc591a55 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,66 @@ 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"); + } + + [Fact] + public void GeneratesImportedInterfaceModule () { AddAssembly(With( """ + [assembly:Import(typeof(IImported))] + public record Record; public interface IImported @@ -150,74 +172,69 @@ 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; + [assembly:Import(typeof(Imported))] - public interface IExported + public class Imported { - 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; + public void Inv () {} } """)); Execute(); - DoesNotContain("JSExported"); + DoesNotContain("JSImported"); } [Fact] - public void RespectsInterfaceNamespace () + public void RespectsModuleNamespace () { AddAssembly(With( """ @@ -236,13 +253,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 +294,46 @@ public void IgnoresImplementedInterfaceMethods () { AddAssembly(With( """ - [assembly:Export(typeof(IExportedStatic))] - [assembly:Import(typeof(IImportedStatic))] + [assembly:Export(typeof(IExported))] + [assembly:Import(typeof(IImported))] - public interface IExportedStatic { int Foo () => 0; } - public interface IImportedStatic { int Foo () => 0; } - public interface IExportedInstanced { int Foo () => 0; } - public interface IImportedInstanced { int Foo () => 0; } + public interface IExported { int Foo () => 0; } + public interface IImported { int Foo () => 0; } + """)); + Execute(); + DoesNotContain("Foo"); + } - public class Class + [Fact] + public void IgnoresStaticMembersOnExportedClassModule () + { + AddAssembly(With( + """ + [assembly:Export(typeof(Exported))] + + 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"); + } + + [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.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index b4b07664..6505a439 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; } @@ -436,16 +436,16 @@ 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] - public void SerializesTypesFromInstancedInterfaces () + public void SerializesTypesFromInstanced () { AddAssembly(With( """ @@ -467,7 +467,7 @@ public class Class } [Fact] - public void DoesntSerializeInstancedInterfacesThemselves () + 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 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; } }"), - 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 772399e2..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 () {} } """)); @@ -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( """ @@ -623,19 +623,19 @@ public interface IImported { Info Fun (string str, Info info); } } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ 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,26 +643,26 @@ 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) }; """); } [Fact] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -707,7 +707,7 @@ public interface IImported } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -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 = { @@ -766,7 +766,7 @@ class JSExported { } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -796,7 +796,7 @@ public interface IImported { event Action Evt; } } [Fact] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ @@ -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 = { @@ -846,7 +846,7 @@ class JSExported { } [Fact] - public void DoesNotEmitDuplicateInterfaceRegistrations () + public void DoesNotEmitDuplicateModuleRegistrations () { AddAssembly(With( """ @@ -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 e411aafa..d86429e9 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 interface Record { - } export enum Enum { A, B } + export type Record = Readonly<{ + }>; export namespace Class { export function inv(r: Record): Enum; @@ -70,8 +70,8 @@ public void NestedTypesAreDeclaredUnderClassSpace () Contains( """ export namespace Foo { - export interface Bar { - } + export type Bar = Readonly<{ + }>; } export namespace Class { @@ -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; } } @@ -533,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( @@ -544,7 +546,8 @@ export interface Generic { value: T; } export interface GenericNull { - value?: T; + readonly value?: T; + foo(t: T | undefined): T | null; } } @@ -554,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 () { @@ -630,58 +658,71 @@ 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 RecordClass(double A); + 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 ReadonlyRecordStruct Rrs { get; } public RecordClass Rc { get; } } - public class Baz { public List Bars { get; } public Enum E { get; } } - public class Class { [Export] public static Baz GetBaz () => default; } + public class Bar : Foo { public Dictionary Rc { get; } } + 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 { - bars: Array; - e: Space.Enum; - } - export interface Bar extends Space.Foo { - rrs: Space.ReadonlyRecordStruct; - rc: Space.RecordClass; + export interface Key extends Space.Baz { } - export interface ReadonlyRecordStruct { - a: number; - } - export interface RecordClass { - a: number; - } - export interface Struct { - a: number; + export interface Bar { + readonly rc: Map; + readonly s: Space.Struct; + readonly rs: Space.ReadonlyStruct; } - export interface ReadonlyStruct { - a: number; + export interface Nya { + mew(): boolean; } - export interface Foo { - s: Space.Struct; - rs: Space.ReadonlyStruct; + export interface Baz extends Space.Bar { + readonly bars: Array; + readonly e: Space.Enum; } export enum Enum { A, B } + export type ReadonlyRecordStruct = Readonly<{ + a: number; + }>; + export type ReadonlyStruct = Readonly<{ + a: number; + }>; + export type RecordClassA = Readonly<{ + a: number; + str: Space.ReadonlyRecordStruct; + }>; + export type RecordClassB = Space.RecordClassA & Readonly<{ + b: Space.RecordClassA; + }>; + export type Struct = Readonly<{ + a: number; + mew: Space.Nya; + }>; } 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; } }"), @@ -691,11 +732,56 @@ public void StaticPropertiesAreNotIncluded () """ export namespace Class { export interface Foo { + readonly soo: string; } } """); } + [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 () { @@ -704,8 +790,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; @@ -714,15 +798,15 @@ public bool SetOnly { set { } } Contains( """ export namespace Class { - export interface Foo { + export type Foo = Readonly<{ boo: boolean; - } + }>; } """); } [Fact] - public void GeneratesForMethodsInStaticInterfaces () + public void GeneratesForMethodsInModules () { AddAssembly(With( """ @@ -737,9 +821,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; @@ -751,7 +835,7 @@ export namespace Imported { } [Fact] - public void GeneratesForMethodsInInstancedInterfaces () + public void GeneratesForMethodsInInstanced () { AddAssembly(With( """ @@ -762,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(); @@ -776,19 +860,19 @@ export interface IExported { inv(str: string, info: Info): Info; reset(): void; } - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; 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; } """); } [Fact] - public void GeneratesForPropertiesInStaticInterfaces () + public void GeneratesForPropertiesInModules () { AddAssembly(With( """ @@ -818,13 +902,13 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export interface Info { - value: string; - } export interface IExportedInstanced { } export interface IImportedInstanced { } + export type Info = Readonly<{ + value: string; + }>; export namespace ExportedStatic { export let state: Info; @@ -841,7 +925,7 @@ export namespace ImportedStatic { } [Fact] - public void GeneratesForPropertiesInInstancedInterfaces () + public void GeneratesForPropertiesInInstanced () { AddAssembly(With( """ @@ -863,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(); @@ -875,24 +959,24 @@ export interface IImported { readonly imported: IImported; exported: IExported; } - export interface Info { - 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; - export let getImported: (inst: IExported) => IImported; + export function getExported(it: IImported): IExported; + export let getImported: (it: IExported) => IImported; } """); } [Fact] - public void GeneratesForEventsInStaticInterfaces () + public void GeneratesForEventsInModules () { AddAssembly(With( """ @@ -910,13 +994,13 @@ public interface IImportedInstanced {} Execute(); Contains( """ - export interface Info { - 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]>; @@ -928,7 +1012,7 @@ export namespace Imported { } [Fact] - public void GeneratesForEventsInInstancedInterfaces () + public void GeneratesForEventsInInstanced () { AddAssembly(With( """ @@ -939,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(); @@ -953,13 +1037,13 @@ export interface IExported { changed: EventSubscriber<[obj: Info]>; done: EventSubscriber<[]>; } - export interface Info { + export type Info = Readonly<{ value: string; - } + }>; 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; } """); } @@ -1043,10 +1127,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 +1152,7 @@ public void NullableEnumsAreCrawled () """ export namespace n { export interface Bar { - foo?: n.Foo; + readonly foo?: n.Foo; } export enum Foo { A, @@ -1125,7 +1209,7 @@ export namespace Fun.Class { } [Fact] - public void RespectsSpacePrefInStaticInterfaces () + public void RespectsSpacePrefInModules () { AddAssembly(With( """ @@ -1163,7 +1247,7 @@ public void RespectsTypePreference () AddAssembly(With( """ [assembly: Bootsharp.Preferences( - Type = [@"Record", "Foo", @".+`.+", "Bar"] + Type = [@"Record", "Foo", @".+`.+", "Bar"] )] public record Record; @@ -1177,13 +1261,13 @@ [Export] public static void Inv (Record r, Generic g) {} Execute(); Contains( """ - export interface Record { - } - export interface Generic { - } + export type Bar = Readonly<{ + }>; + export type Foo = Readonly<{ + }>; export namespace Class { - export function inv(r: Foo, g: Bar): void; + export function inv(r: Foo, g: Bar): void; } """); } @@ -1333,12 +1417,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.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..533a4d6c 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -1,11 +1,12 @@ global using static Bootsharp.Publish.GlobalInspection; -using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; namespace Bootsharp.Publish; +internal delegate TypeMeta InspectType (Type type, InteropKind ik); + internal static class GlobalInspection { public static MetadataLoadContext CreateLoadContext (string directory) @@ -31,6 +32,15 @@ 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; + return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention + } + public static bool IsAutoProperty (PropertyInfo prop) { var backingFieldName = $"<{prop.Name}>k__BackingField"; @@ -39,13 +49,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/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index faf5f9d1..2c817849 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; @@ -56,9 +67,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 _); @@ -83,6 +105,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"; @@ -132,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/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs similarity index 50% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs index f78cc280..d0161577 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InspectionReporter.cs @@ -5,30 +5,31 @@ 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.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 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(); } - private HashSet GetDiscoveredMembers (SolutionInspection inspection) + private HashSet GetDiscoveredMembers (SolutionInspection spec) { - return inspection.StaticMembers - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) - .Concat(inspection.InstancedInterfaces.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(); } diff --git a/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs new file mode 100644 index 00000000..d29202a1 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspector/InstancedInspector.cs @@ -0,0 +1,99 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class InstancedInspector (MemberInspector members) +{ + private readonly Dictionary byType = []; + private readonly HashSet modules = []; + + 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); + if (!IsInstancedType(type)) return null; + return CollectMembers(byType[type] = InspectType(type, ik)); + } + + 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)); + return new(type) { Interop = ik, Namespace = it.Namespace, Name = it.Name, Members = it.Members }; + } + + public IReadOnlyCollection Collect () + { + return byType.Values.ToArray(); + } + + private InstancedMeta InspectType (Type type, InteropKind ik) => new(type) { + Interop = ik, + Namespace = BuildSpace(type, ik), + Name = BuildName(type), + JSName = BuildJSName(type), + Members = new List(), + Exporter = BuildExporter(type, ik), + Importer = BuildImporter(type, ik) + }; + + private InstancedMeta CollectMembers (InstancedMeta it) + { + var cl = (List)it.Members; + 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; + } + + 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 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 BuildName (Type type) + { + var trimmed = type.IsInterface ? type.Name[1..] : type.Name; + return "JS" + trimmed; + } + + private string BuildJSName (Type type) + { + var name = BuildName(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 new file mode 100644 index 00000000..42d1d09c --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspector/MemberInspector.cs @@ -0,0 +1,59 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +internal sealed class MemberInspector (Preferences prefs, InspectType inspect) +{ + public EventMeta Inspect (EventInfo evt, InteropKind ik) => new(evt) { + Interop = ik, + Space = evt.DeclaringType!.FullName!, + JSSpace = BuildJSSpace(evt.DeclaringType!), + Name = evt.Name, + JSName = BuildJSName(evt.Name), + Arguments = evt.EventHandlerType!.GetMethod("Invoke")!.GetParameters() + .Select(p => CreateArg(p, GetNullability(evt, p), ik)).ToArray() + }; + + public PropertyMeta Inspect (PropertyInfo prop, InteropKind ik) => new(prop) { + Interop = ik, + Space = prop.DeclaringType!.FullName!, + JSSpace = BuildJSSpace(prop.DeclaringType!), + Name = 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 + }; + + public MethodMeta Inspect (MethodInfo method, InteropKind ik) => new(method) { + Interop = ik, + Space = method.DeclaringType!.FullName!, + JSSpace = BuildJSSpace(method.DeclaringType!), + Name = 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), + Async = IsTaskLike(method.ReturnParameter.ParameterType) + }; + + private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil, InteropKind ik) => new(param) { + Name = param.Name!, + JSName = BuildJSName(param.Name!), + Value = CreateValue(param.ParameterType, nil, ik) + }; + + private ValueMeta CreateValue (Type type, NullabilityInfo nil, InteropKind ik) => new() { + Type = inspect(type, ik), + TypeSyntax = BuildSyntax(type, nil), + Nullable = IsNullable(type, nil) + }; + + private string BuildJSSpace (Type decl) + { + var space = decl.Namespace ?? ""; + var name = TrimGeneric(decl.Name); + if (decl.IsInterface) name = name[1..]; + var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; + return WithPrefs(prefs.Space, fullname, fullname); + } +} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs similarity index 94% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/SerializedInspector.cs index 175637c0..46aabb9a 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/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 (IsInstancedInterface(type, out _)) 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)!) : BuildObject(type); cycle.Remove(type); return meta; @@ -91,9 +93,10 @@ 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) { + Info = prop, 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), @@ -164,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/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs similarity index 52% rename from src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs rename to src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs index bd6e1af5..8cb4b7e3 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspection.cs @@ -14,31 +14,24 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable { /// - /// Interop interfaces specified under or - /// for which static bindings have to be emitted. - /// - public required IReadOnlyCollection StaticInterfaces { 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 InstancedInterfaces { get; init; } - /// - /// 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 StaticMembers { get; init; } + public required IReadOnlyCollection Static { get; init; } /// - /// All the types that cross the interop boundary or referenced by them. + /// Interop API surfaces specified under assembly-level + /// or attributes. /// - public required IReadOnlyCollection Types { get; init; } + public required IReadOnlyCollection Modules { 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 whose instances are passed by 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/Inspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs new file mode 100644 index 00000000..09358ad1 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspector/SolutionInspector.cs @@ -0,0 +1,114 @@ +using System.Reflection; +using System.Xml.Linq; + +namespace Bootsharp.Publish; + +internal sealed class SolutionInspector +{ + private readonly List statics = []; + private readonly List modules = []; + private readonly List docs = []; + private readonly List warnings = []; + private readonly MemberInspector members; + private readonly InstancedInspector itd; + private readonly SerializedInspector serde; + + public SolutionInspector (Preferences prefs) + { + members = new(prefs, InspectType); + itd = new(members); + serde = new(itd); + } + + /// + /// Inspects specified solution assembly paths in the output directory. + /// + /// Absolute path to directory containing compiled assemblies. + /// Absolute paths of the assemblies to inspect. + public SolutionInspection Inspect (string directory, IEnumerable paths) + { + var ctx = CreateLoadContext(directory); + foreach (var assemblyPath in paths) + try { InspectAssemblyFile(assemblyPath, ctx); } + catch (Exception e) { AddSkippedAssemblyWarning(assemblyPath, e); } + 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); + if (!IsUserAssembly(assemblyName)) return; + InspectDocumentation(assemblyPath, assemblyName); + InspectAssembly(ctx.LoadFromAssemblyPath(assemblyPath)); + } + + private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception) + { + var fileName = Path.GetFileName(assemblyPath); + var message = $"Failed to inspect '{fileName}' assembly; " + + $"affected interop members won't be available in JavaScript. Error: {exception.Message}"; + warnings.Add(message); + } + + private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { + Static = statics.ToArray(), + Modules = modules.ToArray(), + Instanced = itd.Collect(), + Serialized = serde.Collect(), + Documentation = docs.ToArray(), + Warnings = warnings.ToArray() + }; + + private void InspectDocumentation (string assemblyPath, string assemblyName) + { + var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); + if (File.Exists(xmlPath)) docs.Add(new(assemblyName, XDocument.Load(xmlPath))); + } + + private void InspectAssembly (Assembly assembly) + { + foreach (var type in assembly.GetExportedTypes()) + InspectStatic(type); + foreach (var attr in assembly.CustomAttributes) + InspectModules(attr); + } + + 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 { } ik) + statics.Add(members.Inspect(evt, ik)); + foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) + if (ResolveInterop(method) is { } ik) + statics.Add(members.Inspect(method, ik)); + } + + private void InspectModules (CustomAttributeData attr) + { + if (ResolveInterop(attr) is not { } ik) return; + foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) + if (itd.InspectModule((Type)arg.Value!, ik) is { } md) + modules.Add(md); + } + + private InteropKind? ResolveInterop (MemberInfo info) + { + foreach (var attr in info.CustomAttributes) + if (ResolveInterop(attr) is { } ik) + return ik; + return null; + } + + private InteropKind? ResolveInterop (CustomAttributeData attr) + { + 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/Common/Meta/InstancedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs new file mode 100644 index 00000000..c8558210 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Meta/InstancedMeta.cs @@ -0,0 +1,40 @@ +namespace Bootsharp.Publish; + +/// +/// Describes a mutable CLR type whose instances are passed by reference when crossing the interop boundary. +/// +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; } + /// + /// 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}"; + /// + /// Name of the generated JavaScript bindings wrapper. + /// + public required string JSName { get; init; } + /// + /// 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/InterfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs deleted file mode 100644 index e64f9232..00000000 --- a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Interface supplied by user under either -/// or representing static interop API, or in -/// an interop method, representing instanced interop API. -/// -internal sealed record InterfaceMeta -{ - /// - /// Whether the interface represents C# API consumed in - /// JavaScript (export) or vice versa (import). - /// - public required InteropKind Interop { get; init; } - /// - /// C# type of the interface. - /// - public required Type Type { get; init; } - /// - /// C# syntax of the interface type, as specified in source code. - /// - public required string TypeSyntax { get; init; } - /// - /// C# namespace of the generated interop class implementation. - /// - public required string Namespace { get; init; } - /// - /// C# name of the generated interop class implementation. - /// - public required string Name { get; init; } - /// - /// Full C# type name of the generated interop class implementation. - /// - public required string FullName { get; init; } - /// - /// JS name of the generated interop class implementation. - /// - public required string JSName { get; init; } - /// - /// Members declared on the interface, representing the interop API. - /// - public required IReadOnlyCollection Members { get; init; } -} 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 64e53b1e..5e4a2d1f 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs @@ -1,9 +1,10 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; 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 { @@ -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 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 . -/// 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; } @@ -70,7 +64,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 +79,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 { @@ -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/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/Common/Meta/SerializedMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs index 1962c337..c46b18e8 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/SerializedMeta.cs @@ -1,72 +1,76 @@ +using System.Reflection; + 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 an instance reference under a serialized object. +/// +/// 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. /// /// 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 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 fc38b611..fb94a3be 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/TypeMeta.cs @@ -1,9 +1,9 @@ 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. /// -internal sealed record TypeMeta (Type Clr) +internal record TypeMeta (Type Clr) { /// /// The described CLR type. diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index d42e7865..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; @@ -9,7 +8,7 @@ namespace Bootsharp.Publish; internal sealed record ValueMeta { /// - /// The type of the value. + /// Type info of the value. /// public required TypeMeta Type { get; init; } /// @@ -22,25 +21,21 @@ internal sealed record ValueMeta /// public required bool Nullable { get; init; } /// - /// Nullability context of the value. - /// - public required NullabilityInfo Nullability { get; init; } - /// /// Serialization info when , null otherwise. /// - public required SerializedMeta? Serialized { get; init; } + public SerializedMeta? Serialized => Type as SerializedMeta; /// - /// Associated interop interface instance type when , null otherwise. + /// Instance info when , null otherwise. /// - public required Type? InstanceType { 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(InstanceType))] - public bool IsInstance => InstanceType != null; + [MemberNotNullWhen(true, nameof(Instanced))] + public bool IsInstanced => Instanced != null; } 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 deleted file mode 100644 index 4d886799..00000000 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ /dev/null @@ -1,86 +0,0 @@ -using System.Reflection; - -namespace Bootsharp.Publish; - -internal sealed class MemberInspector (Preferences prefs, TypeInspector types, SerializedInspector serde) -{ - public EventMeta Inspect (EventInfo evt, InteropKind interop) - { - var inv = evt.EventHandlerType!.GetMethod("Invoke")!; - return new(evt) { - Interop = interop, - Assembly = evt.DeclaringType!.Assembly.GetName().Name!, - Space = evt.DeclaringType.FullName!, - 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)) - }; - - NullabilityInfo GetArgNullability (ParameterInfo param, int index) - { - if (evt.EventHandlerType!.IsGenericType) - { - var genType = evt.EventHandlerType.GetGenericTypeDefinition() - .GetMethod("Invoke")!.GetParameters()[index].ParameterType; - if (genType.IsGenericParameter) - return GetNullability(evt).GenericTypeArguments[genType.GenericParameterPosition]; - } - return GetNullability(param); - } - } - - 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 MethodMeta Inspect (MethodInfo method, InteropKind interop) => new(method) { - Interop = interop, - Assembly = method.DeclaringType!.Assembly.GetName().Name!, - Space = method.DeclaringType.FullName!, - 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)), - Void = IsVoid(method.ReturnParameter.ParameterType), - Async = IsTaskLike(method.ReturnParameter.ParameterType) - }; - - private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil) => new(param) { - Name = param.Name!, - JSName = param.Name == "function" ? "fn" : param.Name!, - Value = CreateValue(param.ParameterType, nil) - }; - - private ValueMeta CreateValue (Type type, NullabilityInfo nil) - { - 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 - }; - } - - private string BuildJSSpace (Type decl) - { - var space = decl.Namespace ?? ""; - var name = TrimGeneric(decl.Name); - if (decl.IsInterface) name = name[1..]; - var fullname = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; - return WithPrefs(prefs.Space, fullname, fullname); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs deleted file mode 100644 index 2f3eb1f9..00000000 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Reflection; -using System.Xml.Linq; - -namespace Bootsharp.Publish; - -internal sealed class SolutionInspector -{ - private readonly List staticInterfaces = []; - private readonly List instancedInterfaces = []; - private readonly List staticMembers = []; - 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; - - public SolutionInspector (Preferences prefs, string entryAssemblyName) - { - memberInspector = new(prefs, typeInspector, serdeInspector); - interfaceInspector = new(memberInspector, entryAssemblyName); - } - - /// - /// Inspects specified solution assembly paths in the output directory. - /// - /// Absolute path to directory containing compiled assemblies. - /// Absolute paths of the assemblies to inspect. - public SolutionInspection Inspect (string directory, IEnumerable paths) - { - var ctx = CreateLoadContext(directory); - foreach (var assemblyPath in paths) - try { InspectAssemblyFile(assemblyPath, ctx); } - catch (Exception e) { AddSkippedAssemblyWarning(assemblyPath, e); } - return CreateInspection(ctx); - } - - private void InspectAssemblyFile (string assemblyPath, MetadataLoadContext ctx) - { - var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); - if (!IsUserAssembly(assemblyName)) return; - InspectDocumentation(assemblyPath, assemblyName); - InspectAssembly(ctx.LoadFromAssemblyPath(assemblyPath)); - } - - private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception) - { - var fileName = Path.GetFileName(assemblyPath); - var message = $"Failed to inspect '{fileName}' assembly; " + - $"affected interop members won't be available in JavaScript. Error: {exception.Message}"; - warnings.Add(message); - } - - 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(), - Documentation = docs.ToArray(), - Warnings = warnings.ToArray() - }; - - private void InspectDocumentation (string assemblyPath, string assemblyName) - { - var xmlPath = Path.ChangeExtension(assemblyPath, ".xml"); - if (File.Exists(xmlPath)) docs.Add(new(assemblyName, XDocument.Load(xmlPath))); - } - - private void InspectAssembly (Assembly assembly) - { - foreach (var exported in assembly.GetExportedTypes()) - InspectExportedType(exported); - foreach (var attribute in assembly.CustomAttributes) - InspectAssemblyAttribute(attribute); - } - - private void InspectExportedType (Type type) - { - if (type.Namespace?.StartsWith("Bootsharp.Generated") ?? false) return; - foreach (var evt in type.GetEvents(BindingFlags.Public | BindingFlags.Static)) - InspectStaticEvent(evt); - foreach (var method in type.GetMethods(BindingFlags.Public | BindingFlags.Static)) - InspectStaticMethod(method); - } - - private void InspectAssemblyAttribute (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; - foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) - InspectStaticInterface((Type)arg.Value!, interop); - } - - private void InspectStaticMethod (MethodInfo 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); - } - } - - private void InspectStaticEvent (EventInfo 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 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)); - } -} diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs deleted file mode 100644 index 17077608..00000000 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/TypeInspector.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace Bootsharp.Publish; - -internal sealed class TypeInspector -{ - private readonly Dictionary byType = []; - - public TypeMeta Inspect (Type type) - { - return Crawl(type); - } - - public IReadOnlyCollection Collect () - { - return byType.Values.ToArray(); - } - - private TypeMeta Crawl (Type type) - { - if (byType.TryGetValue(type, out var meta)) return meta; - meta = byType[type] = new(type); - 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); - } - return meta; - } - - 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); - } -} 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 new file mode 100644 index 00000000..b826f237 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Emit/InstanceGenerator.cs @@ -0,0 +1,121 @@ +namespace Bootsharp.Publish; + +/// +/// 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 + + 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")}} + """; + + 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) => + $$""" + namespace {{(this.it = it).Namespace}} + { + public class {{it.Name}} (global::System.Int32 id) : {{it.Syntax}} + { + internal readonly global::System.Int32 _id = id; + + ~{{it.Name}}() => Instances.DisposeImported(_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 type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; + var space = $"global::Bootsharp.Generated.Interop.{it.FullName.Replace('.', '_')}"; + var getArgs = PrependIdArg(""); + var setArgs = PrependIdArg("value"); + return + $$""" + {{type}} {{it.Syntax}}.{{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 = $"{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/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..af078795 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -7,228 +7,208 @@ namespace Bootsharp.Publish; /// internal sealed class InteropGenerator { - private readonly HashSet registered = []; - private IReadOnlyCollection instanced = []; + [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 + private string id = null!, space = null!; + private InstancedMeta? it; + private ModuleMeta? md; + + 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 + { + [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() + .Where(e => e.Interop == InteropKind.Export) + .Select(e => EmitStaticEventSubscription(e, e.Space)), + ..spec.Modules.SelectMany(md => md.Members.OfType() + .Where(e => e.Interop == InteropKind.Export) + .Select(e => EmitStaticEventSubscription(e, md.FullName))), + ..spec.Static.OfType() + .Where(m => m.Interop == InteropKind.Import) + .Select(EmitStaticMethodAssignment) + ], 2)}} + } - {{new InteropInitializerGenerator().Generate(inspection)}} + {{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))))}} + } + """; - {{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 EmitStaticEventSubscription (EventMeta evt, string space) + { + var handler = $"Handle_{space.Replace('.', '_')}_{evt.Name}"; + return $"global::{space}.{evt.Name} += {handler};"; } - 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 static string EmitStaticMethodAssignment (MethodMeta method) + { + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; + } + + private IEnumerable EmitMember (MemberMeta member, InstancedMeta? it, ModuleMeta? md) + { + this.it = it; + this.md = md; + space = it?.FullName ?? md?.FullName ?? member.Space; + id = space.Replace('.', '_'); + 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 - var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; + 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});"; } private IEnumerable EmitEventImport (EventMeta evt) { - var inst = TryInstanced(evt, out var instance); - 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 (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::{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});"; } private IEnumerable EmitPropertyExport (PropertyMeta prop) { - var inst = TryInstanced(prop, out var instance); if (prop.CanGet) { - var attr = $"[JSExport] {MarshalAmbiguous(prop.Value, 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}" - : $"global::{prop.Space}.GetProperty{prop.Name}()"); - yield return $"{attr}internal static {BuildValueSyntax(prop.Value)} {name} ({args}) => {body};"; + var attr = $"[JSExport] {MarshalAmbiguous(prop.GetValue, true)}"; + var name = $"{id}_GetProperty{prop.Name}"; + var args = isIt ? $"{BuildSyntax(typeof(int))} _id" : ""; + 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};"; } 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}" - : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; + var name = $"{id}_SetProperty{prop.Name}"; + var args = BuildParameter(prop.SetValue, "value"); + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var value = Import(prop.SetValue, "value"); + var body = isIt + ? $"Instances.Exported<{it.Syntax}>(_id).{prop.Name} = {value}" + : $"global::{space}.SetProperty{prop.Name}({value})"; yield return $"[JSExport] internal static void {name} ({args}) => {body};"; } } 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 name = $"{id}_GetProperty{prop.Name}"; + var body = Import(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 name = $"{id}_SetProperty{prop.Name}"; + var args = $"{prop.SetValue.TypeSyntax} value"; + if (isIt) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var value = Export(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 name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var @return = BuildValueSyntax(method.Value); + var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; + 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))); - if (inst) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; - var invArgs = string.Join(", ", method.Arguments.Select(Deserialize)); - var invName = inst - ? $"Instances.Exported<{instance!.TypeSyntax}>(_id).{method.Name}" - : $"global::{method.Space}.{method.Name}"; - var body = Serialize(method.Value, $"{(wait ? "await " : "")}{invName}({invArgs})"); + if (isIt) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; + var invArgs = string.Join(", ", method.Arguments.Select(Import)); + var invName = isIt + ? $"Instances.Exported<{it.Syntax}>(_id).{method.Name}" + : $"global::{space}.{method.Name}"; + var body = Export(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 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))); - 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)}"; - 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) sigArgs = $"{BuildSyntax(typeof(int))} {PrependIdArg(sigArgs)}"; + var invArgs = string.Join(", ", method.Arguments.Select(Export)); + if (isIt) invArgs = PrependIdArg(invArgs); + var body = Import(method.Return, $"{(wait ? "await " : "")}{name}_Serialized({invArgs})"); yield return $"public static {@return} {name} ({sigArgs}) => {body};"; } - private string? EmitInstanceRegistrar (InterfaceMeta instance) - { - if (!registered.Add(instance)) return null; - var events = instance.Members.OfType().ToArray(); - return - $$""" - private static int Register ({{instance.TypeSyntax}} 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.IsInstance) return RegisterInstance(value, 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.InstanceType 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 (Serialized(value, out var id)) return $"Serializer.Deserialize({exp}, {id})"; - return 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; } @@ -249,30 +229,9 @@ 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 (ValueMeta value, 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})"; - 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 new file mode 100644 index 00000000..ed0966a3 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Emit/ModuleGenerator.cs @@ -0,0 +1,150 @@ +namespace Bootsharp.Publish; + +/// +/// Generates implementations for interop modules. +/// +internal sealed class ModuleGenerator +{ + private ModuleMeta md = null!; + + public string Generate (SolutionInspection spec) => + $$""" + #nullable enable + #pragma warning disable + + namespace Bootsharp.Generated + { + internal static class ModuleRegistrations + { + [System.Runtime.CompilerServices.ModuleInitializer] + internal static void RegisterModules () + { + {{Fmt(spec.Modules.Select(EmitRegistration), 3)}} + } + } + } + + {{Fmt(spec.Modules.Select(EmitModule), 0, "\n\n")}} + """; + + private string EmitRegistration (ModuleMeta md) + { + 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 (ModuleMeta md) + { + this.md = md; + if (md.Interop == InteropKind.Export) return EmitModuleExport(); + return EmitModuleImport(); + } + + private string EmitModuleExport () => + $$""" + namespace {{md.Namespace}} + { + public class {{md.Name}} + { + private static {{md.Syntax}} handler = null!; + + public {{md.Name}} ({{md.Syntax}} handler) + { + {{Fmt([ + $"{md.Name}.handler = handler;", + ..md.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") + ], 3)}} + } + + {{Fmt(md.Members.Select(EmitMemberExport), 2)}} + } + } + """; + + private string EmitModuleImport () => + $$""" + namespace {{md.Namespace}} + { + public class {{md.Name}} : {{md.Syntax}} + { + {{Fmt(md.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.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); + } + + private string EmitPropertyImport (PropertyMeta prop) + { + var space = $"global::Bootsharp.Generated.Interop.{md.FullName.Replace('.', '_')}"; + var type = (prop.GetValue ?? prop.SetValue!).TypeSyntax; + return + $$""" + {{type}} {{md.Syntax}}.{{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.Return.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 = $"{md.FullName.Replace('.', '_')}_{method.Name}"; + return $"{method.Return.TypeSyntax} {md.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..31b6e4a1 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; @@ -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})", - SerializedObjectMeta => $"new(Write_{meta.Id}, Read_{meta.Id})", - _ => ResolvePrimitive(meta.Type) + SerializedListMeta list => $"Serializer.{TrimGeneric(list.Clr.Name)}({list.Element.Id})", + SerializedDictionaryMeta dic => $"Serializer.{TrimGeneric(dic.Clr.Name)}({dic.Key.Id}, {dic.Value.Id})", + SerializedObjectMeta or SerializedInstanceMeta => $"new(Write_{meta.Id}, Read_{meta.Id})", + _ => ResolvePrimitive(meta.Clr) }};"; static string ResolvePrimitive (Type type) @@ -42,26 +42,43 @@ 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) { - 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 +94,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 +123,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 +132,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/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..496b74cc 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -5,63 +5,65 @@ 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 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 builder = new(); - private IReadOnlyCollection instanced = []; + private readonly StringBuilder bld = new(); 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)))) - .OrderBy(m => m.Namespace).ToArray(); + bindings = spec.Static + .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, 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.Space).ToArray(); 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(inspection.Serialized)); - builder.Append("\n\n"); + bld.Append(new BindingSerializerGenerator().Generate(spec.Serialized)); + bld.Append("\n\n"); - foreach (var instance in inspection.InstancedInterfaces - .Where(i => i.Interop == InteropKind.Import && i.Members.OfType().Any())) - EmitInstanceRegistrar(instance); - builder.Append("\n\n"); + foreach (var it in spec.Instanced.Where(i => i.Importer != null)) + EmitImporter(it); + bld.Append("\n\n"); - if (inspection.InstancedInterfaces.Count > 0) - builder.Append(new BindingClassGenerator().Generate(inspection.InstancedInterfaces)); + if (spec.Instanced.Count > 0) + 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 +75,7 @@ private void EmitImports () private void EmitDebugHelpers () { - builder.Append( + bld.Append( """ function getExport(name) { return (...args) => { @@ -97,7 +99,7 @@ function getImport(handler, serializedHandler, name) { private void EmitHelpers () { - builder.Append( + bld.Append( """ function importEvent(handler) { const event = new Event(); @@ -120,39 +122,39 @@ 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) 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 () { if (nextBinding is null) return true; - return nextBinding.Namespace != binding.Namespace; + return nextBinding.Space != binding.Space; } 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 () { 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; @@ -175,112 +177,107 @@ 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) + $"{ImportJS(arg)}{(arg.Value.Nullable ? " ?? undefined" : "")}")); + if (isIt) { - var invName = $"instances.export(_id, id => new {instance!.JSName}(id)).broadcast{evt.Name}"; - builder.Append($"{Br}{name}({PrependIdArg(args)}) {{ {invName}({invArgs}); }}"); + var invName = $"instances.export(_id, id => new {it.JSName}(id)).broadcast{evt.Name}"; + 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})"); } } private void EmitEventImport (EventMeta evt) { - if (TryInstanced(evt, out _)) return; // instanced import event handlers are emitted in the registrar - var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; + if (isIt) return; // instanced import event handlers are emitted in the registrar + 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)); - builder.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); + var invArgs = string.Join(", ", evt.Arguments.Select(ExportJS)); + bld.Append($"{Br}{evt.JSName}: importEvent(({args}) => {invName}({invArgs}))"); } private void EmitPropertyExport (PropertyMeta prop) { - var inst = TryInstanced(prop, out _); 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.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}; }}"); - else builder.Append($"{Br}get {prop.JSName}() {{ return {body}; }}"); + 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}; }}"); } 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.Value, "value"); - var body = inst ? $"{invName}(_id, {value})" : $"{invName}({value})"; - if (inst) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); - else builder.Append($"{Br}set {prop.JSName}(value) {{ {body}; }}"); + 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}; }}"); } } 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}"); - builder.Append($"{Br}getProperty{prop.Name}Serialized({args}) {{ return {body}; }}"); + if (!isIt) bld.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); + var args = isIt ? "_id" : ""; + 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 (!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}"; - builder.Append($"{Br}setProperty{prop.Name}Serialized({args}) {{ {body}; }}"); + if (!isIt) bld.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = 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}; }}"); } } private void EmitMethodExport (MethodMeta method) { - var inst = TryInstanced(method, out _); 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 (inst) 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})"); - builder.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({args}) => {body}"); + if (isIt) args = PrependIdArg(args); + var invArgs = string.Join(", ", method.Arguments.Select(ExportJS)); + if (isIt) invArgs = PrependIdArg(invArgs); + var body = ImportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); + bld.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); - 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})"); + if (isIt) args = PrependIdArg(args); + var invArgs = string.Join(", ", method.Arguments.Select(ImportJS)); + var invName = isIt ? $"instances.imported(_id).{name}" : $"this.{name}Handler"; + var body = ExportJS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; - if (inst) 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}; }}"); + 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}; }}"); } } @@ -290,26 +287,26 @@ 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 (InterfaceMeta instance) + private void EmitImporter (InstancedMeta it) { - var events = instance.Members.OfType().ToArray(); - builder.Append( + 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 = $"{e.Space.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}); }}"; }))}} }); @@ -318,54 +315,14 @@ 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.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.InstanceType is { } it) - { - 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.IsSerialized) return $"deserialize({exp}, {value.Serialized.Id})"; - return exp; - } - - private string RegisterInstance (ValueMeta value, 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) - { - 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; - } - 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)}"; 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/BindingGenerator/BindingSerializerGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs index e486ce6f..867f40fb 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs @@ -20,8 +20,8 @@ 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})", - _ => ResolvePrimitive(meta.Type) + SerializedObjectMeta or SerializedInstanceMeta => $"binary(write_{meta.Id}, read_{meta.Id})", + _ => ResolvePrimitive(meta.Clr) }};"; static string ResolvePrimitive (Type type) @@ -34,22 +34,37 @@ 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) { - if (!obj.Type.IsValueType) + if (!obj.Clr.IsValueType) { yield return "writer.writeBool(value != null);"; yield return "if (value == null) return;"; @@ -65,7 +80,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/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..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 membersGenerator = new(prefs); - private readonly TypeDeclarationGenerator typesGenerator = new(prefs); + private readonly ModuleDeclarationGenerator modules = 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), + 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 50% rename from src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs rename to src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs index 1bb6583d..d83b36c9 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/ModuleDeclarationGenerator.cs @@ -2,10 +2,10 @@ namespace Bootsharp.Publish; -internal sealed class MemberDeclarationGenerator (Preferences prefs) +internal sealed class ModuleDeclarationGenerator (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]; @@ -15,15 +15,15 @@ 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(); - 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,40 +59,39 @@ 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(evt.Info, a.Info)}")); + bld.Append("]>;"); } private void DeclareProperty (PropertyMeta prop) { - 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(';'); + bld.Append(docs.BuildProperty(prop.Info, 1)); + bld.Append($"\n export {(prop.CanGet && !prop.CanSet ? "const" : "let")} {prop.JSName}: "); + bld.Append(ts.BuildVariable(prop.Info)); + 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.Info)}")); + bld.Append($"): {ts.BuildReturn(method.Info)};"); } 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.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 1444cb92..c84fbe88 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text; @@ -5,61 +6,65 @@ namespace Bootsharp.Publish; internal sealed class TypeDeclarationGenerator (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 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 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 InterfaceMeta[] instanced = null!; - private Type[] types = null!; + private readonly Dictionary metaByClr = []; + private readonly List metas = []; private int index; - public string Generate (SolutionInspection inspection) + public string Generate (SolutionInspection spec) { - docs = new(inspection.Documentation); - instanced = [..inspection.InstancedInterfaces]; - types = inspection.Types.Select(t => t.Clr).Where(IsUserType).OrderBy(GetNamespace).ToArray(); - for (index = 0; index < types.Length; index++) + docs = new(spec.Documentation); + CollectMetas(spec); + for (index = 0; index < metas.Count; index++) DeclareType(); - return builder.ToString(); + return bld.ToString(); } - private Type GetTypeAt (int index) + private void CollectMetas (SolutionInspection spec) { - var type = types[index]; - return type.IsGenericType ? type.GetGenericTypeDefinition() : type; + 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.IsEnum) DeclareEnum(); - else DeclareInterface(); + 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 () @@ -67,119 +72,119 @@ private void CloseNamespace () AppendLine("}", 0); } - private void DeclareEnum () + private void DeclareEnum (SerializedEnumMeta enu) { - builder.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++) { - builder.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 DeclareInterface () + private void DeclareSerialized (SerializedObjectMeta obj) { - builder.Append(docs.BuildType(type, indent)); - AppendLine($"export interface {BuildTypeName(type)}", indent); - AppendExtensions(); - builder.Append(" {"); - if (instanced.FirstOrDefault(i => i.Type == type) is { } inst) - foreach (var member in inst.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); - } + bld.Append(docs.BuildType(obj.Clr, indent)); + AppendLine($"export type {ts.BuildName(obj.Clr)} = ", indent); + if (TryGetBase(obj.Clr, out var baseType)) + bld.Append(ts.BuildFullName(baseType)).Append(" & "); + bld.Append("Readonly<{"); + foreach (var prop in obj.Properties) + if (ShouldDeclareOn(obj.Clr, prop.Info)) + AppendProperty(prop); + AppendLine("}>;", indent); - private 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) - builder.Append(" extends ").AppendJoin(", ", extTypes.Select(t => typeBuilder.Build(t, null))); + void AppendProperty (SerializedPropertyMeta prop) + { + bld.Append(docs.BuildProperty(prop.Info, indent + 1)); + AppendLine(prop.JSName, indent + 1); + bld.Append(ts.BuildProperty(prop.Info)); + bld.Append(';'); + } } - private void AppendProperties () + private void DeclareInstanced (InstancedMeta it) { - 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)); - AppendProperty(ToFirstLower(prop.Name), prop.PropertyType, GetNullability(prop)); - } - } + bld.Append(docs.BuildType(it.Clr, indent)); + AppendLine($"export interface {ts.BuildName(it.Clr)}", indent); + AppendExtensions(); + bld.Append(" {"); + 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); - private void AppendProperty (string name, Type type, NullabilityInfo? nullability) - { - AppendLine(name, indent + 1); - if (IsNullable(type, nullability)) builder.Append('?'); - builder.Append(": "); - if (type.IsGenericTypeParameter) builder.Append(type.Name); - else builder.Append(typeBuilder.Build(type, nullability)); - builder.Append(';'); - } + void AppendExtensions () + { + 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)); + } - 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}: {typeBuilder.BuildArg(a)}")); - builder.Append("]>;"); - } + 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(evt.Info, a.Info)}")); + bld.Append("]>;"); + } - private void AppendInstancedProperty (PropertyMeta prop) - { - 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); - } + void AppendProperty (PropertyMeta prop) + { + bld.Append(docs.BuildProperty(prop.Info, indent + 1)); + var name = !prop.CanSet ? $"readonly {prop.JSName}" : prop.JSName; + AppendLine(name, indent + 1); + bld.Append(ts.BuildProperty(prop.Info)); + bld.Append(';'); + } - 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.Append("): "); - builder.Append(typeBuilder.BuildReturn(meta)); - builder.Append(';'); + 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.Info)}")); + bld.Append("): "); + bld.Append(ts.BuildReturn(meta.Info)); + 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 GetNamespace (TypeMeta meta) + { + return BuildJSSpace(meta.Clr, prefs); } - private string BuildTypeName (Type type) + private bool TryGetBase (Type clr, [NotNullWhen(true)] out Type? baseType) { - if (!type.IsGenericType) return type.Name; - var name = TrimGeneric(type.Name); - var args = string.Join(", ", type.GetGenericArguments().Select(BuildTypeName)); - return $"{name}<{args}>"; + if ((baseType = clr.BaseType) == null || !IsUserType(baseType)) return false; + return metaByClr.ContainsKey(baseType); } - private string GetNamespace (Type type) + private bool ShouldDeclareOn (Type host, MemberInfo member) { - return BuildJSSpace(type, prefs); + if (member.DeclaringType == host) return true; + return !TryGetBase(member.DeclaringType!, out _) && !TryGetBase(host, out _); } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index 8a367327..5102dc86 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -4,30 +4,74 @@ namespace Bootsharp.Publish; internal sealed class TypeSyntaxBuilder (Preferences prefs) { - private NullabilityInfo? nullability; + private NullabilityInfo? nullity; - 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.Value.Nullable ? " | null" : ""; - return Build(method.Value.Type.Clr, method.Value.Nullability) + nil; + if (type.IsGenericType) type = type.GetGenericTypeDefinition(); + return Build(type, null); } - public string Build (Type type, NullabilityInfo? nullability) + public string BuildArg (ParameterInfo param) { - this.nullability = nullability; - // nullability of topmost declarations is handled upstream (?/undefined/null) - if (IsNullable(type, nullability, out var value)) type = value; + 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(evt, param); + 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? nullity) + { + this.nullity = nullity; + // nullability of topmost declarations is handled downstream (?/undefined/null) + if (IsNullable(type, nullity, 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 +124,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}>"; } @@ -103,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; } } 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/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/cs/Directory.Build.props b/src/cs/Directory.Build.props index ffad2bc0..13f7408f 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.172 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..45e07f59 --- /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..894ff4d7 --- /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 b5572e26..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)Bootsharp.Interfaces.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..e4180fcf --- /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 fd39faab..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", () => { @@ -223,26 +232,41 @@ 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 () => { - 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(); + it("can interop with imported modules", async () => { + 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 interface instances", async () => { - Test.Types.ImportedStatic.getInstanceAsync = async (arg) => { + it("can interop with imported instances", async () => { + 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 static interfaces", () => { - 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 interface instances", async () => { - const exported = await Test.Types.ExportedStatic.getInstanceAsync("bar"); + it("can interop with exported instances", async () => { + 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", () => {