From 5cfd4f85e32bac9710b1d829cf76334d55635c02 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 23 Apr 2026 14:46:56 +0300 Subject: [PATCH 01/10] update agents --- AGENTS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index ab85b2e2..67661eaf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ IMPORTANT: NEVER RUN ANY BUILD/PUBLISH COMMANDS IN PARALLEL. # Packaging Bootsharp -Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after updating JS or C# code. +Follow these steps exactly and sequentially whenever the Bootsharp package consumed by other projects must be actualized, or when running the JS end-to-end tests after updating the package's C# or JS code. 1. Build the JS package with `npm run build` under `src/js`. 2. Bump the Bootsharp library alpha version in `src/cs/Directory.Build.props` @@ -23,6 +23,8 @@ Follow these steps exactly and sequentially whenever the Bootsharp package consu Important: Always execute these steps in order, do not parallelize them. +Note: Bumping the package version is only required after modifying the package's C# or JS sources. If you're only editing E2E tests, there is no need to follow the full repackaging procedure each time. + # Code Coverage We have a strict 100% coverage policy for both the C# and JS codebases. From 1693b29bc610bfc1587e58bd548024e70668d625 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 23 Apr 2026 17:07:45 +0300 Subject: [PATCH 02/10] rename attributes, support events --- AGENTS.md | 7 +- docs/guide/declarations.md | 46 +- docs/guide/emit-prefs.md | 8 +- docs/guide/events.md | 30 +- docs/guide/extensions/dependency-injection.md | 4 +- docs/guide/getting-started.md | 12 +- docs/guide/interop-instances.md | 5 +- docs/guide/interop-interfaces.md | 22 +- docs/guide/namespaces.md | 16 +- docs/guide/nullability.md | 16 +- docs/guide/serialization.md | 12 +- samples/bench/bootsharp/Program.cs | 16 +- samples/bench/bootsharp/init.mjs | 8 +- samples/minimal/cs/Program.cs | 12 +- .../react/backend/Backend.Prime/IPrimeUI.cs | 6 - samples/react/backend/Backend.Prime/Prime.cs | 10 +- samples/react/backend/Backend.WASM/Program.cs | 6 +- samples/react/backend/Backend/IComputer.cs | 4 + samples/trimming/cs/Program.cs | 2 +- src/cs/Bootsharp.Common.Test/InstancesTest.cs | 57 +- src/cs/Bootsharp.Common.Test/TypesTest.cs | 20 +- .../Attributes/ExportAttribute.cs | 45 ++ .../Attributes/ImportAttribute.cs | 43 ++ .../Attributes/JSEventAttribute.cs | 15 - .../Attributes/JSExportAttribute.cs | 36 - .../Attributes/JSFunctionAttribute.cs | 17 - .../Attributes/JSImportAttribute.cs | 37 - .../Attributes/JSInvokableAttribute.cs | 14 - ...esAttribute.cs => PreferencesAttribute.cs} | 16 +- .../Build/Bootsharp.Common.props | 2 +- .../Interop/ExportInterface.cs | 2 +- .../Interop/ImportInterface.cs | 2 +- src/cs/Bootsharp.Common/Interop/Instances.cs | 64 +- src/cs/Bootsharp.Common/Interop/Interfaces.cs | 6 +- src/cs/Bootsharp.Common/Interop/Proxies.cs | 7 +- src/cs/Bootsharp.Generate.Test/EventTest.cs | 43 -- .../Bootsharp.Generate.Test/GeneratorTest.cs | 42 +- .../ImportEventTest.cs | 57 ++ .../{FunctionTest.cs => ImportMethodTest.cs} | 88 +-- src/cs/Bootsharp.Generate/Global.cs | 35 + src/cs/Bootsharp.Generate/ImportClass.cs | 59 ++ src/cs/Bootsharp.Generate/ImportEvent.cs | 18 + src/cs/Bootsharp.Generate/ImportMethod.cs | 38 + src/cs/Bootsharp.Generate/PartialClass.cs | 62 -- src/cs/Bootsharp.Generate/PartialMethod.cs | 75 -- src/cs/Bootsharp.Generate/SourceGenerator.cs | 20 +- src/cs/Bootsharp.Generate/SyntaxReceiver.cs | 35 +- .../Emit/DependenciesTest.cs | 24 +- .../Emit/InterfacesTest.cs | 173 ++--- .../Emit/InteropTest.cs | 467 ++++++++----- .../Emit/SerializerTest.cs | 64 +- .../Mock/MockProject.cs | 2 +- .../Pack/BindingTest.cs | 471 +++++++------ .../Pack/DeclarationTest.cs | 647 ++++++++++-------- .../Pack/SolutionInspectionTest.cs | 14 +- .../Common/Global/GlobalInspection.cs | 16 + .../Common/Global/GlobalText.cs | 10 +- .../Common/Global/GlobalType.cs | 65 +- .../Common/Meta/InterfaceMeta.cs | 17 +- .../Common/Meta/MemberMeta.cs | 20 +- .../Common/Preferences/Preferences.cs | 10 +- .../Common/Preferences/PreferencesResolver.cs | 9 +- .../SolutionInspector/InspectionReporter.cs | 12 +- .../SolutionInspector/InterfaceInspector.cs | 77 ++- .../SolutionInspector/MemberInspector.cs | 43 +- .../SolutionInspector/SerializedInspector.cs | 4 +- .../SolutionInspector/SolutionInspection.cs | 12 +- .../SolutionInspector/SolutionInspector.cs | 64 +- .../Emit/DependencyGenerator.cs | 16 +- .../Emit/InterfaceGenerator.cs | 115 ++-- .../Emit/InteropGenerator.cs | 297 ++++---- .../Emit/InteropInitializerGenerator.cs | 61 +- .../Emit/SerializerGenerator.cs | 12 +- .../BindingGenerator/BindingClassGenerator.cs | 28 +- .../Pack/BindingGenerator/BindingGenerator.cs | 271 +++++--- .../BindingSerializerGenerator.cs | 8 +- .../DeclarationGenerator.cs | 4 +- .../DocumentationBuilder.cs | 34 +- .../MemberDeclarationGenerator.cs | 21 +- .../TypeDeclarationGenerator.cs | 23 +- .../DeclarationGenerator/TypeSyntaxBuilder.cs | 14 +- .../Pack/ResourceGenerator.cs | 8 +- src/cs/Directory.Build.props | 2 +- src/js/src/imports.ts | 5 +- src/js/src/instances.ts | 77 ++- .../Interfaces/ExportedInstanced.cs | 4 +- .../Test.Types/Interfaces/ExportedStatic.cs | 5 +- .../Interfaces/IExportedInstanced.cs | 3 + .../Test.Types/Interfaces/IExportedStatic.cs | 4 + .../Interfaces/IImportedInstanced.cs | 3 + .../Test.Types/Interfaces/IImportedStatic.cs | 6 +- .../cs/Test.Types/Interfaces/Interfaces.cs | 42 +- src/js/test/cs/Test.Types/RecordChanged.cs | 3 + src/js/test/cs/Test.Types/Vehicle/Registry.cs | 41 +- src/js/test/cs/Test/Event.cs | 33 +- src/js/test/cs/Test/Functions.cs | 85 +-- src/js/test/cs/Test/Invokable.cs | 39 +- src/js/test/cs/Test/Platform.cs | 12 +- src/js/test/cs/Test/Program.cs | 6 +- src/js/test/cs/Test/Serialization.cs | 42 +- src/js/test/spec/interop.spec.ts | 160 +++-- 101 files changed, 2711 insertions(+), 2191 deletions(-) create mode 100644 src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs create mode 100644 src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs delete mode 100644 src/cs/Bootsharp.Common/Attributes/JSEventAttribute.cs delete mode 100644 src/cs/Bootsharp.Common/Attributes/JSExportAttribute.cs delete mode 100644 src/cs/Bootsharp.Common/Attributes/JSFunctionAttribute.cs delete mode 100644 src/cs/Bootsharp.Common/Attributes/JSImportAttribute.cs delete mode 100644 src/cs/Bootsharp.Common/Attributes/JSInvokableAttribute.cs rename src/cs/Bootsharp.Common/Attributes/{JSPreferencesAttribute.cs => PreferencesAttribute.cs} (75%) delete mode 100644 src/cs/Bootsharp.Generate.Test/EventTest.cs create mode 100644 src/cs/Bootsharp.Generate.Test/ImportEventTest.cs rename src/cs/Bootsharp.Generate.Test/{FunctionTest.cs => ImportMethodTest.cs} (63%) create mode 100644 src/cs/Bootsharp.Generate/Global.cs create mode 100644 src/cs/Bootsharp.Generate/ImportClass.cs create mode 100644 src/cs/Bootsharp.Generate/ImportEvent.cs create mode 100644 src/cs/Bootsharp.Generate/ImportMethod.cs delete mode 100644 src/cs/Bootsharp.Generate/PartialClass.cs delete mode 100644 src/cs/Bootsharp.Generate/PartialMethod.cs create mode 100644 src/js/test/cs/Test.Types/RecordChanged.cs diff --git a/AGENTS.md b/AGENTS.md index 67661eaf..ff191125 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,8 @@ - Keep the code lean and efficient, including the use of `unsafe` when it is justified. - Use the latest available .NET and C# features when they improve the code and fit the existing style. -- Avoid defensive programming and compatibility overhead. Target only the modern 32-bit WASM runtime, current JS specs, and current browser capabilities. -- Follow the existing code style, architecture, project structure, naming, and formatting strictly. -- Do not stop at analysis or a partial fix. If the task requires code or verification, carry it through to the expected result. +- Avoid defensive programming and compatibility overhead. Target only the modern WASM runtime, current JS specs and current browser capabilities. +- Follow the existing code style, architecture, project structure, naming and formatting strictly. - If clarification is required, use the question tool instead of guessing. IMPORTANT: NEVER RUN ANY BUILD/PUBLISH COMMANDS IN PARALLEL. @@ -31,7 +30,7 @@ We have a strict 100% coverage policy for both the C# and JS codebases. - Tests must be meaningful and cover real behavior. - Do not add fake tests just to satisfy the numbers. -- No unreachable code is allowed, except in rare cases where testing is not practical. In those cases, `[ExcludeFromCodeCoverage]` may be used deliberately. +- No unreachable code is allowed, except in rare cases where testing is not practical. In those cases, ask how to proceed. - Treat branch coverage as part of the requirement, not just line coverage. To check C# coverage, use `reportgenerator` on merged coverlet output. Example workflow reference: `src/cs/.scripts/cover.sh`. Do not run that script verbatim in automation; it is intended for interactive usage. diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 6148fff6..f967e4d0 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -6,12 +6,12 @@ Bootsharp will automatically generate [type declarations](https://www.typescript For the interop methods, function declarations are emitted. -Exported `[JSInvokable]` methods will have associated function assigned under the declaring type space: +Exported methods will have associated function assigned under the declaring type space: ```csharp public class Foo { - [JSInvokable] + [Export] public static void Bar() { } } ``` @@ -32,14 +32,14 @@ import { Foo } from "bootsharp"; Foo.bar(); ``` -Imported `[JSFunction]` methods will be emitted as properties, which have to be assigned before booting the runtime: +Imported methods will be emitted as properties, which have to be assigned before booting the runtime: ::: code-group ```csharp [Foo.cs] public partial class Foo { - [JSFunction] + [Import] public static partial void Bar(); } ``` @@ -60,21 +60,21 @@ Foo.bar = () => {}; ## Event Declarations -`[JSEvent]` methods will be emitted as objects with `subscribe` and `unsubscribe` methods: +Exported events are emitted as `EventSubscriber` objects: ::: code-group ```csharp [Foo.cs] public class Foo { - [JSEvent] - public static partial void OnBar (string payload); + [Export] + public static event Action? OnBar; } ``` ```ts [bindings.d.ts] export namespace Foo { - export const onBar: Event<[string]>; + export const onBar: EventSubscriber<[payload: string]>; } ``` @@ -86,6 +86,32 @@ Foo.onBar.subscribe(payload => {}); ::: +Imported events are emitted as `EventBroadcaster` objects: + +::: code-group + +```csharp [Foo.cs] +public static partial class Foo +{ + [Import] + public static event Action? OnBar; +} +``` + +```ts [bindings.d.ts] +export namespace Foo { + export const onBar: EventBroadcaster<[payload: string]>; +} +``` + +```ts [main.ts] +import { Foo } from "bootsharp"; + +Foo.onBar.broadcast("updated"); +``` + +::: + ## Documentation Declarations When an inspected assembly has XML documentation generated, Bootsharp mirrors the matching documentation into the emitted TypeScript declarations. @@ -100,7 +126,7 @@ public class MathApi /// Left number. /// Right number. /// The sum. - [JSInvokable] + [Export] public static int Add (int left, int right) => left + right; } ``` @@ -146,7 +172,7 @@ public record Bar (Foo foo); public partial class Foo { - [JSFunction] + [Import] public static partial Bar GetBar(); } ``` diff --git a/docs/guide/emit-prefs.md b/docs/guide/emit-prefs.md index a1570aaf..9a05eaa2 100644 --- a/docs/guide/emit-prefs.md +++ b/docs/guide/emit-prefs.md @@ -1,6 +1,6 @@ # Emit Preferences -Use `[JSPreferences]` 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. +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. ## Space @@ -9,7 +9,7 @@ By default, all the generated JavaScript binding objects and TypeScript declarat To customize emitted spaces, use `Space` parameter. For example, to make all bindings declared under "Foo.Bar" C# namespace have "Baz" namespace in JavaScript: ```cs -[assembly: JSPreferences( +[assembly: Preferences( Space = ["^Foo\.Bar\.(\S+)", "Baz.$1"] )] ``` @@ -24,10 +24,6 @@ The patterns are matched against full type name of declaring C# type when genera Allows customizing generated TypeScript type syntax. The patterns are matched against full C# type names of interop method arguments, return values and object properties. -## Event - -Used to customize which C# methods should be transformed into JavaScript events, as well as generated event names. The patterns are matched against C# method names declared under `[JSImport]` interfaces. By default, methods starting with "Notify..." are matched and renamed to "On...". - ## Function Customizes generated JavaScript function names. The patterns are matched against C# interop method names. diff --git a/docs/guide/events.md b/docs/guide/events.md index cd0f55cd..83803f4a 100644 --- a/docs/guide/events.md +++ b/docs/guide/events.md @@ -1,10 +1,15 @@ # Events -To make a C# method act as event broadcaster for JavaScript consumers, annotate it with `[JSEvent]` attribute: +To expose a C# event to JavaScript consumers, declare a static event and annotate it with `[Export]`: ```csharp -[JSEvent] -public static partial void OnSomethingChanged (string payload); +[Export] +public static event Action? OnSomethingChanged; + +public static void UpdateSomething (string payload) +{ + OnSomethingChanged?.Invoke(payload); +} ``` — and consume it from JavaScript as follows: @@ -18,13 +23,24 @@ function handleSomething(payload: string) { } ``` -When the method in invoked in C#, subscribed JavaScript handlers will be notified. In TypeScript the event will have typed generic declaration corresponding to the event arguments. +When the event is raised in C#, subscribed JavaScript handlers will be notified. In TypeScript exported events are declared as `EventSubscriber<...>` with argument types inferred from the event delegate signature. + +To use a JavaScript event from C#, declare a static event on a partial type and annotate it with `[Import]`: -## Events in Interop Interfaces +```csharp +[Import] +public static event Action? OnSomethingChanged; +``` + +JavaScript will see it as an `EventBroadcaster`: + +```ts +Program.onSomethingChanged.broadcast("updated"); +``` -To make a method in an [interop interface](/guide/interop-interfaces) act as event broadcaster, make its name start with "Notify". Such methods will be detected by Bootsharp and exposed to JavaScript as events with "Notify" changed to "On". For example, `NotifyUserUpdated` C# method will be exposed as `OnUserUpdated` JavaScript event. +Bootsharp supports all common event types: `Action`, `EventHandler`, and any custom delegate types without a return type. -Which interface methods are considered events and the way they are named in JavaScript can be customized with [emit preferences](/guide/emit-prefs). +Events on the [interop interfaces](/guide/interop-interfaces) 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 ae6c7125..20d27b57 100644 --- a/docs/guide/extensions/dependency-injection.md +++ b/docs/guide/extensions/dependency-injection.md @@ -28,12 +28,12 @@ using Bootsharp; using Bootsharp.Inject; using Microsoft.Extensions.DependencyInjection; -[assembly: JSExport( +[assembly: Export( typeof(IExported), // other APIs to export to JavaScript )] -[assembly: JSImport( +[assembly: Import( typeof(IImported), // other APIs to import from JavaScript )] diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index c658e4cd..528d51e8 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -30,18 +30,18 @@ using Bootsharp; public static partial class Program { + [Export] // Used in JS as Program.onMainInvoked.subscribe(..) + public static event Action? OnMainInvoked; + public static void Main () { - OnMainInvoked($"Hello {GetFrontendName()}, .NET here!"); + OnMainInvoked?.Invoke($"Hello {GetFrontendName()}, .NET here!"); } - [JSEvent] // Used in JS as Program.onMainInvoked.subscribe(..) - public static partial void OnMainInvoked (string message); - - [JSFunction] // Set in JS as Program.getFrontendName = () => .. + [Import] // Set in JS as Program.getFrontendName = () => .. public static partial string GetFrontendName (); - [JSInvokable] // Invoked from JS as Program.GetBackendName() + [Export] // Invoked from JS as Program.GetBackendName() public static string GetBackendName () => Environment.Version; } ``` diff --git a/docs/guide/interop-instances.md b/docs/guide/interop-instances.md index 8f3818ac..9c14120a 100644 --- a/docs/guide/interop-instances.md +++ b/docs/guide/interop-instances.md @@ -23,8 +23,8 @@ public class Exported : IExported public static partial class Factory { - [JSInvokable] public static IExported GetExported () => new Exported(); - [JSFunction] public static partial IImported GetImported (); + [Export] public static IExported GetExported () => new Exported(); + [Import] public static partial IImported GetImported (); } var imported = Factory.GetImported(); @@ -51,5 +51,4 @@ _ = exported.value; // invokes the C# getter Interop instances are subject to the following limitations: - Can't be args or return values of other interop instance method -- Can't be args of events - Interfaces from "System" namespace are not qualified diff --git a/docs/guide/interop-interfaces.md b/docs/guide/interop-interfaces.md index e148ca61..06a14b91 100644 --- a/docs/guide/interop-interfaces.md +++ b/docs/guide/interop-interfaces.md @@ -1,23 +1,20 @@ # Interop Interfaces -Instead of manually authoring a binding for each method, let Bootsharp generate them automatically using the `[JSImport]` and `[JSExport]` assembly attributes. +Instead of manually authoring a binding for each member, let Bootsharp generate them automatically using the `[Import]` and `[Export]` assembly attributes. -For example, say we have a JavaScript UI (frontend) that needs to be notified when data is mutated in the C# domain layer (backend), so it can render the updated state. Additionally, the frontend may have a setting (e.g., stored in the browser cache) to temporarily mute notifications, which the backend needs to retrieve. You can create the following interface in C# to describe the expected frontend APIs: +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: ```csharp interface IFrontend { bool IsMuted { get; set; } - void NotifyDataChanged (Data data); } ``` Now, add the interface type to the JS import list: ```csharp -[assembly: JSImport([ - typeof(IFrontend) -])] +[assembly: Import(typeof(IFrontend))] ``` Bootsharp will automatically implement the interface in C#, wiring it to JavaScript, while also providing you with a TypeScript spec to implement on the frontend: @@ -25,15 +22,15 @@ Bootsharp will automatically implement the interface in C#, wiring it to JavaScr ```ts export namespace Frontend { export let isMuted: boolean; - export const onDataChanged: Event<[Data]>; } ``` -Now, say we want to provide an API for the frontend to request a mutation of the data: +Now, export the backend contract to JavaScript: ```csharp interface IBackend { + event Action OnDataChanged; Data? Current { get; set; } void AddData (Data data); } @@ -42,20 +39,21 @@ interface IBackend Export the interface to JavaScript: ```csharp -[assembly: JSExport([ - typeof(IBackend) -])] +[assembly: Export(typeof(IBackend))] ``` This will produce the following spec to be consumed on the JavaScript side: ```ts export namespace Backend { - export let current: Data | null; + export const onDataChanged: EventSubscriber<[data: Data]>; + export let current: Data | undefined; export function addData(data: Data): void; } ``` +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. + To make Bootsharp automatically inject and initialize the generated interop implementations, use the [dependency injection](/guide/extensions/dependency-injection) extension. ::: tip Example diff --git a/docs/guide/namespaces.md b/docs/guide/namespaces.md index 4487b787..fa592c1c 100644 --- a/docs/guide/namespaces.md +++ b/docs/guide/namespaces.md @@ -7,9 +7,9 @@ Bootsharp maps generated binding APIs based on the name of the associated C# typ Full type name (including namespace) of the declaring type of the static interop method is mapped into JavaScript object name: ```csharp -class Class { [JSInvokable] static void Method() {} } -namespace Foo { class Class { [JSInvokable] static void Method() {} } } -namespace Foo.Bar { class Class { [JSInvokable] static void Method() {} } } +class Class { [Export] static void Method() {} } +namespace Foo { class Class { [Export] static void Method() {} } } +namespace Foo.Bar { class Class { [Export] static void Method() {} } } ``` ```ts @@ -27,7 +27,7 @@ namespace Foo; public class Class { - public class Nested { [JSInvokable] public static void Method() {} } + public class Nested { [Export] public static void Method() {} } } ``` @@ -42,11 +42,11 @@ Foo.Class.Nested.method(); 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. ```csharp -[JSExport([ +[Export( typeof(IExported), typeof(Foo.IExported), - typeof(Foo.Bar.IExported), -])] + typeof(Foo.Bar.IExported) +)] interface IExported { void Method(); } namespace Foo { interface IExported { void Method(); } } @@ -71,7 +71,7 @@ namespace Foo { public record Record; } partial class Class { - [JSFunction] + [Import] public static partial Record Method(Foo.Record r); } ``` diff --git a/docs/guide/nullability.md b/docs/guide/nullability.md index cdfddde9..c0a3e3dc 100644 --- a/docs/guide/nullability.md +++ b/docs/guide/nullability.md @@ -23,7 +23,7 @@ Nullable method arguments are emitted as `| undefined`. ::: code-group ```csharp [C#] -[JSInvokable] +[Export] public static void SetTitle (string? title) { } ``` @@ -49,7 +49,7 @@ Nullable method return values are emitted as `| null`. ::: code-group ```csharp [C#] -[JSInvokable] +[Export] public static string? FindUserName (int id) => null; ``` @@ -102,10 +102,10 @@ Nullable collection elements are emitted as `| null`. ::: code-group ```csharp [C#] -[JSInvokable] +[Export] public static string?[]? EchoNames (string?[]? names) => names; -[JSInvokable] +[Export] public static List? EchoNumbers (List? numbers) => numbers; ``` @@ -132,7 +132,7 @@ Nullable dictionary values are also emitted as `| null`. ::: code-group ```csharp [C#] -[JSInvokable] +[Export] public static Dictionary? GetLabels () => new () { ["a"] = "Ready", ["b"] = null }; ``` @@ -161,13 +161,13 @@ Events are the one special case where missing nullable payload values are expose ::: code-group ```csharp [C#] -[JSEvent] -public static partial void OnVehicleChanged (int id, Vehicle? vehicle); +[Export] +public static event Action? OnVehicleChanged; ``` ```ts [TypeScript] export namespace Program { - export const onVehicleChanged: Event<[id: number, vehicle: Vehicle | undefined]>; + export const onVehicleChanged: EventSubscriber<[id: number, vehicle: Vehicle | undefined]>; } ``` diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index 4ac39965..c5d23b92 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -18,11 +18,11 @@ When a value of non-natively supported type is specified in an interop API, Boot ```csharp public record User (long Id, string Name, DateTime Registered); -[JSInvokable] +[Export] public static void AddUser (User user) { } -[JSEvent] -public static partial void OnUserModified (User user); +[Export] +public static event Action? OnUserModified; ``` — Bootsharp will automatically emit C# and JavaScript code required to de-/serialize `User` record on both ends, so that you can consume the APIs as if they were initially authored in JavaScript: @@ -44,7 +44,7 @@ Enums are marshalled as numbers for better performance, while additional name <- ```csharp public enum Options { Foo, Bar } -[JSInvokable] +[Export] public static Options GetOption () => Options.Bar; ``` @@ -65,7 +65,7 @@ console.log(Program.Options[1]); // "Bar" Bootsharp marshals C# dictionaries as ES6 [Map](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map): ```csharp -[JSInvokable] +[Export] public static Dictionary GetMap () => new () { ["foo"] = true, ["bar"] = false }; ``` @@ -85,7 +85,7 @@ console.log(map.get("bar")); // false It's common to use various collection interfaces, such as `IReadOnlyList` or `IReadOnlyDictionary` when authoring C# APIs. Bootsharp will accept any kind of array or dictionary compatible interface in the interop APIs and marshal them as plain arrays and maps by default: ```csharp -[JSInvokable] +[Export] public static IReadOnlyDictionary Map ( IReadOnlyList a, IReadOnlyCollection b) { } ``` diff --git a/samples/bench/bootsharp/Program.cs b/samples/bench/bootsharp/Program.cs index 2a2af115..eff5cf2d 100644 --- a/samples/bench/bootsharp/Program.cs +++ b/samples/bench/bootsharp/Program.cs @@ -2,12 +2,12 @@ using Bootsharp.Inject; using Microsoft.Extensions.DependencyInjection; -[assembly: JSImport(typeof(IImport))] -[assembly: JSExport(typeof(IExport))] +[assembly: Import(typeof(IImported))] +[assembly: Export(typeof(IExported))] new ServiceCollection() .AddBootsharp() - .AddSingleton() + .AddSingleton() .BuildServiceProvider() .RunBootsharp(); @@ -19,23 +19,23 @@ public struct Data public string[] Messages { get; set; } } -public interface IImport +public interface IImported { int GetNumber (); Data GetStruct (); } -public interface IExport +public interface IExported { int EchoNumber (); Data EchoStruct (); int Fi (int n); } -public class Export (IImport import) : IExport +public class Exported (IImported imported) : IExported { - public int EchoNumber () => import.GetNumber(); - public Data EchoStruct () => import.GetStruct(); + public int EchoNumber () => imported.GetNumber(); + public Data EchoStruct () => imported.GetStruct(); public int Fi (int n) => F(n); // Due to heavy recursion, a significant degradation accumulates due to constant // dereferencing of the instance on each iteration, hence using the static version. diff --git a/samples/bench/bootsharp/init.mjs b/samples/bench/bootsharp/init.mjs index 9b455f81..bbc4c461 100644 --- a/samples/bench/bootsharp/init.mjs +++ b/samples/bench/bootsharp/init.mjs @@ -1,12 +1,12 @@ -import bootsharp, { Export, Import } from "./bin/bootsharp/index.mjs"; +import bootsharp, { Exported, Imported } from "./bin/bootsharp/index.mjs"; import { getNumber, getStruct } from "../fixtures.mjs"; /** @returns {Promise} */ export async function init() { - Import.getNumber = getNumber; - Import.getStruct = getStruct; + Imported.getNumber = getNumber; + Imported.getStruct = getStruct; await bootsharp.boot(); - return { ...Export }; + return { ...Exported }; } diff --git a/samples/minimal/cs/Program.cs b/samples/minimal/cs/Program.cs index 9a3f7bac..9ff26511 100644 --- a/samples/minimal/cs/Program.cs +++ b/samples/minimal/cs/Program.cs @@ -3,17 +3,17 @@ public static partial class Program { + [Export] // Used in JS as Program.onMainInvoked.subscribe(...) + public static event Action? OnMainInvoked; + public static void Main () { - OnMainInvoked($"Hello {GetFrontendName()}, .NET here!"); + OnMainInvoked?.Invoke($"Hello {GetFrontendName()}, .NET here!"); } - [JSEvent] // Used in JS as Program.onMainInvoked.subscribe(...) - public static partial void OnMainInvoked (string message); - - [JSFunction] // Assigned in JS as Program.getFrontendName = ... + [Import] // Assigned in JS as Program.getFrontendName = ... public static partial string GetFrontendName (); - [JSInvokable] // Invoked from JS as Program.GetBackendName() + [Export] // Invoked from JS as Program.GetBackendName() public static string GetBackendName () => $".NET {Environment.Version}"; } diff --git a/samples/react/backend/Backend.Prime/IPrimeUI.cs b/samples/react/backend/Backend.Prime/IPrimeUI.cs index 1e7b7661..c7762b60 100644 --- a/samples/react/backend/Backend.Prime/IPrimeUI.cs +++ b/samples/react/backend/Backend.Prime/IPrimeUI.cs @@ -7,10 +7,4 @@ namespace Backend.Prime; public interface IPrimeUI { Options GetOptions (); - // Imported methods starting with "Notify" will automatically - // be converted to JavaScript events and renamed to "On...". - // This can be configured with "JSImport.EventPattern" and - // "JSImport.EventReplacement" attribute parameters. - void NotifyComputing (bool computing); - void NotifyComplete (long time); } diff --git a/samples/react/backend/Backend.Prime/Prime.cs b/samples/react/backend/Backend.Prime/Prime.cs index 6129c4ae..deeda378 100644 --- a/samples/react/backend/Backend.Prime/Prime.cs +++ b/samples/react/backend/Backend.Prime/Prime.cs @@ -1,3 +1,4 @@ +using System; using System.Diagnostics; namespace Backend.Prime; @@ -7,6 +8,9 @@ namespace Backend.Prime; public class Prime (IPrimeUI ui) : IComputer { + public event Action? OnComputing; + public event Action? OnComplete; + private static readonly SemaphoreSlim semaphore = new(0); private readonly Stopwatch watch = new(); private CancellationTokenSource? cts; @@ -15,12 +19,12 @@ public void StartComputing () { cts?.Cancel(); cts = new CancellationTokenSource(); - cts.Token.Register(() => ui.NotifyComputing(false)); + cts.Token.Register(() => OnComputing?.Invoke(false)); var options = ui.GetOptions(); if (!options.Multithreading) ComputeLoop(options.Complexity, cts.Token); else new Thread(() => ComputeLoop(options.Complexity, cts.Token)).Start(); ObserveLoop(cts.Token); - ui.NotifyComputing(true); + OnComputing?.Invoke(true); } public void StopComputing () => cts?.Cancel(); @@ -37,7 +41,7 @@ private async void ObserveLoop (CancellationToken token) finally { watch.Stop(); - ui.NotifyComplete(watch.ElapsedMilliseconds); + OnComplete?.Invoke(watch.ElapsedMilliseconds); } await Task.Delay(1); } diff --git a/samples/react/backend/Backend.WASM/Program.cs b/samples/react/backend/Backend.WASM/Program.cs index 52d943e4..bcd431b5 100644 --- a/samples/react/backend/Backend.WASM/Program.cs +++ b/samples/react/backend/Backend.WASM/Program.cs @@ -8,11 +8,11 @@ // and can be shared with other build targets (console, MAUI, etc). // Generate C# -> JavaScript interop handlers for specified contracts. -[assembly: JSExport(typeof(Backend.IComputer))] +[assembly: Export(typeof(Backend.IComputer))] // Generate JavaScript -> C# interop handlers for specified contracts. -[assembly: JSImport(typeof(Backend.Prime.IPrimeUI))] +[assembly: Import(typeof(Backend.Prime.IPrimeUI))] // Group all generated JavaScript APIs under "Computer" namespace. -[assembly: JSPreferences(Space = [".+", "Computer"])] +[assembly: Preferences(Space = [".+", "Computer"])] // Perform dependency injection. new ServiceCollection() diff --git a/samples/react/backend/Backend/IComputer.cs b/samples/react/backend/Backend/IComputer.cs index 625e64fc..ed1a3f21 100644 --- a/samples/react/backend/Backend/IComputer.cs +++ b/samples/react/backend/Backend/IComputer.cs @@ -1,3 +1,5 @@ +using System; + namespace Backend; // In the domain assembly we outline the contract of a computer service. @@ -6,6 +8,8 @@ namespace Backend; public interface IComputer { + event Action OnComputing; + event Action OnComplete; void StartComputing (); void StopComputing (); bool IsComputing (); diff --git a/samples/trimming/cs/Program.cs b/samples/trimming/cs/Program.cs index d13e5297..ca8c3424 100644 --- a/samples/trimming/cs/Program.cs +++ b/samples/trimming/cs/Program.cs @@ -7,6 +7,6 @@ public static void Main () Log("Hello from .NET!"); } - [JSFunction] + [Import] public static partial void Log (string message); } diff --git a/src/cs/Bootsharp.Common.Test/InstancesTest.cs b/src/cs/Bootsharp.Common.Test/InstancesTest.cs index 6912dfcb..81682021 100644 --- a/src/cs/Bootsharp.Common.Test/InstancesTest.cs +++ b/src/cs/Bootsharp.Common.Test/InstancesTest.cs @@ -5,38 +5,63 @@ namespace Bootsharp.Common.Test; public class InstancesTest { [Fact] - public void ThrowsWhenGettingUnregisteredInstance () + public void CaneExportAndDisposeInstance () { - Assert.Throws(() => Get(0)); + var exported = new object(); + var id = Export(exported); + Assert.Same(exported, Exported(id)); + DisposeExported(id); } [Fact] - public void ThrowsWhenDisposingUnregisteredInstance () + public void GeneratesUniqueIdsForUniqueExports () { - Assert.Throws(() => Dispose(0)); + Assert.NotEqual(Export(new object()), Export(new object())); } [Fact] - public void CanRegisterGetAndDisposeInstance () + public void KeepsStableIdsForSameExports () { - var instance = new object(); - var id = Register(instance); - Assert.Same(instance, Get(id)); - Dispose(id); - Assert.Throws(() => Get(id)); + var exported = new object(); + Assert.Equal(Export(exported), Export(exported)); } [Fact] - public void GeneratesUniqueIdsOnEachRegister () + public void InvokesExportFactoryCallbacks () { - Assert.NotEqual(Register(new object()), Register(new object())); + var exported = false; + var disposed = false; + var id = Export(new object(), (_, _) => { + exported = true; + return () => disposed = true; + }); + Assert.True(exported); + Assert.False(disposed); + DisposeExported(id); + Assert.True(disposed); } [Fact] - public void ReusesIdOfDisposedInstance () + public void CanImportAndDisposeInstance () { - var id = Register(new object()); - Dispose(id); - Assert.Equal(id, Register(new object())); + var imported = new object(); + Assert.Same(imported, Import(1, _ => imported)); + DisposeImported(1); + } + + [Fact] + public void CachesImportsUntilDisposed () + { + var imported = new object(); + Import(42, _ => imported); + Assert.Same(imported, + // We don't use the factory here and ignore the fact that it returns another instance, + // because we already have previous import associated with the '42' ID — it's the + // responsibility of the JS side to let us know when the instance is disposed. + Import(42, _ => new object())); + // Here, we simulate JS side telling us to dispose the '42' instance. + DisposeImported(42); + // Now we exercise the factory and register the new instance as '42'. + Assert.NotSame(imported, Import(42, _ => new object())); } } diff --git a/src/cs/Bootsharp.Common.Test/TypesTest.cs b/src/cs/Bootsharp.Common.Test/TypesTest.cs index 899dabff..538153e3 100644 --- a/src/cs/Bootsharp.Common.Test/TypesTest.cs +++ b/src/cs/Bootsharp.Common.Test/TypesTest.cs @@ -1,8 +1,8 @@ using System.Reflection; using Bootsharp; -[assembly: JSExport(typeof(IBackend))] -[assembly: JSImport(typeof(IFrontend))] +[assembly: Export(typeof(IBackend))] +[assembly: Import(typeof(IFrontend))] namespace Bootsharp.Common.Test; @@ -14,29 +14,31 @@ public class TypesTest [Fact] public void TypesAreAssigned () { - Assert.Equal([typeof(IBackend)], new JSExportAttribute(typeof(IBackend)).Types); - Assert.Equal([typeof(IFrontend)], new JSImportAttribute(typeof(IFrontend)).Types); - Assert.Equal("Space", (new JSPreferencesAttribute { Space = ["Space"] }).Space[0]); + Assert.Equal([typeof(IBackend)], new ExportAttribute(typeof(IBackend)).Types); + Assert.Equal([typeof(IFrontend)], new ImportAttribute(typeof(IFrontend)).Types); + Assert.Equal("Space", (new PreferencesAttribute { Space = ["Space"] }).Space[0]); } [Fact] public void ExportParametersEqualArguments () { Assert.Equal([typeof(IBackend)], - (export.ConstructorArguments[0].Value as IReadOnlyCollection).Select(a => a.Value)); + (export.ConstructorArguments[0].Value as IReadOnlyCollection) + .Select(a => a.Value)); } [Fact] public void ImportParametersEqualArguments () { Assert.Equal([typeof(IFrontend)], - (import.ConstructorArguments[0].Value as IReadOnlyCollection).Select(a => a.Value)); + (import.ConstructorArguments[0].Value as IReadOnlyCollection) + .Select(a => a.Value)); } private static CustomAttributeData GetMockExportAttribute () => typeof(TypesTest).Assembly.CustomAttributes - .First(a => a.AttributeType == typeof(JSExportAttribute)); + .First(a => a.AttributeType == typeof(ExportAttribute)); private static CustomAttributeData GetMockImportAttribute () => typeof(TypesTest).Assembly.CustomAttributes - .First(a => a.AttributeType == typeof(JSImportAttribute)); + .First(a => a.AttributeType == typeof(ImportAttribute)); } diff --git a/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs new file mode 100644 index 00000000..e9a13342 --- /dev/null +++ b/src/cs/Bootsharp.Common/Attributes/ExportAttribute.cs @@ -0,0 +1,45 @@ +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 +/// be automatically exported for consumption on the JavaScript side. +/// +/// +/// When used on the assembly level, generated bindings have to be initialized with the handler implementation. +/// For example, given "IHandler" interface is exported, "JSHandler" class will be generated, +/// which has to be instantiated with an "IHandler" implementation instance. +/// +/// +/// Expose "GetName" method to JavaScript: +/// +/// [Export] +/// public static string GetName () => "Sharp"; +/// +/// Expose "OnSomething" event to JavaScript: +/// +/// [Export] +/// public static event Action OnSomething; +/// +/// Expose "IHandlerA" and "IHandlerB" C# APIs to JavaScript and wrap invocations in "Utils.Try()": +/// +/// [assembly: Export( +/// typeof(IHandlerA), +/// typeof(IHandlerB), +/// invokePattern = "(.+)", +/// invokeReplacement = "Utils.Try(() => $1)" +/// )] +/// +/// +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Event)] +public sealed class ExportAttribute : Attribute +{ + /// + /// When applied to assembly, lists the interface types to generated export bindings for. + /// + public Type[] Types { get; } + + /// The interface 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 new file mode 100644 index 00000000..2bd597b2 --- /dev/null +++ b/src/cs/Bootsharp.Common/Attributes/ImportAttribute.cs @@ -0,0 +1,43 @@ +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 used on the assembly level, generated bindings have to be implemented on the JavaScript side. +/// For example, given "IFrontend" interface is imported, "JSFrontend" class will be generated, +/// which has to be implemented in JavaScript. +/// +/// +/// Bind "GetHostName" method with a JavaScript function: +/// +/// [Import] +/// public static partial string GetHostName (); +/// +/// Allows broadcasting "OnSomething" event on the JavaScript side: +/// +/// [Import] +/// public static event Action OnSomething; +/// +/// Generate JavaScript APIs based on "IFrontendAPI" and "IOtherFrontendAPI" interfaces: +/// +/// [assembly: Import( +/// typeof(IFrontendAPI), +/// typeof(IOtherFrontendAPI) +/// )] +/// +/// +[AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Method | AttributeTargets.Event)] +public sealed class ImportAttribute : Attribute +{ + /// + /// When applied to assembly, lists the interface types to generated import bindings for. + /// + public Type[] Types { get; } + + /// The 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/Attributes/JSEventAttribute.cs b/src/cs/Bootsharp.Common/Attributes/JSEventAttribute.cs deleted file mode 100644 index 71ffe9e7..00000000 --- a/src/cs/Bootsharp.Common/Attributes/JSEventAttribute.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Bootsharp; - -/// -/// Applied to a partial method to bind it with an event meant to be -/// broadcast (invoked) in C# and subscribed (listened) to in JavaScript. -/// -/// -/// -/// [JSEvent] -/// public static partial void OnSomethingHappened (string payload); -/// Namespace.onSomethingHappened.subscribe(payload => ...); -/// -/// -[AttributeUsage(AttributeTargets.Method)] -public sealed class JSEventAttribute : Attribute; diff --git a/src/cs/Bootsharp.Common/Attributes/JSExportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/JSExportAttribute.cs deleted file mode 100644 index 811470d5..00000000 --- a/src/cs/Bootsharp.Common/Attributes/JSExportAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Bootsharp; - -/// -/// When applied to WASM entry point assembly, specified interfaces will -/// be automatically exported for consumption on JavaScript side. -/// -/// -/// Generated bindings have to be initialized with the handler implementation. -/// For example, given "IHandler" interface is exported, "JSHandler" class will be generated, -/// which has to be instantiated with an "IHandler" implementation instance. -/// -/// -/// Expose "IHandlerA" and "IHandlerB" C# APIs to JavaScript and wrap invocations in "Utils.Try()": -/// -/// [assembly: JSExport( -/// typeof(IHandlerA), -/// typeof(IHandlerB), -/// invokePattern = "(.+)", -/// invokeReplacement = "Utils.Try(() => $1)" -/// )] -/// -/// -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class JSExportAttribute : Attribute -{ - /// - /// The interface types to generated export bindings for. - /// - public Type[] Types { get; } - - /// The interface types to generate export bindings for. - public JSExportAttribute (params Type[] types) - { - Types = types; - } -} diff --git a/src/cs/Bootsharp.Common/Attributes/JSFunctionAttribute.cs b/src/cs/Bootsharp.Common/Attributes/JSFunctionAttribute.cs deleted file mode 100644 index 68e6bf0c..00000000 --- a/src/cs/Bootsharp.Common/Attributes/JSFunctionAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Bootsharp; - -/// -/// Applied to a partial method to bind it with a JavaScript function. -/// -/// -/// The implementation is expected to be assigned as "Namespace.method = function". -/// -/// -/// -/// [JSFunction] -/// public static partial string GetHostName (); -/// Namespace.getHostName = () => "Browser"; -/// -/// -[AttributeUsage(AttributeTargets.Method)] -public sealed class JSFunctionAttribute : Attribute; diff --git a/src/cs/Bootsharp.Common/Attributes/JSImportAttribute.cs b/src/cs/Bootsharp.Common/Attributes/JSImportAttribute.cs deleted file mode 100644 index 18310a18..00000000 --- a/src/cs/Bootsharp.Common/Attributes/JSImportAttribute.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace Bootsharp; - -/// -/// When applied to WASM entry point assembly, JavaScript bindings for the specified interfaces -/// will be automatically generated for consumption on C# side. -/// -/// -/// Generated bindings have to be implemented on JavaScript side. -/// For example, given "IFrontend" interface is imported, "JSFrontend" class will be generated, -/// which has to be implemented in JavaScript.
-/// When an interface method starts with "Notify", an event bindings will ge generated (instead of function); -/// JavaScript name of the event will start with "on" instead of "Notify". -/// This behaviour can be configured via preferences. -///
-/// -/// Generate JavaScript APIs based on "IFrontendAPI" and "IOtherFrontendAPI" interfaces: -/// -/// [assembly: JSImport( -/// typeof(IFrontendAPI), -/// typeof(IOtherFrontendAPI) -/// )] -/// -/// -[AttributeUsage(AttributeTargets.Assembly)] -public sealed class JSImportAttribute : Attribute -{ - /// - /// The interface types to generated import bindings for. - /// - public Type[] Types { get; } - - /// The interface types to generate import bindings for. - public JSImportAttribute (params Type[] types) - { - Types = types; - } -} diff --git a/src/cs/Bootsharp.Common/Attributes/JSInvokableAttribute.cs b/src/cs/Bootsharp.Common/Attributes/JSInvokableAttribute.cs deleted file mode 100644 index 10e8978d..00000000 --- a/src/cs/Bootsharp.Common/Attributes/JSInvokableAttribute.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Bootsharp; - -/// -/// Applied to a static method to make it invokable in JavaScript. -/// -/// -/// -/// [JSInvokable] -/// public static string GetName () => "Sharp"; -/// console.log(Namespace.getName()); -/// -/// -[AttributeUsage(AttributeTargets.Method)] -public sealed class JSInvokableAttribute : Attribute; diff --git a/src/cs/Bootsharp.Common/Attributes/JSPreferencesAttribute.cs b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs similarity index 75% rename from src/cs/Bootsharp.Common/Attributes/JSPreferencesAttribute.cs rename to src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs index f6f8c1a6..7123a274 100644 --- a/src/cs/Bootsharp.Common/Attributes/JSPreferencesAttribute.cs +++ b/src/cs/Bootsharp.Common/Attributes/PreferencesAttribute.cs @@ -11,13 +11,13 @@ namespace Bootsharp; /// /// Make all spaces starting with "Foo.Bar" replaced with "Baz": /// -/// [assembly: Bootsharp.JSPreferences( +/// [assembly: Bootsharp.Preferences( /// Space = ["^Foo\.Bar\.(\S+)", "Baz.$1"] /// )] /// /// [AttributeUsage(AttributeTargets.Assembly)] -public sealed class JSPreferencesAttribute : Attribute +public sealed class PreferencesAttribute : Attribute { /// /// Customize generated JavaScript object names and TypeScript namespaces. @@ -26,7 +26,7 @@ public sealed class JSPreferencesAttribute : Attribute /// The patterns are matched against full type name (namespace.typename) of /// declaring C# type when generating JavaScript objects for interop methods /// and against namespace when generating TypeScript syntax for C# types. - /// Matched type names have following modifications:
+ /// Matched type names have the following modifications:
/// - interfaces have first character removed
/// - generics have parameter spec removed
/// - nested have "+" replaced with "."
@@ -41,16 +41,6 @@ public sealed class JSPreferencesAttribute : Attribute /// public string[] Type { get; init; } = []; /// - /// Customize which C# methods should be transformed into JavaScript - /// events, as well as generated event names. - /// - /// - /// The patterns are matched against C# method names declared under - /// interfaces. By default, methods - /// starting with "Notify.." are matched. - /// - public string[] Event { get; init; } = []; - /// /// Customize generated JavaScript function names. /// /// diff --git a/src/cs/Bootsharp.Common/Build/Bootsharp.Common.props b/src/cs/Bootsharp.Common/Build/Bootsharp.Common.props index 3d5ee83e..dea745bb 100644 --- a/src/cs/Bootsharp.Common/Build/Bootsharp.Common.props +++ b/src/cs/Bootsharp.Common/Build/Bootsharp.Common.props @@ -1,7 +1,7 @@ - + true diff --git a/src/cs/Bootsharp.Common/Interop/ExportInterface.cs b/src/cs/Bootsharp.Common/Interop/ExportInterface.cs index 12cb2db6..f71ceb85 100644 --- a/src/cs/Bootsharp.Common/Interop/ExportInterface.cs +++ b/src/cs/Bootsharp.Common/Interop/ExportInterface.cs @@ -2,7 +2,7 @@ namespace Bootsharp; /// /// Metadata about generated interop class for an interface supplied -/// under . +/// under . /// /// Type of the exported interface. /// Takes export interface implementation instance; returns interop class instance. diff --git a/src/cs/Bootsharp.Common/Interop/ImportInterface.cs b/src/cs/Bootsharp.Common/Interop/ImportInterface.cs index a925ce99..8c299ff0 100644 --- a/src/cs/Bootsharp.Common/Interop/ImportInterface.cs +++ b/src/cs/Bootsharp.Common/Interop/ImportInterface.cs @@ -2,7 +2,7 @@ namespace Bootsharp; /// /// Metadata about generated implementation for interface supplied -/// under . +/// under . /// /// Import interface implementation instance. public record ImportInterface (object Instance); diff --git a/src/cs/Bootsharp.Common/Interop/Instances.cs b/src/cs/Bootsharp.Common/Interop/Instances.cs index 29d62379..770f3fdd 100644 --- a/src/cs/Bootsharp.Common/Interop/Instances.cs +++ b/src/cs/Bootsharp.Common/Interop/Instances.cs @@ -1,46 +1,76 @@ namespace Bootsharp; /// -/// Manages exported (C# -> JavaScript) instanced interop interfaces. +/// Manages exported (C# -> JavaScript) and imported (JavaScript -> C#) instanced interop interfaces. /// +/// +/// Exported instances originate from C# and are created in the user code; +/// we track them to be able to invoke their members in the static bindings. +/// Imported instances originate from JS and are represented with the C# wrappers generated by Bootsharp; +/// we track them to be able to invoke their event raisers in the static bindings. +/// public static class Instances { - private static readonly Dictionary idToInstance = []; + private static readonly Dictionary importedById = []; + private static readonly Dictionary exportedById = []; + private static readonly Dictionary idByExported = new(ReferenceEqualityComparer.Instance); + private static readonly Dictionary onDisposeById = []; private static readonly Queue idPool = []; private static int nextId = int.MinValue; /// - /// Registers specified interop instance and associates it with unique ID. + /// Registers specified exported instance and associates it with a unique ID, unless it's already registered, + /// in which case the ID of the registered instance is returned. /// /// The instance to register. + /// Callbacks to invoke when registering and disposing the instance. /// Unique ID associated with the registered instance. - public static int Register (object instance) + public static int Export (T instance, Func? factory = null) where T : class { - var id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; - idToInstance[id] = instance; + if (idByExported.TryGetValue(instance, out var id)) return id; + id = idPool.Count > 0 ? idPool.Dequeue() : nextId++; + exportedById[idByExported[instance] = id] = instance; + if (factory != null) onDisposeById[id] = factory(id, instance); return id; } /// - /// Resolves registered instance by the specified ID. + /// Returns a registered exported instance associated with the specified ID. /// - /// Unique ID of the instance to resolve. - public static object Get (int id) + public static T Exported (int id) where T : class { - if (!idToInstance.TryGetValue(id, out var instance)) - throw new Error($"Failed to resolve exported interop instance with '{id}' ID: not registered."); + return (T)exportedById[id]; + } + + /// + /// Invokes the specified factory to create and register an imported instance wrapper associated with the ID, + /// unless an imported instance is already registered under the ID, in which case returns its wrapper. + /// + public static T Import (int id, Func factory) where T : class + { + if (importedById.GetValueOrDefault(id) is { } weak) return (T)weak.Target!; + var instance = factory(id); + importedById[id] = new(instance); return instance; } /// - /// Notifies that interop instance is no longer used on JavaScript side - /// (eg, was garbage collected) and can be released on C# side as well. + /// Notifies that an exported instance with the specified ID is no longer used on the JavaScript side + /// (eg, was garbage collected) and can be released on the C# side as well. /// - /// ID of the disposed interop instance. - public static void Dispose (int id) + public static void DisposeExported (int id) { - if (!idToInstance.Remove(id)) - throw new Error($"Failed to dispose exported interop instance with '{id}' ID: not registered."); + if (exportedById.Remove(id, out var instance)) idByExported.Remove(instance); + if (onDisposeById.Remove(id, out var onDispose)) onDispose(); idPool.Enqueue(id); } + + /// + /// Notifies that an imported interop instance with the specified ID is no longer used + /// on the C# side and can be untracked. + /// + public static void DisposeImported (int id) + { + importedById.Remove(id); + } } diff --git a/src/cs/Bootsharp.Common/Interop/Interfaces.cs b/src/cs/Bootsharp.Common/Interop/Interfaces.cs index 68c6aa44..1874b3d2 100644 --- a/src/cs/Bootsharp.Common/Interop/Interfaces.cs +++ b/src/cs/Bootsharp.Common/Interop/Interfaces.cs @@ -4,7 +4,7 @@ namespace Bootsharp; /// /// Provides access to generated interop types for interfaces supplied -/// under and . +/// under and . /// /// /// Exported interfaces are C# APIs invoked in JavaScript. Their C# implementation @@ -17,13 +17,13 @@ namespace Bootsharp; public static class Interfaces { /// - /// Interop classes generated for 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 + /// Implementations generated for interop /// interfaces mapped by the interface type of the associated implementation. /// public static IReadOnlyDictionary Imports => imports; diff --git a/src/cs/Bootsharp.Common/Interop/Proxies.cs b/src/cs/Bootsharp.Common/Interop/Proxies.cs index 97898336..9e87ae5a 100644 --- a/src/cs/Bootsharp.Common/Interop/Proxies.cs +++ b/src/cs/Bootsharp.Common/Interop/Proxies.cs @@ -5,14 +5,14 @@ namespace Bootsharp; ///
/// /// Below is for internal reference; end users are not expected to use this API.
-/// Partial interop methods ( and ) +/// Partial interop methods declared with /// are accessed via delegates registered by associated IDs, where ID is the full name of /// the declaring type of the interop method joined with the method's name by a dot. Eg, given:
/// /// namespace Space; /// public static partial class Class /// { -/// [JSFunction] public static partial int Foo (string arg); +/// [Import] public static partial int Foo (string arg); /// } ///
/// Proxy for the "Foo" method is registered as follows (emitted at build; @@ -45,8 +45,7 @@ public static void Set (string id, Delegate @delegate) /// Returns interop delegate of specified ID and type. /// /// - /// Used in sources generated for partial - /// and methods. + /// Used in sources generated for partial methods. /// public static T Get (string id) where T : Delegate { diff --git a/src/cs/Bootsharp.Generate.Test/EventTest.cs b/src/cs/Bootsharp.Generate.Test/EventTest.cs deleted file mode 100644 index 7fc20949..00000000 --- a/src/cs/Bootsharp.Generate.Test/EventTest.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace Bootsharp.Generate.Test; - -public static class EventTest -{ - public static object[][] Data { get; } = [ - // Can generate event binding without namespace and arguments. - [ - """ - partial class Foo - { - [JSEvent] partial void OnBar (); - } - """, - """ - unsafe partial class Foo - { - private static delegate* managed Proxy_Foo_OnBar; - partial void OnBar () => Proxy_Foo_OnBar(); - } - """ - ], - // Can generate event binding with namespace and arguments. - [ - """ - namespace Space; - - public static partial class Foo - { - [JSEvent] public static partial void OnBar (string a, int b); - } - """, - """ - namespace Space; - - public static unsafe partial class Foo - { - private static delegate* managed Proxy_Space_Foo_OnBar; - public static partial void OnBar (global::System.String a, global::System.Int32 b) => Proxy_Space_Foo_OnBar(a, b); - } - """ - ] - ]; -} diff --git a/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs b/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs index 7ebe1279..22f8ae40 100644 --- a/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs +++ b/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs @@ -26,31 +26,31 @@ public async Task WhenAttributeIsFromOtherNamespaceItsIgnored () { await Verify( """ - [assembly:JSImport([])] - public class JSImportAttribute : System.Attribute { public JSImportAttribute (Type[] _) { } } + [assembly:Import([])] + public class ImportAttribute : System.Attribute { public ImportAttribute (Type[] _) { } } """); } [Fact] - public async Task DoesntEmitDuplicateRegistrations () + public async Task DoesntEmitDuplicateImportSources () { verifier.TestBehaviors = TestBehaviors.SkipGeneratedSourcesCheck; await Verify( """ - partial class FunctionAfterInvokable + partial class ImportMethodAfterExport { - [JSInvokable] static void Bar () { } - [JSFunction] partial void Baz (); + [Export] static void Bar () { } + [Import] partial void Baz (); } - partial class EventAfterInvokable + partial class ImportEventAfterExport { - [JSInvokable] static void Bar () { } - [JSEvent] partial void Baz (); + [Export] static void Bar () { } + [Import] static event Action? Baz; } - partial class EventAfterFunction + partial class ImportEventAfterMethod { - [JSFunction] partial void Bar (); - [JSEvent] partial void Baz (); + [Import] partial void Bar (); + [Import] static event Action? Baz; } """); } @@ -63,21 +63,21 @@ public async Task DoesntAnalyzeGeneratedFiles () """ public static partial class Foo { - [JSInvokable] public static void Bar () { } - [JSFunction] public static void Baz () { } - [JSEvent] public static void Nya () { } + [Export] public static void Bar () { } + [Import] public static void Baz () { } + [Import] public static event Action? Nya; } """)); await Verify(""); } - [Theory, MemberData(nameof(FunctionTest.Data), MemberType = typeof(FunctionTest))] - public Task PartialFunctionsImplemented (string source, string expected) - => Verify(source, ("FooFunctions.g.cs", expected)); + [Theory, MemberData(nameof(ImportMethodTest.Data), MemberType = typeof(ImportMethodTest))] + public Task PartialImportMethodsImplemented (string source, string expected) + => Verify(source, ("FooImports.g.cs", expected)); - [Theory, MemberData(nameof(EventTest.Data), MemberType = typeof(EventTest))] - public Task PartialEventsImplemented (string source, string expected) - => Verify(source, ("FooEvents.g.cs", expected)); + [Theory, MemberData(nameof(ImportEventTest.Data), MemberType = typeof(ImportEventTest))] + public Task PartialImportEventsImplemented (string source, string expected) + => Verify(source, ("FooImports.g.cs", expected)); private async Task Verify (string source, params (string file, string content)[] expected) { diff --git a/src/cs/Bootsharp.Generate.Test/ImportEventTest.cs b/src/cs/Bootsharp.Generate.Test/ImportEventTest.cs new file mode 100644 index 00000000..3560d3c3 --- /dev/null +++ b/src/cs/Bootsharp.Generate.Test/ImportEventTest.cs @@ -0,0 +1,57 @@ +namespace Bootsharp.Generate.Test; + +public static class ImportEventTest +{ + public static TheoryData Data { get; } = new() { + // Can generate import event without namespace and arguments. + { + """ + partial class Foo + { + [Import] static event Action? OnBar; + } + """, + """ + unsafe partial class Foo + { + internal static void Bootsharp_Invoke_OnBar () => OnBar?.Invoke(); + } + """ + }, + // Can generate import event with namespace and arguments. + { + """ + namespace Space; + + public static partial class Foo + { + [Import] public static event Action? OnBar; + } + """, + """ + namespace Space; + + public static unsafe partial class Foo + { + internal static void Bootsharp_Invoke_OnBar (global::System.String arg1, global::System.Int32 arg2) => OnBar?.Invoke(arg1, arg2); + } + """ + }, + // Ignores non-static events. + { + """ + partial class Foo + { + [Import] static event Action? OnBar; + [Import] event Action? OnBaz; + } + """, + """ + unsafe partial class Foo + { + internal static void Bootsharp_Invoke_OnBar () => OnBar?.Invoke(); + } + """ + } + }; +} diff --git a/src/cs/Bootsharp.Generate.Test/FunctionTest.cs b/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs similarity index 63% rename from src/cs/Bootsharp.Generate.Test/FunctionTest.cs rename to src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs index 45a342c6..215a3b13 100644 --- a/src/cs/Bootsharp.Generate.Test/FunctionTest.cs +++ b/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs @@ -1,26 +1,26 @@ namespace Bootsharp.Generate.Test; -public static class FunctionTest +public static class ImportMethodTest { - public static object[][] Data { get; } = [ - // Can generate void binding under root namespace. - [ + public static TheoryData Data { get; } = new() { + // Can generate void import method under root namespace. + { """ partial class Foo { - [JSFunction] partial void Bar (); + [Import] partial void Bar (); } """, """ unsafe partial class Foo { - private static delegate* managed Proxy_Foo_Bar; - partial void Bar () => Proxy_Foo_Bar(); + private static delegate* managed Bootsharp_Bar; + partial void Bar () => Bootsharp_Bar(); } """ - ], - // Can generate void task binding under file-scoped namespace. - [ + }, + // Can generate void task import method under file-scoped namespace. + { """ using System.Threading.Tasks; @@ -28,7 +28,7 @@ namespace File.Scoped; public static partial class Foo { - [JSFunction] private static partial Task BarAsync (string[] a, int? b); + [Import] private static partial Task BarAsync (string[] a, int? b); } """, """ @@ -38,13 +38,13 @@ namespace File.Scoped; public static unsafe partial class Foo { - private static delegate* managed Proxy_File_Scoped_Foo_BarAsync; - private static partial global::System.Threading.Tasks.Task BarAsync (global::System.String[] a, global::System.Int32? b) => Proxy_File_Scoped_Foo_BarAsync(a, b); + private static delegate* managed Bootsharp_BarAsync; + private static partial global::System.Threading.Tasks.Task BarAsync (global::System.String[] a, global::System.Int32? b) => Bootsharp_BarAsync(a, b); } """ - ], - // Can generate value task binding. - [ + }, + // Can generate value task import method. + { """ using System.Threading.Tasks; @@ -52,7 +52,7 @@ namespace File.Scoped; public static partial class Foo { - [JSFunction] private static partial Task BarAsync (); + [Import] private static partial Task BarAsync (); } """, """ @@ -62,31 +62,31 @@ namespace File.Scoped; public static unsafe partial class Foo { - private static delegate* managed> Proxy_File_Scoped_Foo_BarAsync; - private static partial global::System.Threading.Tasks.Task BarAsync () => Proxy_File_Scoped_Foo_BarAsync(); + private static delegate* managed> Bootsharp_BarAsync; + private static partial global::System.Threading.Tasks.Task BarAsync () => Bootsharp_BarAsync(); } """ - ], + }, // Can generate custom types. - [ + { """ public record Record; partial class Foo { - [JSFunction] partial void Bar (Record a); + [Import] partial void Bar (Record a); } """, """ unsafe partial class Foo { - private static delegate* managed Proxy_Foo_Bar; - partial void Bar (global::Record a) => Proxy_Foo_Bar(a); + private static delegate* managed Bootsharp_Bar; + partial void Bar (global::Record a) => Bootsharp_Bar(a); } """ - ], + }, // Can generate under classic namespace. - [ + { """ using System; using System.Threading.Tasks; @@ -95,8 +95,8 @@ namespace Classic { partial class Foo { - [JSFunction] public partial DateTime GetTime (DateTime time); - [JSFunction] public partial Task GetTimeAsync (DateTime time); + [Import] public partial DateTime GetTime (DateTime time); + [Import] public partial Task GetTimeAsync (DateTime time); } } """, @@ -108,22 +108,22 @@ namespace Classic { unsafe partial class Foo { - private static delegate* managed Proxy_Classic_Foo_GetTime; - public partial global::System.DateTime GetTime (global::System.DateTime time) => Proxy_Classic_Foo_GetTime(time); - private static delegate* managed> Proxy_Classic_Foo_GetTimeAsync; - public partial global::System.Threading.Tasks.Task GetTimeAsync (global::System.DateTime time) => Proxy_Classic_Foo_GetTimeAsync(time); + private static delegate* managed Bootsharp_GetTime; + public partial global::System.DateTime GetTime (global::System.DateTime time) => Bootsharp_GetTime(time); + private static delegate* managed> Bootsharp_GetTimeAsync; + public partial global::System.Threading.Tasks.Task GetTimeAsync (global::System.DateTime time) => Bootsharp_GetTimeAsync(time); } } """ - ], + }, // Special corner case when UsingDirectiveSyntax.Name is null. - [ + { """ using x = (System.String, System.Boolean); partial class Foo { - [JSFunction] partial void Bar (); + [Import] partial void Bar (); } """, """ @@ -131,26 +131,26 @@ partial class Foo unsafe partial class Foo { - private static delegate* managed Proxy_Foo_Bar; - partial void Bar () => Proxy_Foo_Bar(); + private static delegate* managed Bootsharp_Bar; + partial void Bar () => Bootsharp_Bar(); } """ - ], + }, // Doesn't add 'unsafe' class modified when it's already specified. - [ + { """ unsafe partial class Foo { - [JSFunction] partial void Bar (); + [Import] partial void Bar (); } """, """ unsafe partial class Foo { - private static delegate* managed Proxy_Foo_Bar; - partial void Bar () => Proxy_Foo_Bar(); + private static delegate* managed Bootsharp_Bar; + partial void Bar () => Bootsharp_Bar(); } """ - ] - ]; + } + }; } diff --git a/src/cs/Bootsharp.Generate/Global.cs b/src/cs/Bootsharp.Generate/Global.cs new file mode 100644 index 00000000..e81f4b70 --- /dev/null +++ b/src/cs/Bootsharp.Generate/Global.cs @@ -0,0 +1,35 @@ +global using static Bootsharp.Generate.Global; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Bootsharp.Generate; + +internal static class Global +{ + public static string BuildSyntax (ITypeSymbol type) + { + if (type.SpecialType == SpecialType.System_Void) return "void"; + if (type is IArrayTypeSymbol arrayType) return $"{BuildSyntax(arrayType.ElementType)}[]"; + var nil = type.NullableAnnotation == NullableAnnotation.Annotated ? "?" : ""; + if (IsGeneric(type, out var args)) return BuildGeneric(type, args) + nil; + return $"global::{ResolveTypeName(type)}{nil}"; + + static string BuildGeneric (ITypeSymbol type, ImmutableArray args) + { + if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) return BuildSyntax(args[0]); + return $"global::{ResolveTypeName(type)}<{string.Join(", ", args.Select(BuildSyntax))}>"; + } + + static string ResolveTypeName (ITypeSymbol type) + { + if (type.ContainingNamespace.IsGlobalNamespace) return type.Name; + return string.Join(".", type.ContainingNamespace.ConstituentNamespaces) + "." + type.Name; + } + + static bool IsGeneric (ITypeSymbol type, out ImmutableArray args) + { + args = type is INamedTypeSymbol { IsGenericType: true } named ? named.TypeArguments : default; + return args != default; + } + } +} diff --git a/src/cs/Bootsharp.Generate/ImportClass.cs b/src/cs/Bootsharp.Generate/ImportClass.cs new file mode 100644 index 00000000..d98adfc2 --- /dev/null +++ b/src/cs/Bootsharp.Generate/ImportClass.cs @@ -0,0 +1,59 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Bootsharp.Generate; + +internal sealed class ImportClass (Compilation cmp, ClassDeclarationSyntax stx, + IReadOnlyList methods, IReadOnlyList events) +{ + public string Name { get; } = stx.Identifier.ToString(); + + public string EmitSource () => + """ + #nullable enable + #pragma warning disable + + """ + + EmitUsings() + + WrapNamespace( + EmitHeader() + + EmitMembers() + + EmitFooter() + ); + + private string EmitUsings () + { + var usings = string.Join("\n", stx.SyntaxTree.GetRoot() + .DescendantNodesAndSelf().OfType()); + return string.IsNullOrEmpty(usings) ? "" : usings + "\n\n"; + } + + private string EmitHeader () + { + var mods = stx.Modifiers.ToString(); + if (!mods.Contains("unsafe")) mods = mods.Replace("partial", "unsafe partial"); + return $"{mods} class {stx.Identifier}{stx.TypeParameterList}{stx.BaseList}{stx.ConstraintClauses}\n{{"; + } + + private string EmitMembers () => "\n" + string.Join("\n", [ + ..events.Select(e => " " + e.EmitSource(cmp)), + ..methods.Select(m => " " + m.EmitSource(cmp)) + ]); + + private string EmitFooter () => "\n}"; + + private string WrapNamespace (string src) + { + if (stx.Parent is NamespaceDeclarationSyntax space) + return $$""" + namespace {{space.Name}} + { + {{string.Join("\n", src.Split(["\r\n", "\r", "\n"], StringSplitOptions.None) + .Select((s, i) => i > 0 && s.Length > 0 ? " " + s : s))}} + } + """; + if (stx.Parent is FileScopedNamespaceDeclarationSyntax fileSpace) + return $"namespace {fileSpace.Name};\n\n{src}"; + return src; + } +} diff --git a/src/cs/Bootsharp.Generate/ImportEvent.cs b/src/cs/Bootsharp.Generate/ImportEvent.cs new file mode 100644 index 00000000..85fcfaf8 --- /dev/null +++ b/src/cs/Bootsharp.Generate/ImportEvent.cs @@ -0,0 +1,18 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Bootsharp.Generate; + +internal sealed class ImportEvent (EventFieldDeclarationSyntax stx) +{ + public string EmitSource (Compilation cmp) + { + var evt = (IEventSymbol)cmp.GetSemanticModel(stx.SyntaxTree) + .GetDeclaredSymbol(stx.Declaration.Variables.Single())!; + var inv = ((INamedTypeSymbol)evt.Type).DelegateInvokeMethod!; + var sigArgs = string.Join(", ", inv.Parameters.Select(p => $"{BuildSyntax(p.Type)} {p.Name}")); + var invArgs = string.Join(", ", inv.Parameters.Select(p => p.Name)); + return $"internal static void Bootsharp_Invoke_{evt.Name} ({sigArgs}) => " + + $"{evt.Name}?.Invoke({invArgs});"; + } +} diff --git a/src/cs/Bootsharp.Generate/ImportMethod.cs b/src/cs/Bootsharp.Generate/ImportMethod.cs new file mode 100644 index 00000000..2d204da4 --- /dev/null +++ b/src/cs/Bootsharp.Generate/ImportMethod.cs @@ -0,0 +1,38 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Bootsharp.Generate; + +internal sealed class ImportMethod (MethodDeclarationSyntax stx) +{ + private IMethodSymbol method = null!; + + public string EmitSource (Compilation cmp) + { + method = cmp.GetSemanticModel(stx.SyntaxTree).GetDeclaredSymbol(stx)!; + return $""" + private static delegate* managed<{EmitPointerSignature()}> Bootsharp_{method.Name}; + {stx.Modifiers} {EmitMethodSignature()} => Bootsharp_{method.Name}({EmitArgs()}); + """; + } + + private string EmitPointerSignature () + { + var args = method.Parameters.Select(p => BuildSyntax(p.Type)).ToList(); + args.Add(BuildSyntax(method.ReturnType)); + return string.Join(", ", args); + } + + private string EmitMethodSignature () + { + var args = method.Parameters.Select(p => $"{BuildSyntax(p.Type)} {p.Name}"); + return $"{BuildSyntax(method.ReturnType)} {method.Name} ({string.Join(", ", args)})"; + } + + private string EmitArgs () + { + if (method.Parameters.Length == 0) return ""; + return string.Join(", ", method.Parameters.Select(p => p.Name)); + } +} diff --git a/src/cs/Bootsharp.Generate/PartialClass.cs b/src/cs/Bootsharp.Generate/PartialClass.cs deleted file mode 100644 index 2be5c95b..00000000 --- a/src/cs/Bootsharp.Generate/PartialClass.cs +++ /dev/null @@ -1,62 +0,0 @@ -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Bootsharp.Generate; - -internal sealed class PartialClass ( - Compilation compilation, - ClassDeclarationSyntax syntax, - IReadOnlyList methods) -{ - public string Name { get; } = syntax.Identifier.ToString(); - - public string EmitSource () => - """ - #nullable enable - #pragma warning disable - - """ + - EmitUsings() + - WrapNamespace( - EmitHeader() + - EmitMethods() + - EmitFooter() - ); - - private string EmitUsings () - { - var usings = string.Join("\n", syntax.SyntaxTree.GetRoot() - .DescendantNodesAndSelf().OfType()); - return string.IsNullOrEmpty(usings) ? "" : usings + "\n\n"; - } - - private string EmitHeader () - { - var mods = syntax.Modifiers.ToString(); - if (!mods.Contains("unsafe")) mods = mods.Replace("partial", "unsafe partial"); - return $"{mods} class {syntax.Identifier}{syntax.TypeParameterList}{syntax.BaseList}{syntax.ConstraintClauses}\n{{"; - } - - private string EmitMethods () - { - var sources = methods.Select(m => " " + m.EmitSource(compilation)); - return "\n" + string.Join("\n", sources); - } - - private string EmitFooter () => "\n}"; - - private string WrapNamespace (string source) - { - if (syntax.Parent is NamespaceDeclarationSyntax space) - return $$""" - namespace {{space.Name}} - { - {{string.Join("\n", source.Split(["\r\n", "\r", "\n"], StringSplitOptions.None) - .Select((s, i) => i > 0 && s.Length > 0 ? " " + s : s))}} - } - """; - if (syntax.Parent is FileScopedNamespaceDeclarationSyntax fileSpace) - return $"namespace {fileSpace.Name};\n\n{source}"; - return source; - } -} diff --git a/src/cs/Bootsharp.Generate/PartialMethod.cs b/src/cs/Bootsharp.Generate/PartialMethod.cs deleted file mode 100644 index cb3a4e96..00000000 --- a/src/cs/Bootsharp.Generate/PartialMethod.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; -using Microsoft.CodeAnalysis.CSharp; -using Microsoft.CodeAnalysis.CSharp.Syntax; - -namespace Bootsharp.Generate; - -internal sealed class PartialMethod (MethodDeclarationSyntax syntax) -{ - private IMethodSymbol method = null!; - - public string EmitSource (Compilation compilation) - { - method = compilation.GetSemanticModel(syntax.SyntaxTree).GetDeclaredSymbol(syntax)!; - var ptrName = EmitPointerName(); - return $""" - private static delegate* managed<{EmitPointerSignature()}> {ptrName}; - {syntax.Modifiers} {EmitMethodSignature()} => {ptrName}({EmitArgs()}); - """; - } - - private string EmitPointerSignature () - { - var args = method.Parameters.Select(p => BuildSyntax(p.Type)).ToList(); - args.Add(BuildSyntax(method.ReturnType)); - return string.Join(", ", args); - } - - private string EmitPointerName () - { - var space = method.ContainingNamespace.IsGlobalNamespace ? method.ContainingType.Name - : string.Join("_", [..method.ContainingNamespace.ConstituentNamespaces, method.ContainingType.Name]); - return string.Concat($"Proxy_{space}_{method.Name}" - .Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_')); - } - - private string EmitMethodSignature () - { - var args = method.Parameters.Select(p => $"{BuildSyntax(p.Type)} {p.Name}"); - return $"{BuildSyntax(method.ReturnType)} {method.Name} ({string.Join(", ", args)})"; - } - - private string EmitArgs () - { - if (method.Parameters.Length == 0) return ""; - return string.Join(", ", method.Parameters.Select(p => p.Name)); - } - - private static string BuildSyntax (ITypeSymbol type) - { - if (type.SpecialType == SpecialType.System_Void) return "void"; - if (type is IArrayTypeSymbol arrayType) return $"{BuildSyntax(arrayType.ElementType)}[]"; - var nullable = type.NullableAnnotation == NullableAnnotation.Annotated ? "?" : ""; - if (IsGeneric(type, out var args)) return BuildGeneric(type, args) + nullable; - return $"global::{ResolveTypeName(type)}{nullable}"; - - static string BuildGeneric (ITypeSymbol type, ImmutableArray args) - { - if (type.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T) return BuildSyntax(args[0]); - return $"global::{ResolveTypeName(type)}<{string.Join(", ", args.Select(BuildSyntax))}>"; - } - - static string ResolveTypeName (ITypeSymbol type) - { - if (type.ContainingNamespace.IsGlobalNamespace) return type.Name; - return string.Join(".", type.ContainingNamespace.ConstituentNamespaces) + "." + type.Name; - } - - static bool IsGeneric (ITypeSymbol type, out ImmutableArray args) - { - args = type is INamedTypeSymbol { IsGenericType: true } named ? named.TypeArguments : default; - return args != default; - } - } -} diff --git a/src/cs/Bootsharp.Generate/SourceGenerator.cs b/src/cs/Bootsharp.Generate/SourceGenerator.cs index c10fd89a..d8830d1d 100644 --- a/src/cs/Bootsharp.Generate/SourceGenerator.cs +++ b/src/cs/Bootsharp.Generate/SourceGenerator.cs @@ -5,25 +5,23 @@ namespace Bootsharp.Generate; [Generator(LanguageNames.CSharp)] public sealed class SourceGenerator : IIncrementalGenerator { - public void Initialize (IncrementalGeneratorInitializationContext context) => context - .RegisterSourceOutput(context.CompilationProvider, Compile); + public void Initialize (IncrementalGeneratorInitializationContext ctx) => ctx + .RegisterSourceOutput(ctx.CompilationProvider, Compile); - private static void Compile (SourceProductionContext context, Compilation compilation) + private static void Compile (SourceProductionContext ctx, Compilation cmp) { - var receiver = VisitNodes(compilation); - foreach (var @class in receiver.FunctionClasses) - context.AddSource($"{@class.Name}Functions.g", @class.EmitSource()); - foreach (var @class in receiver.EventClasses) - context.AddSource($"{@class.Name}Events.g", @class.EmitSource()); + var receiver = VisitNodes(cmp); + foreach (var @class in receiver.ImportClasses) + ctx.AddSource($"{@class.Name}Imports.g", @class.EmitSource()); } - private static SyntaxReceiver VisitNodes (Compilation compilation) + private static SyntaxReceiver VisitNodes (Compilation cmp) { var receiver = new SyntaxReceiver(); - foreach (var tree in compilation.SyntaxTrees) + foreach (var tree in cmp.SyntaxTrees) if (!tree.FilePath.EndsWith(".g.cs")) foreach (var node in tree.GetRoot().DescendantNodesAndSelf()) - receiver.VisitNode(node, compilation); + receiver.VisitNode(node, cmp); return receiver; } } diff --git a/src/cs/Bootsharp.Generate/SyntaxReceiver.cs b/src/cs/Bootsharp.Generate/SyntaxReceiver.cs index 7f863372..a4cdbfaf 100644 --- a/src/cs/Bootsharp.Generate/SyntaxReceiver.cs +++ b/src/cs/Bootsharp.Generate/SyntaxReceiver.cs @@ -5,34 +5,41 @@ namespace Bootsharp.Generate; internal sealed class SyntaxReceiver { - public List FunctionClasses { get; } = []; - public List EventClasses { get; } = []; + public List ImportClasses { get; } = []; - public void VisitNode (SyntaxNode node, Compilation compilation) + public void VisitNode (SyntaxNode node, Compilation cmp) { if (node is ClassDeclarationSyntax classSyntax) - VisitClass(classSyntax, compilation); + VisitClass(classSyntax, cmp); } - private void VisitClass (ClassDeclarationSyntax syntax, Compilation compilation) + private void VisitClass (ClassDeclarationSyntax stx, Compilation cmp) { - var functions = GetMethodsWithAttribute(syntax, "JSFunction"); - if (functions.Count > 0) FunctionClasses.Add(new(compilation, syntax, functions)); - var events = GetMethodsWithAttribute(syntax, "JSEvent"); - if (events.Count > 0) EventClasses.Add(new(compilation, syntax, events)); + var methods = GetMethodsWithAttribute(stx, "Import"); + var events = GetEventsWithAttribute(stx, "Import"); + if (methods.Count > 0 || events.Count > 0) + ImportClasses.Add(new(cmp, stx, methods, events)); } - private List GetMethodsWithAttribute (ClassDeclarationSyntax syntax, string attribute) + private List GetMethodsWithAttribute (ClassDeclarationSyntax stx, string attribute) { - return syntax.Members + return stx.Members .OfType() .Where(s => HasAttribute(s, attribute)) - .Select(m => new PartialMethod(m)).ToList(); + .Select(m => new ImportMethod(m)).ToList(); } - private bool HasAttribute (MethodDeclarationSyntax syntax, string attributeName) + private List GetEventsWithAttribute (ClassDeclarationSyntax stx, string attribute) { - return syntax.AttributeLists + return stx.Members + .OfType() + .Where(s => s.Modifiers.Any(m => m.Text == "static") && HasAttribute(s, attribute)) + .Select(e => new ImportEvent(e)).ToList(); + } + + private bool HasAttribute (MemberDeclarationSyntax stx, string attributeName) + { + return stx.AttributeLists .SelectMany(l => l.Attributes) .Any(a => a.ToString().Contains(attributeName)); } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs index 10db4f4b..ae7b9573 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs @@ -28,11 +28,11 @@ internal static void RegisterDynamicDependencies () { } } [Fact] - public void AddsStaticInteropInterfaceImplementations () + public void AddsStaticInterfaceImplementations () { AddAssembly( - With("[assembly:JSExport(typeof(IExported), typeof(Space.IExported))]"), - With("[assembly:JSImport(typeof(IImported), typeof(Space.IImported))]"), + With("[assembly:Export(typeof(IExported), typeof(Space.IExported))]"), + With("[assembly:Import(typeof(IImported), typeof(Space.IImported))]"), With("public interface IExported {}"), With("public interface IImported {}"), With("Space", "public interface IExported {}"), @@ -45,12 +45,12 @@ public void AddsStaticInteropInterfaceImplementations () } [Fact] - public void AddsInstancedInteropInterfaceImplementations () + public void AddsInstancedInterfaceImplementations () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] public interface IExportedStatic { IExportedInstancedA CreateExported (); } public interface IImportedStatic { IImportedInstancedA CreateImported (); } @@ -62,8 +62,8 @@ public interface IImportedInstancedB { } public class Class { - [JSInvokable] public static IExportedInstancedB CreateExported () => default; - [JSFunction] public static IImportedInstancedB CreateImported () => default; + [Export] public static IExportedInstancedB CreateExported () => default; + [Import] public static IImportedInstancedB CreateImported () => default; } """)); Execute(); @@ -76,12 +76,12 @@ public class Class } [Fact] - public void AddsClassesWithStaticInteropMethods () + public void AddsClassesWithStaticInteropMembers () { AddAssembly("Assembly.With.Dots.dll", - With("SpaceA", "public class ClassA { [JSInvokable] public static void Foo () {} }"), - With("SpaceB.SpaceC", "public class ClassB { [JSFunction] public static void Foo () {} }"), - With("public class ClassC { [JSEvent] public static void Foo () {} }")); + With("SpaceA", "public class ClassA { [Export] public static void Foo () {} }"), + With("SpaceB.SpaceC", "public class ClassB { [Import] public static void Foo () {} }"), + With("public class ClassC { [Export] public static event Action? Evt; }")); Execute(); Added(All, "SpaceA.ClassA", "Assembly.With.Dots"); Added(All, "SpaceB.SpaceC.ClassB", "Assembly.With.Dots"); diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs index 7e42bcc2..894c4a68 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs @@ -9,12 +9,14 @@ public void GeneratesImplementationForExportedStaticInterface () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExported))] + [assembly:Export(typeof(IExported))] public record Record; public interface IExported { + event Action OnRecordChanged; + Record? Record { get; set; } void Inv (string? a); @@ -48,15 +50,17 @@ public class JSExported public JSExported (global::IExported handler) { JSExported.handler = handler; + handler.OnRecordChanged += OnRecordChanged.Invoke; } - [JSInvokable] public static global::Record? GetPropertyRecord () => handler.Record; - [JSInvokable] public static void SetPropertyRecord (global::Record? value) => handler.Record = value; - [JSInvokable] public static void Inv (global::System.String? a) => handler.Inv(a); - [JSInvokable] public static global::System.Threading.Tasks.Task InvAsync () => handler.InvAsync(); - [JSInvokable] public static global::Record? InvRecord () => handler.InvRecord(); - [JSInvokable] public static global::System.Threading.Tasks.Task InvAsyncResult () => handler.InvAsyncResult(); - [JSInvokable] public static global::System.String[] InvArray (global::System.Int32[] a) => handler.InvArray(a); + [Export] public static event global::System.Action OnRecordChanged; + [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); } } """); @@ -67,12 +71,14 @@ public void GeneratesImplementationForImportedStaticInterface () { AddAssembly(With( """ - [assembly:JSImport(typeof(IImported))] + [assembly:Import(typeof(IImported))] public record Record; public interface IImported { + event Action OnRecordChanged; + Record? Record { get; set; } void Inv (string? a); @@ -101,40 +107,42 @@ namespace Bootsharp.Generated.Imports { public class JSImported : global::IImported { + public event global::System.Action OnRecordChanged; + internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); global::Record? global::IImported.Record { - get => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(); - set => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(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.Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Inv(a); - global::System.Threading.Tasks.Task global::IImported.InvAsync () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsync(); - global::Record? global::IImported.InvRecord () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvRecord(); - global::System.Threading.Tasks.Task global::IImported.InvAsyncResult () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); - global::System.String[] global::IImported.InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvArray(a); + 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 GeneratesImplementationForInstancedImportInterface () + public void GeneratesImplementationForImportedInstanceInterface () { AddAssembly(With( """ public record Record; - public interface IExported { void Inv (string arg); } + public interface IImported { + event Action OnRecordChanged; + Record? Record { get; set; } void Fun (string arg); - void NotifyEvt (string arg); } public class Class { - [JSInvokable] public static IExported GetExported () => default; - [JSFunction] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); + [Import] public static IImported GetImported () => Proxies.Get>("Class.GetImported")(); } """)); Execute(); @@ -142,21 +150,52 @@ public class Class """ namespace Bootsharp.Generated.Imports { - public class JSImported(global::System.Int32 _id) : global::IImported + public class JSImported (global::System.Int32 id) : global::IImported { - ~JSImported() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + internal readonly global::System.Int32 _id = id; + + ~JSImported() + { + global::Bootsharp.Instances.DisposeImported(_id); + global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + } + public event global::System.Action OnRecordChanged; + internal void InvokeOnRecordChanged (global::Record? obj) => OnRecordChanged?.Invoke(obj); global::Record? global::IImported.Record { - get => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_GetPropertyRecord(_id); - set => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_SetPropertyRecord(_id, value); + 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.Proxy_Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); - void global::IImported.NotifyEvt (global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnEvt(_id, arg); + void global::IImported.Fun (global::System.String arg) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); } } """); - DoesNotContain("JSExported"); // Exported instances are authored by user and registered on initial interop. + } + + [Fact] + public void DoesNotGenerateImplementationForExportedInstanceInterface () + { + AddAssembly(With( + """ + public record Record; + + public interface IExported + { + event Action OnRecordChanged; + + Record? Record { get; set; } + + void Fun (string arg); + } + + public class Class + { + [Export] public static IExported GetExported () => default; + } + """)); + Execute(); + DoesNotContain("JSExported"); } [Fact] @@ -164,8 +203,8 @@ public void RespectsInterfaceNamespace () { AddAssembly(With( """ - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] namespace Space; @@ -201,7 +240,7 @@ public JSExported (global::Space.IExported handler) JSExported.handler = handler; } - [JSInvokable] public static void Inv (global::Space.Record a) => handler.Inv(a); + [Export] public static void Inv (global::Space.Record a) => handler.Inv(a); } } @@ -209,69 +248,7 @@ namespace Bootsharp.Generated.Imports.Space { public class JSImported : global::Space.IImported { - void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_Space_JSImported_Fun(a); - } - } - """); - } - - [Fact] - public void WhenImportedMethodStartsWithNotifyEmitsEvent () - { - AddAssembly(With( - """ - [assembly:JSImport(typeof(IImported))] - - public interface IImported { void NotifyFoo (); } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated.Imports - { - public class JSImported : global::IImported - { - void global::IImported.NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnFoo(); - } - } - """); - } - - [Fact] - public void RespectsEventPreference () - { - AddAssembly(With( - """ - [assembly:JSPreferences(Event = [@"^Broadcast(\S+)", "On$1"])] - [assembly:JSImport(typeof(IImported))] - - public interface IImported - { - void NotifyFoo (); - void BroadcastBar (); - } - """)); - Execute(); - Contains( - """ - namespace Bootsharp.Generated - { - internal static class InterfaceRegistrations - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterInterfaces () - { - Interfaces.Register(typeof(global::IImported), new ImportInterface(new Bootsharp.Generated.Imports.JSImported())); - } - } - } - - namespace Bootsharp.Generated.Imports - { - public class JSImported : global::IImported - { - void global::IImported.NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_NotifyFoo(); - void global::IImported.BroadcastBar () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnBar(); + void global::Space.IImported.Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Bootsharp_Generated_Imports_Space_JSImported_Fun(a); } } """); @@ -282,8 +259,8 @@ public void IgnoresImplementedInterfaceMethods () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] public interface IExportedStatic { int Foo () => 0; } public interface IImportedStatic { int Foo () => 0; } @@ -292,8 +269,8 @@ public interface IImportedInstanced { int Foo () => 0; } public class Class { - [JSInvokable] public static IExportedInstanced GetExported () => default; - [JSFunction] public static IImportedInstanced GetImported () => default; + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; } """)); Execute(); diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index 52da4c9d..61cca39c 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -21,8 +21,8 @@ public static partial class Interop public void GeneratesDisposeInstanceBindings () { Execute(); - Contains("JSExport] internal static void DisposeExportedInstance (int id) => Instances.Dispose(id);"); - Contains("""JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (int id);"""); + Contains("[JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id);"); + Contains("""[JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id);"""); } [Fact] @@ -33,11 +33,10 @@ public void GeneratesInitializersForEntryAndLibraryAssemblies () """ public static class Registry { + [Export] public static event Action? Evt; // (the fields are emitted by the source generators) - private static unsafe delegate* managed Proxy_Library_Registry_GetLabel; - private static unsafe delegate* managed Proxy_Library_Registry_OnBroadcast; - [JSFunction] public static string GetLabel (int count) => default!; - [JSEvent] public static void OnBroadcast (string value) { } + private static unsafe delegate* managed Bootsharp_GetLabel; + [Import] public static string GetLabel (int count) => default!; } """)); AddAssembly("Entry.dll", @@ -46,303 +45,435 @@ [JSEvent] public static void OnBroadcast (string value) { } public static class App { // (the field is emitted by the source generators) - private static unsafe delegate* managed Proxy_Entry_App_GetName; - [JSFunction] public static string GetName () => default!; + private static unsafe delegate* managed Bootsharp_GetName; + [Import] public static string GetName () => default!; } """)); Execute(); Contains( """ - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Proxy_Entry_App_GetName")] - private static extern unsafe ref delegate* managed Get_Proxy_Entry_App_GetName ([UnsafeAccessorType("Entry.App, Entry")] object? _); - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Proxy_Library_Registry_GetLabel")] - private static extern unsafe ref delegate* managed Get_Proxy_Library_Registry_GetLabel ([UnsafeAccessorType("Library.Registry, Library")] object? _); - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Proxy_Library_Registry_OnBroadcast")] - private static extern unsafe ref delegate* managed Get_Proxy_Library_Registry_OnBroadcast ([UnsafeAccessorType("Library.Registry, Library")] object? _); + [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_GetName")] + private static extern unsafe ref delegate* managed Access_Entry_App_GetName ([UnsafeAccessorType("Entry.App, Entry")] object? _); + [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_GetLabel")] + private static extern unsafe ref delegate* managed Access_Library_Registry_GetLabel ([UnsafeAccessorType("Library.Registry, Library")] object? _); [ModuleInitializer] internal static unsafe void Initialize () { - Get_Proxy_Entry_App_GetName(default) = &Proxy_Entry_App_GetName; - Get_Proxy_Library_Registry_GetLabel(default) = &Proxy_Library_Registry_GetLabel; - Get_Proxy_Library_Registry_OnBroadcast(default) = &Proxy_Library_Registry_OnBroadcast; + global::Library.Registry.Evt += Handle_Library_Registry_Evt; + Access_Entry_App_GetName(default) = &Entry_App_GetName; + Access_Library_Registry_GetLabel(default) = &Library_Registry_GetLabel; } """); } [Fact] - public void GeneratesForMethodsWithoutNamespace () + public void GeneratesForMembersWithoutNamespace () { AddAssembly(With( """ - public class Class + public partial class Class { - [JSInvokable] public static void Inv () {} - [JSFunction] public static void Fun () => Proxies.Get("Class.Fun")(); - [JSEvent] public static void Evt () => Proxies.Get("Class.Evt")(); + [Export] public static event Action? ExpEvt; + [Import] public static event Action? ImpEvt; + [Export] public static void Inv () {} + [Import] public static void Fun () => Proxies.Get("Class.Fun")(); } """)); Execute(); - Contains("JSExport] internal static void Class_Inv () => global::Class.Inv();"); - Contains("""JSImport("Class.funSerialized", "Bootsharp")] internal static partial void Class_Fun ();"""); - Contains("""JSImport("Class.evtSerialized", "Bootsharp")] internal static partial void Class_Evt ();"""); + Contains("void Handle_Class_ExpEvt () => Class_BroadcastExpEvt_Serialized();"); + Contains("""[JSImport("Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void Class_BroadcastExpEvt_Serialized ();"""); + Contains("[JSExport] internal static void Class_InvokeImpEvt () => global::Class.Bootsharp_Invoke_ImpEvt();"); + Contains("[JSExport] internal static void Class_Inv () => global::Class.Inv();"); + Contains("""[JSImport("Class.funSerialized", "Bootsharp")] internal static partial void Class_Fun_Serialized ();"""); } [Fact] - public void GeneratesForMethodsInCustomSpaces () + public void GeneratesForMembersInCustomSpaces () { AddAssembly(With( """ namespace SpaceA { - public class Class + public partial class Class { - [JSInvokable] public static void Inv () {} - [JSFunction] public static void Fun () => Proxies.Get("SpaceA.Class.Fun")(); - [JSEvent] public static void Evt () => Proxies.Get("SpaceA.Class.Evt")(); + [Export] public static event Action? ExpEvt; + [Export] public static void Inv () {} + [Import] public static void Fun () => Proxies.Get("SpaceA.Class.Fun")(); } } namespace SpaceA.SpaceB { - public class Class + public partial class Class { - [JSInvokable] public static void Inv () {} - [JSFunction] public static void Fun () => Proxies.Get("SpaceA.SpaceB.Class.Fun")(); - [JSEvent] public static void Evt () => Proxies.Get("SpaceA.SpaceB.Class.Evt")(); + [Import] public static event Action? ImpEvt; + [Export] public static void Inv () {} + [Import] public static void Fun () => Proxies.Get("SpaceA.SpaceB.Class.Fun")(); } } """)); Execute(); - Contains("JSExport] internal static void SpaceA_Class_Inv () => global::SpaceA.Class.Inv();"); - Contains("""JSImport("SpaceA.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_Class_Fun ();"""); - Contains("""JSImport("SpaceA.Class.evtSerialized", "Bootsharp")] internal static partial void SpaceA_Class_Evt ();"""); - Contains("JSExport] internal static void SpaceA_SpaceB_Class_Inv () => global::SpaceA.SpaceB.Class.Inv();"); - Contains("""JSImport("SpaceA.SpaceB.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_SpaceB_Class_Fun ();"""); - Contains("""JSImport("SpaceA.SpaceB.Class.evtSerialized", "Bootsharp")] internal static partial void SpaceA_SpaceB_Class_Evt ();"""); + Contains("""[JSImport("SpaceA.Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void SpaceA_Class_BroadcastExpEvt_Serialized ();"""); + Contains("void Handle_SpaceA_Class_ExpEvt () => SpaceA_Class_BroadcastExpEvt_Serialized();"); + Contains("[JSExport] internal static void SpaceA_Class_Inv () => global::SpaceA.Class.Inv();"); + Contains("""[JSImport("SpaceA.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_Class_Fun_Serialized ();"""); + Contains("[JSExport] internal static void SpaceA_SpaceB_Class_InvokeImpEvt () => global::SpaceA.SpaceB.Class.Bootsharp_Invoke_ImpEvt();"); + Contains("[JSExport] internal static void SpaceA_SpaceB_Class_Inv () => global::SpaceA.SpaceB.Class.Inv();"); + Contains("""[JSImport("SpaceA.SpaceB.Class.funSerialized", "Bootsharp")] internal static partial void SpaceA_SpaceB_Class_Fun_Serialized ();"""); } [Fact] - public void GeneratesForStaticInteropInterfaces () + public void IgnoresEventsWithoutImportExportAttributes () + { + AddAssembly(WithClass("public static event Action? Evt;")); + Execute(); + DoesNotContain("Evt"); + } + + [Fact] + public void GeneratesForMethodsInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(IImported))] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public record Info (string Value); - namespace Space { public interface IExported { void Inv (); } } - public interface IImported { void Fun (); void NotifyEvt(); } + public interface IExported { Info Inv (string str, Info info); } + public interface IImported { Info Fun (string str, Info info); } """)); Execute(); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_Inv () => global::Bootsharp.Generated.Exports.Space.JSExported.Inv();"); - Contains("""JSImport("Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImported_Fun ();"""); - Contains("""JSImport("Imported.onEvtSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImported_OnEvt ();"""); + Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_Space_JSExported_Inv (global::System.String str, [JSMarshalAs] global::System.Int64 info) => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.Inv(str, Serializer.Deserialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); + Contains("""[JSImport("Space.Imported.funSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized (global::System.String str, [JSMarshalAs] global::System.Int64 info);"""); + Contains("public static global::Space.Info Bootsharp_Generated_Imports_Space_JSImported_Fun (global::System.String str, global::Space.Info info) => Serializer.Deserialize(Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized(str, Serializer.Serialize(info, SerializerContext.Space_Info)), SerializerContext.Space_Info);"); } [Fact] - public void GeneratesForInstancedInteropInterfaces () + public void GeneratesForMethodsInInstancedInterfaces () { AddAssembly(With( """ - namespace Space - { - public interface IExported { void Inv (); } - public interface IImported { void Fun (); } - } + public record Info (string Value); - public interface IExported { void Inv (); } - public interface IImported { void NotifyEvt(); } + public interface IExported { Info Inv (IExported inst, Info info); } + public interface IImported { Info Fun (IImported inst, Info info); } - public class Class + public partial class Class { - [JSInvokable] public static Task GetExported (Space.IImported arg) => default; - [JSFunction] public static Task GetImported (IExported arg) => Proxies.Get>>("Class.GetImported")(arg); + [Export] public static Task GetExported (IImported inst) => default; + [Import] public static Task GetImported (IExported inst) => default; } """)); Execute(); - Contains("JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (global::System.Int32 arg) => Instances.Register(await global::Class.GetExported(new global::Bootsharp.Generated.Imports.Space.JSImported(arg)));"); - Contains("""JSImport("Class.getImportedSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Class_GetImported (global::System.Int32 arg);"""); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExported_Inv (global::System.Int32 _id) => ((global::IExported)Instances.Get(_id)).Inv();"); - Contains("""JSImport("Imported.onEvtSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImported_OnEvt (global::System.Int32 _id);"""); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_Inv (global::System.Int32 _id) => ((global::Space.IExported)Instances.Get(_id)).Inv();"); - Contains("""JSImport("Space.Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun (global::System.Int32 _id);"""); + 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);"""); } [Fact] - public void IgnoresImplementedInterfaceMethods () + public void GeneratesForPropertiesInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.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; } + namespace Space; - public class Class + public record Info; + + public interface IExported { - [JSInvokable] public static IExportedInstanced GetExported () => default; - [JSFunction] public static IImportedInstanced GetImported () => default; + Info State { get; set; } + bool Active { get; } + int Count { set; } + } + + public interface IImported + { + Info State { get; set; } + bool Active { get; } + int Count { set; } } """)); Execute(); - DoesNotContain("Foo"); + Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_Space_JSExported_GetPropertyState () => Serializer.Serialize(global::Bootsharp.Generated.Exports.Space.JSExported.GetPropertyState(), SerializerContext.Space_Info);"); + Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetPropertyState ([JSMarshalAs] global::System.Int64 value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetPropertyState(Serializer.Deserialize(value, SerializerContext.Space_Info));"); + Contains("""[JSImport("Space.Imported.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Imported_GetPropertyState_Serialized ();"""); + Contains("public static global::Space.Info Bootsharp_Generated_Imports_Space_JSImported_GetPropertyState() => Serializer.Deserialize(Space_Imported_GetPropertyState_Serialized(), SerializerContext.Space_Info);"); + Contains("""[JSImport("Space.Imported.setPropertyStateSerialized", "Bootsharp")] internal static partial void Space_Imported_SetPropertyState_Serialized ([JSMarshalAs] global::System.Int64 value);"""); + Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetPropertyState(global::Space.Info value) => Space_Imported_SetPropertyState_Serialized(Serializer.Serialize(value, SerializerContext.Space_Info));"); + Contains("[JSExport] internal static global::System.Boolean Bootsharp_Generated_Exports_Space_JSExported_GetPropertyActive () => global::Bootsharp.Generated.Exports.Space.JSExported.GetPropertyActive();"); + Contains("""[JSImport("Space.Imported.getPropertyActiveSerialized", "Bootsharp")] internal static partial global::System.Boolean Space_Imported_GetPropertyActive_Serialized ();"""); + Contains("public static global::System.Boolean Bootsharp_Generated_Imports_Space_JSImported_GetPropertyActive() => Space_Imported_GetPropertyActive_Serialized();"); + Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_SetPropertyCount (global::System.Int32 value) => global::Bootsharp.Generated.Exports.Space.JSExported.SetPropertyCount(value);"); + Contains("""[JSImport("Space.Imported.setPropertyCountSerialized", "Bootsharp")] internal static partial void Space_Imported_SetPropertyCount_Serialized (global::System.Int32 value);"""); + Contains("public static void Bootsharp_Generated_Imports_Space_JSImported_SetPropertyCount(global::System.Int32 value) => Space_Imported_SetPropertyCount_Serialized(value);"); } [Fact] - public void DoesntSerializeTypesThatShouldNotBeSerialized () + public void GeneratesForPropertiesInInstancedInterfaces () { AddAssembly(With( """ - namespace Space; + public record Info; + + public interface IExported + { + Info State { get; set; } + IExported Exported { get; } + IImported Imported { set; } + } + + public interface IImported + { + Info State { get; set; } + IImported Imported { get; } + IExported Exported { set; } + } public class Class { - [JSInvokable] public static Task Inv (bool a1, byte a2, char a3, short a4, long a5, int a6, float a7, double a8, nint a9, DateTime a10, DateTimeOffset a11, string a12) => default!; - [JSInvokable] public static Task InvNull (bool? a1, byte? a2, char? a3, short? a4, long? a5, int? a6, float? a7, double? a8, nint? a9, DateTime? a10, DateTimeOffset? a11, string? a12) => default!; - [JSFunction] public static Task Fun (bool a1, byte a2, char a3, short a4, long a5, int a6, float a7, double a8, nint a9, DateTime a10, DateTimeOffset a11, string a12) => Proxies.Get>>("Space.Class.Fun")(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); - [JSFunction] public static Task FunNull (bool? a1, byte? a2, char? a3, short? a4, long? a5, int? a6, float? a7, double? a8, nint? a9, DateTime? a10, DateTimeOffset? a11, string? a12) => Proxies.Get>>("Space.Class.FunNull")(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); + [Export] public static IExported GetExported (IImported arg) => default; + [Import] public static IImported GetImported (IExported arg) => default; } """)); Execute(); - Contains("JSExport] internal static global::System.Threading.Tasks.Task Space_Class_Inv (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12) => global::Space.Class.Inv(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); - Contains("JSExport] [return: JSMarshalAs>] internal static global::System.Threading.Tasks.Task Space_Class_InvNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12) => global::Space.Class.InvNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); - Contains("""JSImport("Space.Class.funSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_Fun (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12);"""); - Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); - Contains("""JSImport("Space.Class.funNullSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12);"""); - Contains("public static global::System.Threading.Tasks.Task Proxy_Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExported_GetPropertyState (global::System.Int32 _id) => Serializer.Serialize(Instances.Exported(_id).State, SerializerContext.Info);"); + Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetPropertyState (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 value) => Instances.Exported(_id).State = Serializer.Deserialize(value, SerializerContext.Info);"); + Contains("""[JSImport("Imported.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Imported_GetPropertyState_Serialized (global::System.Int32 _id);"""); + Contains("[JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExported_GetPropertyExported (global::System.Int32 _id) => Instances.Export(Instances.Exported(_id).Exported);"); + Contains("[JSExport] internal static void Bootsharp_Generated_Exports_JSExported_SetPropertyImported (global::System.Int32 _id, global::System.Int32 value) => Instances.Exported(_id).Imported = Instances.Import(value, static id => new global::Bootsharp.Generated.Imports.JSImported(id));"); + Contains("""[JSImport("Imported.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Imported_GetPropertyImported_Serialized (global::System.Int32 _id);"""); + Contains("public static global::IImported Bootsharp_Generated_Imports_JSImported_GetPropertyImported(global::System.Int32 _id) => Instances.Import(Imported_GetPropertyImported_Serialized(_id), static id => new global::Bootsharp.Generated.Imports.JSImported(id));"); + Contains("""[JSImport("Imported.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Imported_SetPropertyExported_Serialized (global::System.Int32 _id, global::System.Int32 value);"""); + Contains("public static void Bootsharp_Generated_Imports_JSImported_SetPropertyExported(global::System.Int32 _id, global::IExported value) => Imported_SetPropertyExported_Serialized(_id, Instances.Export(value));"); } [Fact] - public void SerializesTypesThatShouldBeSerialized () + public void GeneratesForEventsInStaticInterfaces () { AddAssembly(With( """ + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + namespace Space; + public record Info; + + public interface IExported { event Action Evt; } + public interface IImported { event Action Evt; } + """)); + Execute(); + Contains( + """ + [ModuleInitializer] + internal static unsafe void Initialize () + { + global::Bootsharp.Generated.Exports.Space.JSExported.Evt += Handle_Bootsharp_Generated_Exports_Space_JSExported_Evt; + } + """); + 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));"); + } + + [Fact] + public void GeneratesForEventsInInstancedInterfaces () + { + AddAssembly(With( + """ public record Record; - public class Class + public interface IExported { event Action Changed; } + public interface IImported { event Action Changed; } + + public partial class Class { - [JSInvokable] public static Record InvA (Record a) => default; - [JSInvokable] public static Task InvB (Record?[]? a) => default; - [JSFunction] public static Record FunA (Record a) => Proxies.Get>("Space.Class.FunA")(a); - [JSFunction] public static Task FunB (Record?[]? a) => Proxies.Get>>("Space.Class.FunB")(a); + [Export] public static IExported GetExported (IImported inst) => default; + [Import] public static IImported GetImported (IExported inst) => default; } """)); Execute(); - Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Space_Class_InvA ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(global::Space.Class.InvA(Serializer.Deserialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); - Contains("JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvB ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(await global::Space.Class.InvB(Serializer.Deserialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); - Contains("""JSImport("Space.Class.funASerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Class_FunA ([JSMarshalAs] global::System.Int64 a);"""); - Contains("public static global::Space.Record Proxy_Space_Class_FunA (global::Space.Record a) => Serializer.Deserialize(Space_Class_FunA(Serializer.Serialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); - Contains("""JSImport("Space.Class.funBSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB ([JSMarshalAs] global::System.Int64 a);"""); - Contains("public static async global::System.Threading.Tasks.Task Proxy_Space_Class_FunB (global::Space.Record?[]? a) => Serializer.Deserialize(await Space_Class_FunB(Serializer.Serialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); + 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)));"); } [Fact] - public void RespectsSpacePreference () + public void DoesNotGenerateForUnsupportedProperties () { AddAssembly(With( """ - [assembly:JSPreferences(Space = [@"Space", "Foo"])] - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] + [assembly:Export(typeof(IExportedStatic))] - namespace Space; + public interface IExportedStatic + { + int Ignored { get => 0; } + int IgnoredToo { set { } } + int this[int index] { get; set; } + } - public interface IExported { void Inv (); } - public interface IImported { void Fun (); void NotifyEvt(); } + public interface IExportedInstanced + { + int Ignored { get => 0; } + int IgnoredToo { set { } } + int this[int index] { get; set; } + } public class Class { - [JSInvokable] public static void Inv () {} - [JSFunction] public static void Fun () => Proxies.Get("Class.Fun")(); - [JSEvent] public static void Evt () => Proxies.Get("Class.Evt")(); + [Export] public static IExportedInstanced GetExported () => default; } """)); Execute(); - Contains("JSExport] internal static void Space_Class_Inv () => global::Space.Class.Inv();"); - Contains("""JSImport("Foo.Class.funSerialized", "Bootsharp")] internal static partial void Space_Class_Fun ();"""); - Contains("""JSImport("Foo.Class.evtSerialized", "Bootsharp")] internal static partial void Space_Class_Evt ();"""); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_Inv () => global::Bootsharp.Generated.Exports.Space.JSExported.Inv();"); - Contains("""JSImport("Foo.Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun ();"""); - Contains("""JSImport("Foo.Imported.onEvtSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_OnEvt ();"""); + DoesNotContain("Ignored"); + DoesNotContain("IgnoredToo"); + DoesNotContain("GetPropertyItem"); + DoesNotContain("SetPropertyItem"); } [Fact] - public void GeneratesForInterfaceProperties () + public void DoesNotEmitDuplicateInterfaceRegistrations () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] - - public record Record; + public interface IExported + { + event Action? Changed; + event Action? Done; + } - public interface IExportedStatic + public class Class { - Record State { get; set; } - IExportedInstanced Exported { get; } - IImportedInstanced Imported { set; } - int Count { set; } - int Ignored { get => 0; } - int IgnoredToo { set { } } - int this[int index] { get; set; } + [Export] public static IExported GetExported () => default; } + """)); + Execute(); + Once(@"private static int Register \(global::IExported instance\)"); + } - public interface IImportedStatic + [Fact] + public void IgnoresImplementedInterfaceMethods () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] + + 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 class Class { - Record State { get; set; } - IImportedInstanced Imported { get; } - IExportedInstanced Exported { set; } - int Count { set; } + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; } + """)); + Execute(); + DoesNotContain("Foo"); + } - public interface IExportedInstanced + [Fact] + public void DoesntSerializeTypesThatShouldNotBeSerialized () + { + AddAssembly(With( + """ + namespace Space; + + public class Class { - Record State { get; set; } - IExportedInstanced Exported { get; } - IImportedInstanced Imported { set; } + [Export] public static Task Inv (bool a1, byte a2, char a3, short a4, long a5, int a6, float a7, double a8, nint a9, DateTime a10, DateTimeOffset a11, string a12) => default!; + [Export] public static Task InvNull (bool? a1, byte? a2, char? a3, short? a4, long? a5, int? a6, float? a7, double? a8, nint? a9, DateTime? a10, DateTimeOffset? a11, string? a12) => default!; + [Import] public static Task Fun (bool a1, byte a2, char a3, short a4, long a5, int a6, float a7, double a8, nint a9, DateTime a10, DateTimeOffset a11, string a12) => Proxies.Get>>("Space.Class.Fun")(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); + [Import] public static Task FunNull (bool? a1, byte? a2, char? a3, short? a4, long? a5, int? a6, float? a7, double? a8, nint? a9, DateTime? a10, DateTimeOffset? a11, string? a12) => Proxies.Get>>("Space.Class.FunNull")(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12); } + """)); + Execute(); + Contains("[JSExport] internal static global::System.Threading.Tasks.Task Space_Class_Inv (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12) => global::Space.Class.Inv(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("[JSExport] [return: JSMarshalAs>] internal static global::System.Threading.Tasks.Task Space_Class_InvNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12) => global::Space.Class.InvNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("""[JSImport("Space.Class.funSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_Fun_Serialized (global::System.Boolean a1, global::System.Byte a2, global::System.Char a3, global::System.Int16 a4, [JSMarshalAs] global::System.Int64 a5, global::System.Int32 a6, global::System.Single a7, global::System.Double a8, global::System.IntPtr a9, [JSMarshalAs] global::System.DateTime a10, [JSMarshalAs] global::System.DateTimeOffset a11, global::System.String a12);"""); + Contains("public static global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull_Serialized(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + Contains("""[JSImport("Space.Class.funNullSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunNull_Serialized (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, [JSMarshalAs] global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, [JSMarshalAs] global::System.DateTime? a10, [JSMarshalAs] global::System.DateTimeOffset? a11, global::System.String? a12);"""); + Contains("public static global::System.Threading.Tasks.Task Space_Class_FunNull (global::System.Boolean? a1, global::System.Byte? a2, global::System.Char? a3, global::System.Int16? a4, global::System.Int64? a5, global::System.Int32? a6, global::System.Single? a7, global::System.Double? a8, global::System.IntPtr? a9, global::System.DateTime? a10, global::System.DateTimeOffset? a11, global::System.String? a12) => Space_Class_FunNull_Serialized(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12);"); + } - public interface IImportedInstanced + [Fact] + public void SerializesTypesThatShouldBeSerialized () + { + AddAssembly(With( + """ + namespace Space; + + public record Record; + + public partial class Class { - Record State { get; set; } - IImportedInstanced Imported { get; } - IExportedInstanced Exported { set; } + [Export] public static event Action? ExpEvt; + [Import] public static event Action? ImpEvt; + [Export] public static Record InvA (Record a) => default; + [Export] public static Task InvB (Record?[]? a) => default; + [Import] public static Record FunA (Record a) => Proxies.Get>("Space.Class.FunA")(a); + [Import] public static Task FunB (Record?[]? a) => Proxies.Get>>("Space.Class.FunB")(a); } + """)); + Execute(); + Contains("""[JSImport("Space.Class.broadcastExpEvtSerialized", "Bootsharp")] internal static partial void Space_Class_BroadcastExpEvt_Serialized ([JSMarshalAs] global::System.Int64 obj);"""); + Contains("void Handle_Space_Class_ExpEvt (global::Space.Record obj) => Space_Class_BroadcastExpEvt_Serialized(Serializer.Serialize(obj, SerializerContext.Space_Record));"); + Contains("[JSExport] internal static void Space_Class_InvokeImpEvt ([JSMarshalAs] global::System.Int64 arg1, global::System.Int32 arg2) => global::Space.Class.Bootsharp_Invoke_ImpEvt(Serializer.Deserialize(arg1, SerializerContext.Space_Record), arg2);"); + Contains("[JSExport] [return: JSMarshalAs] internal static global::System.Int64 Space_Class_InvA ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(global::Space.Class.InvA(Serializer.Deserialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); + Contains("[JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvB ([JSMarshalAs] global::System.Int64 a) => Serializer.Serialize(await global::Space.Class.InvB(Serializer.Deserialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); + Contains("""[JSImport("Space.Class.funASerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Space_Class_FunA_Serialized ([JSMarshalAs] global::System.Int64 a);"""); + Contains("public static global::Space.Record Space_Class_FunA (global::Space.Record a) => Serializer.Deserialize(Space_Class_FunA_Serialized(Serializer.Serialize(a, SerializerContext.Space_Record)), SerializerContext.Space_Record);"); + Contains("""[JSImport("Space.Class.funBSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB_Serialized ([JSMarshalAs] global::System.Int64 a);"""); + Contains("public static async global::System.Threading.Tasks.Task Space_Class_FunB (global::Space.Record?[]? a) => Serializer.Deserialize(await Space_Class_FunB_Serialized(Serializer.Serialize(a, SerializerContext.Space_RecordArray)), SerializerContext.Space_RecordArray);"); + } + + [Fact] + public void RespectsSpacePref () + { + AddAssembly(With( + """ + [assembly:Preferences(Space = [@"Space", "Foo"])] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public interface IExported { void Inv (); } + public interface IImported { void Fun (); } public class Class { - [JSInvokable] public static IExportedInstanced GetExported (IImportedInstanced arg) => default; - [JSFunction] public static IImportedInstanced GetImported (IExportedInstanced arg) => default; + [Export] public static event Action? Evt; + [Export] public static void Inv () {} + [Import] public static void Fun () => Proxies.Get("Class.Fun")(); } """)); Execute(); - Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState () => Serializer.Serialize(global::Bootsharp.Generated.Exports.JSExportedStatic.GetPropertyState(), SerializerContext.Record);"); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyState ([JSMarshalAs] global::System.Int64 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyState(Serializer.Deserialize(value, SerializerContext.Record));"); - Contains("""JSImport("ImportedStatic.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState ();"""); - Contains("public static global::Record Proxy_Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState() => Serializer.Deserialize(Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyState(), SerializerContext.Record);"); - Contains("""JSImport("ImportedStatic.setPropertyStateSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState ([JSMarshalAs] global::System.Int64 value);"""); - Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState(global::Record value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyState(Serializer.Serialize(value, SerializerContext.Record));"); - Contains("JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyExported () => Instances.Register(global::Bootsharp.Generated.Exports.JSExportedStatic.GetPropertyExported());"); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyImported (global::System.Int32 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyImported(new global::Bootsharp.Generated.Imports.JSImportedInstanced(value));"); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyCount (global::System.Int32 value) => global::Bootsharp.Generated.Exports.JSExportedStatic.SetPropertyCount(value);"); - Contains("""JSImport("ImportedStatic.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported ();"""); - Contains("public static global::IImportedInstanced Proxy_Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported() => (global::IImportedInstanced)new global::Bootsharp.Generated.Imports.JSImportedInstanced(Bootsharp_Generated_Imports_JSImportedStatic_GetPropertyImported());"); - Contains("""JSImport("ImportedStatic.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported (global::System.Int32 value);"""); - Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported(global::IExportedInstanced value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyExported(Instances.Register(value));"); - Contains("""JSImport("ImportedStatic.setPropertyCountSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount (global::System.Int32 value);"""); - Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount(global::System.Int32 value) => Bootsharp_Generated_Imports_JSImportedStatic_SetPropertyCount(value);"); - Contains("JSExport] [return: JSMarshalAs] internal static global::System.Int64 Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyState (global::System.Int32 _id) => Serializer.Serialize(((global::IExportedInstanced)Instances.Get(_id)).State, SerializerContext.Record);"); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyState (global::System.Int32 _id, [JSMarshalAs] global::System.Int64 value) => ((global::IExportedInstanced)Instances.Get(_id)).State = Serializer.Deserialize(value, SerializerContext.Record);"); - Contains("""JSImport("ImportedInstanced.getPropertyStateSerialized", "Bootsharp")] [return: JSMarshalAs] internal static partial global::System.Int64 Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyState (global::System.Int32 _id);"""); - Contains("JSExport] internal static global::System.Int32 Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyExported (global::System.Int32 _id) => Instances.Register(((global::IExportedInstanced)Instances.Get(_id)).Exported);"); - Contains("JSExport] internal static void Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyImported (global::System.Int32 _id, global::System.Int32 value) => ((global::IExportedInstanced)Instances.Get(_id)).Imported = new global::Bootsharp.Generated.Imports.JSImportedInstanced(value);"); - Contains("""JSImport("ImportedInstanced.getPropertyImportedSerialized", "Bootsharp")] internal static partial global::System.Int32 Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported (global::System.Int32 _id);"""); - Contains("public static global::IImportedInstanced Proxy_Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported(global::System.Int32 _id) => (global::IImportedInstanced)new global::Bootsharp.Generated.Imports.JSImportedInstanced(Bootsharp_Generated_Imports_JSImportedInstanced_GetPropertyImported(_id));"); - Contains("""JSImport("ImportedInstanced.setPropertyExportedSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported (global::System.Int32 _id, global::System.Int32 value);"""); - Contains("public static void Proxy_Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported(global::System.Int32 _id, global::IExportedInstanced value) => Bootsharp_Generated_Imports_JSImportedInstanced_SetPropertyExported(_id, Instances.Register(value));"); + Contains("[JSExport] internal static void Bootsharp_Generated_Exports_Space_JSExported_Inv () => global::Bootsharp.Generated.Exports.Space.JSExported.Inv();"); + Contains("""[JSImport("Foo.Imported.funSerialized", "Bootsharp")] internal static partial void Bootsharp_Generated_Imports_Space_JSImported_Fun_Serialized ();"""); + Contains("""[JSImport("Foo.Class.broadcastEvtSerialized", "Bootsharp")] internal static partial void Foo_Class_BroadcastEvt_Serialized ();"""); + Contains("void Handle_Space_Class_Evt () => Foo_Class_BroadcastEvt_Serialized();"); + Contains("[JSExport] internal static void Space_Class_Inv () => global::Space.Class.Inv();"); + Contains("""[JSImport("Foo.Class.funSerialized", "Bootsharp")] internal static partial void Space_Class_Fun_Serialized ();"""); } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index e9e2929e..b4b07664 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -15,7 +15,7 @@ public void WhenNothingInspectedIsEmpty () public void WhenNoSerializableTypesIsEmpty () { AddAssembly( - WithClass("[JSInvokable] public static bool? Foo (int a, char b, DateTime c, DateTimeOffset d) => default;") + WithClass("[Export] public static bool? Foo (int a, char b, DateTime c, DateTimeOffset d) => default;") ); Execute(); DoesNotContain("Binary<"); @@ -56,7 +56,7 @@ public record Node( public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -100,7 +100,7 @@ public record Node( public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -121,7 +121,7 @@ public record Node (string Id, Node? Parent, Node? Child); public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -147,8 +147,8 @@ public record Node( public class Class { - [JSInvokable] public static Node Echo (Node node) => node; - [JSInvokable] public static Task EchoBytesAsync (byte[] bytes) => default; + [Export] public static Node Echo (Node node) => node; + [Export] public static Task EchoBytesAsync (byte[] bytes) => default; } """)); Execute(); @@ -179,7 +179,7 @@ public record Info(Item?[]?[]? Items); public class Class { - [JSInvokable] public static Info Echo (Info info) => info; + [Export] public static Info Echo (Info info) => info; } """)); Execute(); @@ -200,7 +200,7 @@ public record Node (List? Children); public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -223,7 +223,7 @@ public class Node public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -244,7 +244,7 @@ public Node () { } public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -264,7 +264,7 @@ public class Node public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -290,8 +290,8 @@ public class RecordB public class Class { - [JSInvokable] public static RecordA A (RecordA a) => a; - [JSInvokable] public static RecordB B (RecordB b) => b; + [Export] public static RecordA A (RecordA a) => a; + [Export] public static RecordB B (RecordB b) => b; } """)); Execute(); @@ -314,7 +314,7 @@ public readonly record struct CompletionItem public class Class { - [JSInvokable] public static CompletionItem Echo (CompletionItem item) => item; + [Export] public static CompletionItem Echo (CompletionItem item) => item; } """)); Execute(); @@ -334,7 +334,7 @@ public CompletionItem () { } public class Class { - [JSInvokable] public static CompletionItem Echo (CompletionItem item) => item; + [Export] public static CompletionItem Echo (CompletionItem item) => item; } """)); Execute(); @@ -354,7 +354,7 @@ public struct Node public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -374,7 +374,7 @@ public struct Node public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -394,7 +394,7 @@ public record Node public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -413,7 +413,7 @@ public string WriteOnly { set { } } public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -421,7 +421,7 @@ public class Class } [Fact] - public void SerializesTypesFromInteropMethods () + public void SerializesTypesFromStaticMethods () { AddAssembly(With( """ @@ -431,8 +431,8 @@ public record RecordC; public class Class { - [JSInvokable] public static Task A (RecordC c) => default; - [JSFunction] public static RecordB[] B (RecordC[] c) => default; + [Export] public static Task A (RecordC c) => default; + [Import] public static RecordB[] B (RecordC[] c) => default; } """)); Execute(); @@ -445,7 +445,7 @@ public class Class } [Fact] - public void SerializesTypesFromInteropInterfaces () + public void SerializesTypesFromInstancedInterfaces () { AddAssembly(With( """ @@ -457,7 +457,7 @@ public interface IImported { void Fun (RecordB b); void NotifyEvt(RecordC c); } public class Class { - [JSFunction] public static Task GetImported (IExported arg) => default; + [Import] public static Task GetImported (IExported arg) => default; } """)); Execute(); @@ -467,7 +467,7 @@ public class Class } [Fact] - public void DoesntSerializeInstancedInteropInterfacesThemselves () + public void DoesntSerializeInstancedInterfacesThemselves () { AddAssembly(With( """ @@ -482,8 +482,8 @@ public interface IImported { void Fun (); void NotifyEvt(); } public class Class { - [JSInvokable] public static Space.IExported GetExported (Space.IImported arg) => default; - [JSFunction] public static Task GetImported (IExported arg) => default; + [Export] public static Space.IExported GetExported (Space.IImported arg) => default; + [Import] public static Task GetImported (IExported arg) => default; } """)); Execute(); @@ -514,10 +514,10 @@ namespace Foo; public class Class { - [JSFunction] public static IRegistry GetRegistry () => default; - [JSInvokable] public static Record Echo (Record r) => default; - [JSInvokable] public static Task> List (IReadOnlyList r) => default; - [JSInvokable] public static Task> Map (IReadOnlyDictionary r) => default; + [Import] public static IRegistry GetRegistry () => default; + [Export] public static Record Echo (Record r) => default; + [Export] public static Task> List (IReadOnlyList r) => default; + [Export] public static Task> Map (IReadOnlyDictionary r) => default; } """)); Execute(); @@ -541,7 +541,7 @@ public void SerializesAllTheCrawledSerializableTypes () 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", "[JSInvokable] public static Task GetBaz (Enum e) => default;")); + WithClass("n", "[Export] public static Task GetBaz (Enum e) => default;")); Execute(); Contains("Binary "); Contains("Binary "); diff --git a/src/cs/Bootsharp.Publish.Test/Mock/MockProject.cs b/src/cs/Bootsharp.Publish.Test/Mock/MockProject.cs index 482f5f27..798c0bf1 100644 --- a/src/cs/Bootsharp.Publish.Test/Mock/MockProject.cs +++ b/src/cs/Bootsharp.Publish.Test/Mock/MockProject.cs @@ -54,7 +54,7 @@ private static string[] GetReferencePaths () return [ MetadataReference.CreateFromFile(Path.Combine(coreDir, "System.Runtime.dll")).FilePath, MetadataReference.CreateFromFile(typeof(object).Assembly.Location).FilePath, - MetadataReference.CreateFromFile(typeof(JSExportAttribute).Assembly.Location).FilePath + MetadataReference.CreateFromFile(typeof(ExportAttribute).Assembly.Location).FilePath ]; } } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 87f48e37..3228b2e0 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -11,39 +11,31 @@ public void WhenNoBindingsNothingIsGenerated () Assert.Empty(TestedContent); } - [Fact] - public void InteropFunctionsImported () - { - AddAssembly(WithClass("[JSInvokable] public static void Inv () {}")); - Execute(); - Contains( - """ - import { exports } from "./exports"; - import { Event } from "./event"; - import { registerInstance, getInstance, disposeOnFinalize } from "./instances"; - """); - } - [Fact] public void WhenDebugEnabledEmitsAndUsesExportImportHelpers () { Task.Debug = true; AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] + [assembly:Export(typeof(IExportedStatic))] public interface IExportedStatic { int State { get; set; } } + public interface IImportedInstanced { event Action? Changed; } - public class Class + public partial class Class { - [JSInvokable] public static Task InvAsync () => Task.FromResult(0); - [JSFunction] public static void Fun () {} + [Import] public static event Action? Evt; + [Export] public static Task InvAsync () => Task.FromResult(0); + [Export] public static void UseImported (IImportedInstanced inst) {} + [Import] public static void Fun () {} } """)); Execute(); Contains("function getExport"); Contains("function getImport"); + Contains("""getExport("Class_InvokeEvt")"""); Contains("""getExport("Class_InvAsync")"""); + Contains("""getExport("Bootsharp_Generated_Imports_JSImportedInstanced_InvokeChanged")"""); Contains("""getExport("Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState")"""); Contains("""getImport(this.funHandler, this.funSerializedHandler, "Class.fun")"""); } @@ -54,14 +46,14 @@ public void WhenDebugDisabledDoesntEmitAndDoesntUseExportImportHelpers () Task.Debug = false; AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] + [assembly:Export(typeof(IExportedStatic))] public interface IExportedStatic { int State { get; set; } } public class Class { - [JSInvokable] public static Task InvAsync () => Task.FromResult(0); - [JSFunction] public static void Fun () {} + [Export] public static Task InvAsync () => Task.FromResult(0); + [Import] public static void Fun () {} } """)); Execute(); @@ -75,7 +67,7 @@ [JSFunction] public static void Fun () {} [Fact] public void BindingForInvokableMethodIsGenerated () { - AddAssembly(WithClass("Foo.Bar", "[JSInvokable] public static void Nya () {}")); + AddAssembly(WithClass("Foo.Bar", "[Export] public static void Nya () {}")); Execute(); Contains( """ @@ -92,7 +84,7 @@ public void BindingForInvokableMethodIsGenerated () [Fact] public void BindingForFunctionMethodIsGenerated () { - AddAssembly(WithClass("Foo.Bar", "[JSFunction] public static void Fun () {}")); + AddAssembly(WithClass("Foo.Bar", "[Import] public static void Fun () {}")); Execute(); Contains( """ @@ -109,22 +101,21 @@ public void BindingForFunctionMethodIsGenerated () } [Fact] - public void BindingForEventMethodIsGenerated () + public void BindingForStaticEventsIsGenerated () { AddAssembly( - WithClass("[JSEvent] public static void OnFoo () {}"), - WithClass("[JSEvent] public static void OnBar (string a) {}"), - WithClass("[JSEvent] public static void OnBaz (int a, bool b) {}")); + WithClass("[Export] public static event Action? ExpEvt;"), + WithClass("[Export] public static event Action? Evt;"), + WithClass("[Import] public static event Action? ImpEvt;")); Execute(); Contains( """ export const Class = { - onFoo: new Event(), - onFooSerialized: () => Class.onFoo.broadcast(), - onBar: new Event(), - onBarSerialized: (a) => Class.onBar.broadcast(a), - onBaz: new Event(), - onBazSerialized: (a, b) => Class.onBaz.broadcast(a, b) + expEvt: new Event(), + broadcastExpEvtSerialized: () => Class.expEvt.broadcast(), + evt: new Event(), + broadcastEvtSerialized: (obj) => Class.evt.broadcast(obj), + impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, arg2)) }; """); } @@ -132,7 +123,7 @@ public void BindingForEventMethodIsGenerated () [Fact] public void LibraryExportsNamespaceObject () { - AddAssembly(WithClass("Foo", "[JSInvokable] public static void Bar () {}")); + AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); Execute(); Contains( """ @@ -147,7 +138,7 @@ public void LibraryExportsNamespaceObject () [Fact] public void WhenSpaceContainDotsObjectCreatedForEachPart () { - AddAssembly(WithClass("Foo.Bar.Nya", "[JSInvokable] public static void Bar () {}")); + AddAssembly(WithClass("Foo.Bar.Nya", "[Export] public static void Bar () {}")); Execute(); Contains( """ @@ -167,8 +158,8 @@ public void WhenSpaceContainDotsObjectCreatedForEachPart () public void WhenMultipleSpacesEachGetItsOwnObject () { AddAssembly( - WithClass("Foo", "[JSInvokable] public static void Foo () {}"), - WithClass("Bar.Nya", "[JSFunction] public static void Fun () {}")); + WithClass("Foo", "[Export] public static void Foo () {}"), + WithClass("Bar.Nya", "[Import] public static void Fun () {}")); Execute(); Contains( """ @@ -192,8 +183,8 @@ public void WhenMultipleSpacesEachGetItsOwnObject () [Fact] public void WhenMultipleAssembliesWithEqualSpaceObjectDeclaredOnlyOnce () { - AddAssembly(WithClass("Foo", "[JSInvokable] public static void Bar () {}")); - AddAssembly(WithClass("Foo", "[JSFunction] public static void Fun () {}")); + AddAssembly(WithClass("Foo", "[Export] public static void Bar () {}")); + AddAssembly(WithClass("Foo", "[Import] public static void Fun () {}")); Execute(); Once("export const Foo"); Contains("bar: () => exports.Foo_Class_Bar()"); @@ -209,8 +200,8 @@ public void WhenMultipleAssembliesWithEqualSpaceObjectDeclaredOnlyOnce () public void DifferentSpacesWithSameRootAssignedUnderSameObject () { AddAssembly( - WithClass("Nya.Foo", "[JSInvokable] public static void Foo () {}"), - WithClass("Nya.Bar", "[JSFunction] public static void Fun () {}")); + WithClass("Nya.Foo", "[Export] public static void Foo () {}"), + WithClass("Nya.Bar", "[Import] public static void Fun () {}")); Execute(); Contains( """ @@ -235,8 +226,8 @@ public void DifferentSpacesWithSameRootAssignedUnderSameObject () public void DifferentSpacesStartingEquallyAreNotAssignedToSameObject () { AddAssembly( - WithClass("Foo", "[JSInvokable] public static void Method () {}"), - WithClass("FooBar.Baz", "[JSInvokable] public static void Method () {}") + WithClass("Foo", "[Export] public static void Method () {}"), + WithClass("FooBar.Baz", "[Export] public static void Method () {}") ); Execute(); Contains( @@ -259,8 +250,8 @@ public void DifferentSpacesStartingEquallyAreNotAssignedToSameObject () [Fact] public void BindingsFromMultipleSpacesAssignedToRespectiveObjects () { - AddAssembly(WithClass("Foo", "[JSInvokable] public static int Foo () => 0;")); - AddAssembly(WithClass("Bar.Nya", "[JSFunction] public static void Fun () {}")); + AddAssembly(WithClass("Foo", "[Export] public static int Foo () => 0;")); + AddAssembly(WithClass("Bar.Nya", "[Import] public static void Fun () {}")); Execute(); Contains( """ @@ -285,8 +276,8 @@ public void BindingsFromMultipleSpacesAssignedToRespectiveObjects () public void BindingsFromMultipleClassesAssignedToRespectiveObjects () { AddAssembly( - With("public class ClassA { [JSInvokable] public static void Inv () {} }"), - With("public class ClassB { [JSFunction] public static void Fun () {} }")); + With("public class ClassA { [Export] public static void Inv () {} }"), + With("public class ClassB { [Import] public static void Fun () {} }")); Execute(); Contains( """ @@ -305,8 +296,8 @@ public void BindingsFromMultipleClassesAssignedToRespectiveObjects () public void WhenNoSpaceBindingsAreAssignedToClassObject () { AddAssembly( - WithClass("[JSInvokable] public static Task Nya () => Task.FromResult(0);"), - WithClass("[JSFunction] public static void Fun () {}")); + WithClass("[Export] public static Task Nya () => Task.FromResult(0);"), + WithClass("[Import] public static void Fun () {}")); Execute(); Contains( """ @@ -322,7 +313,7 @@ public void WhenNoSpaceBindingsAreAssignedToClassObject () [Fact] public void VariablesConflictingWithJSTypesAreRenamed () { - AddAssembly(WithClass("[JSInvokable] public static void Fun (string function) {}")); + AddAssembly(WithClass("[Export] public static void Fun (string function) {}")); Execute(); Contains( """ @@ -337,22 +328,21 @@ public void SerializesUserType () { AddAssembly( With("public record Info (DateTimeOffset Date, nint Ptr, Info? Self);"), - WithClass("[JSInvokable] public static Info Foo (Info i) => default;"), - WithClass("[JSFunction] public static Info? Bar (Info? i) => default;"), - WithClass("[JSEvent] public static void Baz (Info?[]? i) {}"), - WithClass("[JSEvent] public static void Yaz (int a, Info i) {}")); + WithClass("[Export] public static event Action? ExpEvt;"), + WithClass("[Import] public static event Action? ImpEvt;"), + WithClass("[Export] public static Info Foo (Info i) => default;"), + WithClass("[Import] public static Info? Bar (Info? i) => default;")); Execute(); Contains( """ export const Class = { + expEvt: new Event(), + broadcastExpEvtSerialized: (arg1, arg2) => Class.expEvt.broadcast(deserialize(arg1, InfoArray) ?? undefined, deserialize(arg2, Info)), + impEvt: importEvent((arg1, arg2) => exports.Class_InvokeImpEvt(arg1, serialize(arg2, Info))), foo: (i) => deserialize(exports.Class_Foo(serialize(i, Info)), Info), get bar() { return this.barHandler; }, set bar(handler) { this.barHandler = handler; this.barSerializedHandler = (i) => serialize(this.barHandler(deserialize(i, Info)), Info); }, - get barSerialized() { return this.barSerializedHandler; }, - baz: new Event(), - bazSerialized: (i) => Class.baz.broadcast(deserialize(i, InfoArray) ?? undefined), - yaz: new Event(), - yazSerialized: (a, i) => Class.yaz.broadcast(a, deserialize(i, Info)) + get barSerialized() { return this.barSerializedHandler; } }; """); } @@ -392,7 +382,7 @@ public record Node( public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -428,7 +418,7 @@ public record Node (List? Children); public class Class { - [JSInvokable] public static Node Echo (Node node) => node; + [Export] public static Node Echo (Node node) => node; } """)); Execute(); @@ -444,10 +434,10 @@ public void AwaitsWhenSerializingInAsyncFunctions () { AddAssembly( With("public record Info;"), - WithClass("[JSInvokable] public static Task Foo (Info i) => default;"), - WithClass("[JSFunction] public static Task Bar (Info? i) => default;"), - WithClass("[JSInvokable] public static Task> Baz () => default;"), - WithClass("[JSFunction] public static Task> Yaz () => default;")); + WithClass("[Export] public static Task Foo (Info i) => default;"), + WithClass("[Import] public static Task Bar (Info? i) => default;"), + WithClass("[Export] public static Task> Baz () => default;"), + WithClass("[Import] public static Task> Yaz () => default;")); Execute(); Contains( """ @@ -469,7 +459,7 @@ public void ExportedEnumsAreDeclaredInJS () { AddAssembly( WithClass("n", "public enum Foo { A, B }"), - WithClass("n", "[JSInvokable] public static Foo GetFoo () => default;")); + WithClass("n", "[Export] public static Foo GetFoo () => default;")); Execute(); Contains( """ @@ -487,7 +477,7 @@ public void DoesntDeclareSystemEnums () { AddAssembly( WithClass("n", "public enum Foo { A, B }"), - WithClass("n", "[JSInvokable] public static Task GetFoo () => default;")); + WithClass("n", "[Export] public static Task GetFoo () => default;")); Execute(); Contains("Foo"); DoesNotContain("LayoutKind"); @@ -501,7 +491,7 @@ public void CustomEnumIndexesArePreservedInJS () { AddAssembly( With("n", "public enum Foo { A = 1, B = 6 }"), - WithClass("n", "[JSInvokable] public static Foo GetFoo () => default;")); + WithClass("n", "[Export] public static Foo GetFoo () => default;")); Execute(); Contains( """ @@ -515,17 +505,17 @@ public void CustomEnumIndexesArePreservedInJS () } [Fact] - public void RespectsSpacePreference () + public void RespectsSpacePreferenceInStaticMembers () { AddAssembly( With( """ - [assembly: Bootsharp.JSPreferences( + [assembly: Bootsharp.Preferences( Space = [@"^Foo\.Bar\.(\S+)", "$1"] )] """), - WithClass("Foo.Bar.Nya", "[JSInvokable] public static Task GetNya () => Task.CompletedTask;"), - WithClass("Foo.Bar.Fun", "[JSFunction] public static void OnFun () {}")); + WithClass("Foo.Bar.Nya", "[Export] public static Task GetNya () => Task.CompletedTask;"), + WithClass("Foo.Bar.Fun", "[Import] public static void OnFun () {}")); Execute(); Contains( """ @@ -544,12 +534,38 @@ public void RespectsSpacePreference () """); } + [Fact] + public void RespectsSpacePreferenceInStaticInterfaces () + { + AddAssembly(With( + """ + [assembly:Preferences(Space = [@".+", "Foo"])] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; + + public interface IExported { void Inv (); } + public interface IImported { void Fun (); } + """)); + Execute(); + Contains( + """ + export const Foo = { + inv: () => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(), + get fun() { return this.funHandler; }, + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = () => this.funHandler(); }, + get funSerialized() { return this.funSerializedHandler; } + }; + """); + } + [Fact] public void RespectsFunctionPreference () { AddAssembly( - With("""[assembly:JSPreferences(Function = [@".+", "foo"])]"""), - WithClass("Space", "[JSInvokable] public static void Inv () {}") + With("""[assembly:Preferences(Function = [@".+", "foo"])]"""), + WithClass("Space", "[Export] public static void Inv () {}") ); Execute(); Contains( @@ -567,8 +583,8 @@ public void IgnoresBindingsInGeneratedNamespace () { AddAssembly(With("Bootsharp.Generated", """ - public static class Exports { [JSInvokable] public static void Inv () {} } - public static class Imports { [JSFunction] public static void Fun () {} } + public static class Exports { [Export] public static void Inv () {} } + public static class Imports { [Import] public static void Fun () {} } """)); Execute(); DoesNotContain("inv: () =>"); @@ -576,233 +592,286 @@ public static class Imports { [JSFunction] public static void Fun () {} } } [Fact] - public void GeneratesForStaticInteropInterfaces () + public void GeneratesForMethodsInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] namespace Space; - public enum Enum { A, B } + public record Info (string Value); - public interface IExported { void Inv (string s, Enum e); } - public interface IImported { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } + public interface IExported { Info Inv (string str, Info info); } + public interface IImported { Info Fun (string str, Info info); } """)); Execute(); Contains( """ export const Space = { - Enum: { "0": "A", "1": "B", "A": 0, "B": 1 }, Exported: { - inv: (s, e) => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(s, serialize(e, Space_Enum)) + inv: (str, info) => deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(str, serialize(info, Space_Info)), Space_Info) }, Imported: { get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = (s, e) => this.funHandler(s, deserialize(e, Space_Enum)); }, - get funSerialized() { return this.funSerializedHandler; }, - onEvt: new Event(), - onEvtSerialized: (s, e) => Space.Imported.onEvt.broadcast(s, deserialize(e, Space_Enum)) + set fun(handler) { this.funHandler = handler; this.funSerializedHandler = (str, info) => serialize(this.funHandler(str, deserialize(info, Space_Info)), Space_Info); }, + get funSerialized() { return this.funSerializedHandler; } } }; """); } [Fact] - public void GeneratesForStaticInteropInterfacesWithSpacePref () + public void GeneratesForMethodsInInstancedInterfaces () { AddAssembly(With( """ - [assembly:JSPreferences(Space = [@".+", "Foo"])] - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] - - namespace Space; + public record Info (string Value); - public enum Enum { A, B } + public interface IExported { Info Inv (IExported inst, Info info); } + public interface IImported { Info Fun (IImported inst, Info info); } - public interface IExported { void Inv (string s, Enum e); } - public interface IImported { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } + public partial class Class + { + [Export] public static Task GetExported (IImported inst) => default; + [Import] public static Task GetImported (IExported inst) => default; + } """)); Execute(); Contains( """ - export const Foo = { - inv: (s, e) => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(s, serialize(e, Space_Enum)), - get fun() { return this.funHandler; }, - set fun(handler) { this.funHandler = handler; this.funSerializedHandler = (s, e) => this.funHandler(s, deserialize(e, Space_Enum)); }, - get funSerialized() { return this.funSerializedHandler; }, - onEvt: new Event(), - onEvtSerialized: (s, e) => Foo.onEvt.broadcast(s, deserialize(e, Space_Enum)), - Enum: { "0": "A", "1": "B", "A": 0, "B": 1 } + class JSExported { + constructor(_id) { this._id = _id; } + inv(inst, info) { return Exported.inv(this._id, inst, info); } + } + + export const Class = { + getExported: async (inst) => instances.export(await exports.Class_GetExported(instances.import(inst)), 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)))); }, + 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) + }; + export const Imported = { + funSerialized: (_id, inst, info) => serialize(instances.imported(_id).fun(instances.imported(inst), deserialize(info, Info)), Info) }; """); } [Fact] - public void GeneratesPropertiesForInteropInterfaces () + public void GeneratesForPropertiesInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] + + namespace Space; public record Info (string Value); - public interface IExportedStatic + public interface IExported { - Info State { get; set; } - IExportedInstanced Exported { get; } - IImportedInstanced Imported { set; } + Info? State { get; set; } int Count { set; } } - public interface IImportedStatic + public interface IImported { - Info State { get; set; } - IImportedInstanced Imported { get; } - IExportedInstanced Exported { set; } + Info? State { get; set; } int Count { set; } } + """)); + Execute(); + Contains( + """ + export const Space = { + Exported: { + get state() { return deserialize(exports.Bootsharp_Generated_Exports_Space_JSExported_GetPropertyState(), Space_Info) ?? undefined; }, + set state(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetPropertyState(serialize(value, Space_Info)); }, + set count(value) { exports.Bootsharp_Generated_Exports_Space_JSExported_SetPropertyCount(value); } + }, + Imported: { + get state() { return this._state; }, + getPropertyStateSerialized() { return serialize(this.state, Space_Info); }, + set state(value) { this._state = value; }, + setPropertyStateSerialized(value) { this.state = deserialize(value, Space_Info); }, + set count(value) { this._count = value; }, + setPropertyCountSerialized(value) { this.count = value; } + } + }; + """); + } + + [Fact] + public void GeneratesForPropertiesInInstancedInterfaces () + { + AddAssembly(With( + """ + public record Info (string Value); - public interface IExportedInstanced + public interface IExported { - Info State { get; set; } - IExportedInstanced Exported { get; } - IImportedInstanced Imported { set; } + Info? State { get; set; } + IExported Exported { get; } + IImported Imported { set; } } - public interface IImportedInstanced + public interface IImported { - Info State { get; set; } - IImportedInstanced Imported { get; } - IExportedInstanced Exported { set; } + Info? State { get; set; } + IImported Imported { get; } + IExported Exported { set; } } - public class Class + public partial class Class { - [JSInvokable] public static IExportedInstanced GetExported (IImportedInstanced inst) => default; - [JSFunction] public static IImportedInstanced GetImported (IExportedInstanced inst) => default; + [Export] public static IExported GetExported (IImported inst) => default; + [Import] public static IImported GetImported (IExported inst) => default; } """)); Execute(); Contains( """ - class JSExportedInstanced { - constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } - get state() { return ExportedInstanced.getPropertyState(this._id); } - set state(value) { ExportedInstanced.setPropertyState(this._id, value); } - get exported() { return ExportedInstanced.getPropertyExported(this._id); } - set imported(value) { ExportedInstanced.setPropertyImported(this._id, value); } + class JSExported { + constructor(_id) { this._id = _id; } + get state() { return Exported.getPropertyState(this._id); } + set state(value) { Exported.setPropertyState(this._id, value); } + get exported() { return Exported.getPropertyExported(this._id); } + set imported(value) { Exported.setPropertyImported(this._id, value); } } export const Class = { - getExported: (inst) => new JSExportedInstanced(exports.Class_GetExported(registerInstance(inst))), + getExported: (inst) => instances.export(exports.Class_GetExported(instances.import(inst)), id => new JSExported(id)), get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => registerInstance(this.getImportedHandler(new JSExportedInstanced(inst))); }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => instances.import(this.getImportedHandler(instances.export(inst, id => new JSExported(id)))); }, get getImportedSerialized() { return this.getImportedSerializedHandler; } }; - export const ExportedInstanced = { - getPropertyState(_id) { return deserialize(exports.Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyState(_id), Info); }, - setPropertyState(_id, value) { exports.Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyState(_id, serialize(value, Info)); }, - getPropertyExported(_id) { return new JSExportedInstanced(exports.Bootsharp_Generated_Exports_JSExportedInstanced_GetPropertyExported(_id)); }, - setPropertyImported(_id, value) { exports.Bootsharp_Generated_Exports_JSExportedInstanced_SetPropertyImported(_id, registerInstance(value)); } - }; - export const ExportedStatic = { - get state() { return deserialize(exports.Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyState(), Info); }, - set state(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyState(serialize(value, Info)); }, - get exported() { return new JSExportedInstanced(exports.Bootsharp_Generated_Exports_JSExportedStatic_GetPropertyExported()); }, - set imported(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyImported(registerInstance(value)); }, - set count(value) { exports.Bootsharp_Generated_Exports_JSExportedStatic_SetPropertyCount(value); } - }; - export const ImportedInstanced = { - getPropertyStateSerialized(_id) { return serialize(getInstance(_id).state, Info); }, - setPropertyStateSerialized(_id, value) { getInstance(_id).state = deserialize(value, Info); }, - getPropertyImportedSerialized(_id) { return registerInstance(getInstance(_id).imported); }, - setPropertyExportedSerialized(_id, value) { getInstance(_id).exported = new JSExportedInstanced(value); } - }; - export const ImportedStatic = { - get state() { return this._state; }, - getPropertyStateSerialized() { return serialize(this.state, Info); }, - set state(value) { this._state = value; }, - setPropertyStateSerialized(value) { this.state = deserialize(value, Info); }, - get imported() { return this._imported; }, - getPropertyImportedSerialized() { return registerInstance(this.imported); }, - set exported(value) { this._exported = value; }, - setPropertyExportedSerialized(value) { this.exported = new JSExportedInstanced(value); }, - set count(value) { this._count = value; }, - setPropertyCountSerialized(value) { this.count = value; } + export const Exported = { + getPropertyState(_id) { return deserialize(exports.Bootsharp_Generated_Exports_JSExported_GetPropertyState(_id), Info) ?? undefined; }, + setPropertyState(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetPropertyState(_id, serialize(value, Info)); }, + getPropertyExported(_id) { return instances.export(exports.Bootsharp_Generated_Exports_JSExported_GetPropertyExported(_id), id => new JSExported(id)); }, + setPropertyImported(_id, value) { exports.Bootsharp_Generated_Exports_JSExported_SetPropertyImported(_id, instances.import(value)); } + }; + export const Imported = { + getPropertyStateSerialized(_id) { return serialize(instances.imported(_id).state, Info); }, + setPropertyStateSerialized(_id, value) { instances.imported(_id).state = deserialize(value, Info); }, + getPropertyImportedSerialized(_id) { return instances.import(instances.imported(_id).imported); }, + setPropertyExportedSerialized(_id, value) { instances.imported(_id).exported = instances.export(value, id => new JSExported(id)); } }; """); } [Fact] - public void GeneratesForInstancedInteropInterfaces () + public void GeneratesForEventsInStaticInterfaces () { AddAssembly(With( """ - public enum Enum { A, B } + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] - public interface IExported { Enum Inv (string str); } - public interface IImported { void NotifyEvt(string str); } + namespace Space; - namespace Space - { - public interface IExported { void Inv (Enum en); } - public interface IImported { Enum Fun (Enum en); } - } + public record Info (string Value); - public class Class + public interface IExported { event Action Evt; } + public interface IImported { event Action Evt; } + """)); + Execute(); + Contains( + """ + export const Space = { + Exported: { + evt: new Event(), + broadcastEvtSerialized: (obj) => Space.Exported.evt.broadcast(deserialize(obj, Space_Info)) + }, + Imported: { + evt: importEvent((obj) => exports.Bootsharp_Generated_Imports_Space_JSImported_InvokeEvt(serialize(obj, Space_Info))) + } + }; + """); + } + + [Fact] + public void GeneratesForEventsInInstancedInterfaces () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported { event Action? Changed; } + public interface IImported { event Action? Changed; } + + public partial class Class { - [JSInvokable] public static Task GetExported (Space.IImported inst) => default; - [JSFunction] public static Task GetImported (IExported inst) => Proxies.Get>>("Class.GetImported")(inst); + [Export] public static IExported GetExported (IImported inst) => default; + [Import] public static IImported GetImported (IExported inst) => default; } """)); Execute(); Contains( """ - class Space_JSExported { - constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } - inv(en) { Space.Exported.inv(this._id, en); } - } - class JSExported { - constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } - inv(str) { return Exported.inv(this._id, str); } + function register_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)); } + }); } """); Contains( """ + class JSExported { + constructor(_id) { this._id = _id; } + changed = new Event(); + broadcastChanged(arg1, arg2) { this.changed.broadcast(arg1, arg2); } + } + export const Class = { - getExported: async (inst) => new Space_JSExported(await exports.Class_GetExported(registerInstance(inst))), + getExported: (inst) => instances.export(exports.Class_GetExported(register_IImported(inst)), id => new JSExported(id)), get getImported() { return this.getImportedHandler; }, - set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = async (inst) => registerInstance(await this.getImportedHandler(new JSExported(inst))); }, + set getImported(handler) { this.getImportedHandler = handler; this.getImportedSerializedHandler = (inst) => register_IImported(this.getImportedHandler(instances.export(inst, id => new JSExported(id)))); }, get getImportedSerialized() { return this.getImportedSerializedHandler; } }; export const Exported = { - inv: (_id, str) => deserialize(exports.Bootsharp_Generated_Exports_JSExported_Inv(_id, str), Enum) - }; - export const Imported = { - onEvtSerialized: (_id, str) => getInstance(_id).onEvt.broadcast(str) - }; - export const Space = { - Exported: { - inv: (_id, en) => exports.Bootsharp_Generated_Exports_Space_JSExported_Inv(_id, serialize(en, Enum)) - }, - Imported: { - funSerialized: (_id, en) => serialize(getInstance(_id).fun(deserialize(en, Enum)), Enum) - } + broadcastChangedSerialized(_id, arg1, arg2) { instances.export(_id, id => new JSExported(id)).broadcastChanged(instances.export(arg1, id => new JSExported(id)), deserialize(arg2, Info)); } }; """); } + [Fact] + public void DoesNotEmitDuplicateInterfaceRegistrations () + { + AddAssembly(With( + """ + public interface IImported + { + event Action? Changed; + event Action? Done; + } + + public class Class + { + [Export] public static void UseImported (IImported instance) {} + } + """)); + Execute(); + Once("function register_IImported"); + } + [Fact] public void IgnoresImplementedInterfaceMethods () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] public interface IExportedStatic { int Foo () => 0; } public interface IImportedStatic { int Foo () => 0; } @@ -811,8 +880,8 @@ public interface IImportedInstanced { int Foo () => 0; } public class Class { - [JSInvokable] public static IExportedInstanced GetExported () => default; - [JSFunction] public static IImportedInstanced GetImported () => default; + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; } """)); Execute(); diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 6b00d8a2..e411aafa 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -5,16 +5,16 @@ public class DeclarationTest : PackTest protected override string TestedContent => GeneratedDeclarations; [Fact] - public void ImportsEventType () + public void ImportsEventTypes () { Execute(); - Contains("""import type { Event } from "./event";"""); + Contains("""import type { EventBroadcaster, EventSubscriber } from "./event";"""); } [Fact] public void DeclaresNamespace () { - AddAssembly(WithClass("Foo", "[JSInvokable] public static void Bar () { }")); + AddAssembly(WithClass("Foo", "[Export] public static void Bar () { }")); Execute(); Contains( """ @@ -27,7 +27,7 @@ export namespace Foo.Class { [Fact] public void DotsInSpaceArePreserved () { - AddAssembly(WithClass("Foo.Bar.Nya", "[JSInvokable] public static void Bar () { }")); + AddAssembly(WithClass("Foo.Bar.Nya", "[Export] public static void Bar () { }")); Execute(); Contains( """ @@ -43,7 +43,7 @@ public void WhenNoNamespaceDeclaresUnderRoot () AddAssembly( With("public record Record;"), With("public enum Enum { A, B }"), - WithClass("[JSInvokable] public static Enum Inv (Record r) => default;")); + WithClass("[Export] public static Enum Inv (Record r) => default;")); Execute(); Contains( """ @@ -65,7 +65,7 @@ public void NestedTypesAreDeclaredUnderClassSpace () { AddAssembly( With("public class Foo { public record Bar; }"), - WithClass("[JSInvokable] public static void Inv (Foo.Bar r) {}")); + WithClass("[Export] public static void Inv (Foo.Bar r) {}")); Execute(); Contains( """ @@ -83,7 +83,7 @@ export namespace Class { [Fact] public void FunctionDeclarationIsExportedForInvokableMethod () { - AddAssembly(WithClass("Foo", "[JSInvokable] public static void Foo () { }")); + AddAssembly(WithClass("Foo", "[Export] public static void Foo () { }")); Execute(); Contains( """ @@ -96,7 +96,7 @@ export namespace Foo.Class { [Fact] public void AssignableVariableIsExportedForFunctionCallback () { - AddAssembly(WithClass("Foo", "[JSFunction] public static void OnFoo () { }")); + AddAssembly(WithClass("Foo", "[Import] public static void OnFoo () { }")); Execute(); Contains( """ @@ -107,19 +107,19 @@ export namespace Foo.Class { } [Fact] - public void EventPropertiesAreExportedForEventMethods () + public void EventPropertiesAreExportedForStaticEvents () { AddAssembly( - WithClass("Foo", "[JSEvent] public static void OnFoo () { }"), - WithClass("Foo", "[JSEvent] public static void OnBar (string baz) { }"), - WithClass("Foo", "[JSEvent] public static void OnFar (int yaz, bool? nya) { }")); + WithClass("Foo", "[Export] public static event Action? ExpEvt;"), + WithClass("Foo", "[Export] public static event Action? Evt;"), + WithClass("Foo", "[Import] public static event Action? ImpEvt;")); Execute(); Contains( """ export namespace Foo.Class { - export const onFoo: Event<[]>; - export const onBar: Event<[baz: string]>; - export const onFar: Event<[yaz: number, nya: boolean | undefined]>; + export const expEvt: EventSubscriber<[]>; + export const evt: EventSubscriber<[obj: string]>; + export const impEvt: EventBroadcaster<[arg1: number, arg2: boolean | undefined]>; } """); } @@ -130,7 +130,7 @@ public void MembersFromSameSpaceAreDeclaredUnderSameSpace () AddAssembly( With("Space", "public class Foo { }"), With("Space", "public class Bar { }"), - WithClass("Space", "[JSInvokable] public static Foo GetFoo (Bar bar) => default;")); + WithClass("Space", "[Export] public static Foo GetFoo (Bar bar) => default;")); Execute(); Contains( """ @@ -153,7 +153,7 @@ public void MembersFromDifferentSpacesAreDeclaredUnderRespectiveSpaces () AddAssembly( With("SpaceA", "public class Foo { }"), With("SpaceB", "public class Bar { }"), - WithClass("[JSInvokable] public static SpaceA.Foo GetFoo (SpaceB.Bar bar) => default;")); + WithClass("[Export] public static SpaceA.Foo GetFoo (SpaceB.Bar bar) => default;")); Execute(); Contains( """ @@ -176,8 +176,8 @@ export namespace Class { public void DifferentSpacesWithSameRootAreDeclaredIndividually () { AddAssembly( - WithClass("Nya.Bar", "[JSInvokable] public static void Fun () { }"), - WithClass("Nya.Foo", "[JSInvokable] public static void Foo () { }")); + WithClass("Nya.Bar", "[Export] public static void Fun () { }"), + WithClass("Nya.Foo", "[Export] public static void Foo () { }")); Execute(); Contains( """ @@ -195,7 +195,7 @@ public void WhenNoNamespaceTypesAreDeclaredUnderRoot () { AddAssembly( With("public class Foo { }"), - WithClass("[JSFunction] public static void OnFoo (Foo foo) { }")); + WithClass("[Import] public static void OnFoo (Foo foo) { }")); Execute(); Contains( """ @@ -214,7 +214,7 @@ public void NumericsTranslatedToNumber () var types = new[] { "byte", "sbyte", "ushort", "uint", "ulong", "short", "int", "decimal", "double", "float" }; var csArgs = string.Join(", ", types.Select(n => $"{n} v{Array.IndexOf(types, n)}")); var tsArgs = string.Join(", ", types.Select(n => $"v{Array.IndexOf(types, n)}: number")); - AddAssembly(WithClass($"[JSInvokable] public static void Num ({csArgs}) {{ }}")); + AddAssembly(WithClass($"[Export] public static void Num ({csArgs}) {{ }}")); Execute(); Contains($"num({tsArgs})"); } @@ -222,7 +222,7 @@ public void NumericsTranslatedToNumber () [Fact] public void Int64TranslatedToBigInt () { - AddAssembly(WithClass("[JSInvokable] public static void Foo (long bar) {}")); + AddAssembly(WithClass("[Export] public static void Foo (long bar) {}")); Execute(); Contains("foo(bar: bigint): void"); } @@ -231,8 +231,8 @@ public void Int64TranslatedToBigInt () public void TaskTranslatedToPromise () { AddAssembly( - WithClass("[JSInvokable] public static Task AsyBool () => default;"), - WithClass("[JSInvokable] public static Task AsyVoid () => default;")); + WithClass("[Export] public static Task AsyBool () => default;"), + WithClass("[Export] public static Task AsyVoid () => default;")); Execute(); Contains("asyBool(): Promise"); Contains("asyVoid(): Promise"); @@ -241,7 +241,7 @@ public void TaskTranslatedToPromise () [Fact] public void CharAndStringTranslatedToString () { - AddAssembly(WithClass("[JSInvokable] public static void Cha (char c, string s) {}")); + AddAssembly(WithClass("[Export] public static void Cha (char c, string s) {}")); Execute(); Contains("cha(c: string, s: string): void"); } @@ -249,7 +249,7 @@ public void CharAndStringTranslatedToString () [Fact] public void BoolTranslatedToBoolean () { - AddAssembly(WithClass("[JSInvokable] public static void Boo (bool b) {}")); + AddAssembly(WithClass("[Export] public static void Boo (bool b) {}")); Execute(); Contains("boo(b: boolean): void"); } @@ -257,7 +257,7 @@ public void BoolTranslatedToBoolean () [Fact] public void DateTimeTranslatedToDate () { - AddAssembly(WithClass("[JSInvokable] public static void Doo (DateTime time) {}")); + AddAssembly(WithClass("[Export] public static void Doo (DateTime time) {}")); Execute(); Contains("doo(time: Date): void"); } @@ -265,7 +265,7 @@ public void DateTimeTranslatedToDate () [Fact] public void ListAndArrayTranslatedToArray () { - AddAssembly(WithClass("[JSInvokable] public static List Goo (DateTime[] d) => default;")); + AddAssembly(WithClass("[Export] public static List Goo (DateTime[] d) => default;")); Execute(); Contains("goo(d: Array): Array"); } @@ -273,7 +273,7 @@ public void ListAndArrayTranslatedToArray () [Fact] public void JaggedArrayAndListOfListsTranslatedToArrayOfArrays () { - AddAssembly(WithClass("[JSInvokable] public static List> Goo (DateTime[][] d) => default;")); + AddAssembly(WithClass("[Export] public static List> Goo (DateTime[][] d) => default;")); Execute(); Contains("goo(d: Array>): Array>"); } @@ -282,15 +282,15 @@ public void JaggedArrayAndListOfListsTranslatedToArrayOfArrays () public void IntArraysTranslatedToRelatedTypes () { AddAssembly( - WithClass("[JSInvokable] public static void Uint8 (byte[] foo) {}"), - WithClass("[JSInvokable] public static void Int8 (sbyte[] foo) {}"), - WithClass("[JSInvokable] public static void Uint16 (ushort[] foo) {}"), - WithClass("[JSInvokable] public static void Int16 (short[] foo) {}"), - WithClass("[JSInvokable] public static void Uint32 (uint[] foo) {}"), - WithClass("[JSInvokable] public static void Int32 (int[] foo) {}"), - WithClass("[JSInvokable] public static void BigInt64 (long[] foo) {}"), - WithClass("[JSInvokable] public static void Float32 (float[] foo) {}"), - WithClass("[JSInvokable] public static void Float64 (double[] foo) {}")); + WithClass("[Export] public static void Uint8 (byte[] foo) {}"), + WithClass("[Export] public static void Int8 (sbyte[] foo) {}"), + WithClass("[Export] public static void Uint16 (ushort[] foo) {}"), + WithClass("[Export] public static void Int16 (short[] foo) {}"), + WithClass("[Export] public static void Uint32 (uint[] foo) {}"), + WithClass("[Export] public static void Int32 (int[] foo) {}"), + WithClass("[Export] public static void BigInt64 (long[] foo) {}"), + WithClass("[Export] public static void Float32 (float[] foo) {}"), + WithClass("[Export] public static void Float64 (double[] foo) {}")); Execute(); Contains("uint8(foo: Uint8Array): void"); Contains("int8(foo: Int8Array): void"); @@ -306,7 +306,7 @@ public void IntArraysTranslatedToRelatedTypes () [Fact] public void OtherTypesAreTranslatedToAny () { - AddAssembly(WithClass("[JSInvokable] public static DBNull Method (IEnumerable t) => default;")); + AddAssembly(WithClass("[Export] public static DBNull Method (IEnumerable t) => default;")); Execute(); Contains("method(t: any): any"); } @@ -316,7 +316,7 @@ public void DefinitionIsGeneratedForObjectType () { AddAssembly( With("n", "public class Foo { public string S { get; set; } public int I { get; set; } }"), - WithClass("n", "[JSInvokable] public static Foo Method (Foo t) => default;")); + WithClass("n", "[Export] public static Foo Method (Foo t) => default;")); Execute(); Contains( """ @@ -340,7 +340,7 @@ public void DefinitionIsGeneratedForInterfaceAndImplementation () With("n", "public interface Interface { Interface Foo { get; } void Bar (Interface b); }"), With("n", "public class Base { }"), With("n", "public class Derived : Base, Interface { public Interface Foo { get; } public void Bar (Interface b) {} }"), - WithClass("n", "[JSInvokable] public static Derived Method (Base b) => default;")); + WithClass("n", "[Export] public static Derived Method (Base b) => default;")); Execute(); Contains( """ @@ -367,7 +367,7 @@ public void DefinitionIsGeneratedForTypeWithListProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public List Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (List items) => default;")); + WithClass("n", "[Export] public static Container Combine (List items) => default;")); Execute(); Contains( """ @@ -391,7 +391,7 @@ public void DefinitionIsGeneratedForTypeWithJaggedArrayProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public Item[][] Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Get () => default;")); + WithClass("n", "[Export] public static Container Get () => default;")); Execute(); Contains( """ @@ -415,7 +415,7 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyListProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public IReadOnlyList Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (IReadOnlyList items) => default;")); + WithClass("n", "[Export] public static Container Combine (IReadOnlyList items) => default;")); Execute(); Contains( """ @@ -439,7 +439,7 @@ public void DefinitionIsGeneratedForTypeWithDictionaryProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public Dictionary Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (Dictionary items) => default;")); + WithClass("n", "[Export] public static Container Combine (Dictionary items) => default;")); Execute(); Contains( """ @@ -463,7 +463,7 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyDictionaryProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public IReadOnlyDictionary Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (IReadOnlyDictionary items) => default;")); + WithClass("n", "[Export] public static Container Combine (IReadOnlyDictionary items) => default;")); Execute(); Contains( """ @@ -487,7 +487,7 @@ public void DefinitionIsGeneratedForTypeWithCollectionProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public ICollection Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (ICollection items) => default;")); + WithClass("n", "[Export] public static Container Combine (ICollection items) => default;")); Execute(); Contains( """ @@ -511,7 +511,7 @@ public void DefinitionIsGeneratedForTypeWithReadOnlyCollectionProperty () AddAssembly( With("n", "public interface Item { }"), With("n", "public class Container { public IReadOnlyCollection Items { get; } }"), - WithClass("n", "[JSInvokable] public static Container Combine (IReadOnlyCollection items) => default;")); + WithClass("n", "[Export] public static Container Combine (IReadOnlyCollection items) => default;")); Execute(); Contains( """ @@ -535,7 +535,7 @@ 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; } }"), - WithClass("n", "[JSInvokable] public static void Method (Generic a, GenericNull b) { }")); + WithClass("n", "[Export] public static void Method (Generic a, GenericNull b) { }")); Execute(); Contains( """ @@ -559,7 +559,7 @@ public void DefinitionIsGeneratedForGenericInterface () { AddAssembly( With("n", "public interface IGenericInterface { public T Value { get; set; } }"), - WithClass("n", "[JSInvokable] public static IGenericInterface Method () => default;")); + WithClass("n", "[Export] public static IGenericInterface Method () => default;")); Execute(); Contains( """ @@ -581,7 +581,7 @@ public void DefinitionIsGeneratedForNestedGenericTypes () AddAssembly( With("Foo", "public class GenericClass { public T Value { get; set; } }"), With("Bar", "public interface GenericInterface { public T Value { get; set; } }"), - WithClass("n", "[JSInvokable] public static void Method (Foo.GenericClass> p) { }")); + WithClass("n", "[Export] public static void Method (Foo.GenericClass> p) { }")); Execute(); Contains( """ @@ -607,7 +607,7 @@ public void DefinitionIsGeneratedForGenericClassWithMultipleTypeArguments () { AddAssembly( With("n", "public class GenericClass { public T1 Key { get; set; } public T2 Value { get; set; } }"), - WithClass("n", "[JSInvokable] public static void Method (GenericClass p) { }")); + WithClass("n", "[Export] public static void Method (GenericClass p) { }")); Execute(); Contains( """ @@ -638,7 +638,7 @@ 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 { [JSInvokable] public static Baz GetBaz () => default; } + public class Class { [Export] public static Baz GetBaz () => default; } """)); Execute(); Contains( @@ -685,7 +685,7 @@ public void StaticPropertiesAreNotIncluded () { AddAssembly( WithClass("public class Foo { public static string Soo { get; } }"), - WithClass("[JSInvokable] public static Foo Bar () => default;")); + WithClass("[Export] public static Foo Bar () => default;")); Execute(); Contains( """ @@ -708,7 +708,7 @@ public bool SetOnly { set { } } public bool this[int index] => true; } - [JSInvokable] public static Foo Bar () => default; + [Export] public static Foo Bar () => default; """)); Execute(); Contains( @@ -722,81 +722,244 @@ export interface Foo { } [Fact] - public void PropertyDeclarationsAreGeneratedForInteropInterfaces () + public void GeneratesForMethodsInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(IExported))] + [assembly:Import(typeof(IImported))] - public record Record (string Value); + public record Info (string Value); + + public interface IExported { Info Inv (string str, Info info); } + public interface IImported { Info Fun (string str, Info info); } + """)); + Execute(); + Contains( + """ + export interface Info { + value: string; + } + + export namespace Exported { + export function inv(str: string, info: Info): Info; + } + export namespace Imported { + export let fun: (str: string, info: Info) => Info; + } + """); + } + + [Fact] + public void GeneratesForMethodsInInstancedInterfaces () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported { Info Inv (string str, Info info); void Reset (); } + 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; + } + """)); + Execute(); + Contains( + """ + export interface IImported { + fun(info: Info, str: string): Info; + } + export interface IExported { + inv(str: string, info: Info): Info; + reset(): void; + } + export interface Info { + value: string; + } + + export namespace Class { + export function getExported(inst: IImported): Promise; + export let getImported: (inst: IExported) => Promise; + } + """); + } + + [Fact] + public void GeneratesForPropertiesInStaticInterfaces () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] + + public record Info (string Value); public interface IExportedStatic { - Record State { get; set; } - Record? Optional { get; } + Info State { get; set; } + Info? Optional { get; } IExportedInstanced Exported { get; } IImportedInstanced Imported { set; } } public interface IImportedStatic { - Record State { get; } + Info State { get; } IImportedInstanced Imported { get; } IExportedInstanced Exported { set; } } - public interface IExportedInstanced + public interface IExportedInstanced {} + public interface IImportedInstanced {} + """)); + Execute(); + Contains( + """ + export interface Info { + value: string; + } + export interface IExportedInstanced { + } + export interface IImportedInstanced { + } + + export namespace ExportedStatic { + export let state: Info; + export const optional: Info | undefined; + export const exported: IExportedInstanced; + export let imported: IImportedInstanced; + } + export namespace ImportedStatic { + export const state: Info; + export const imported: IImportedInstanced; + export let exported: IExportedInstanced; + } + """); + } + + [Fact] + public void GeneratesForPropertiesInInstancedInterfaces () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported { - Record State { get; } - IExportedInstanced Exported { get; } - IImportedInstanced Imported { set; } + Info State { get; set; } + IExported Exported { get; } + IImported Imported { set; } } - public interface IImportedInstanced + public interface IImported { - Record State { get; set; } - IImportedInstanced Imported { get; } - IExportedInstanced Exported { set; } + Info State { get; set; } + IImported Imported { get; } + IExported Exported { set; } } public class Class { - [JSInvokable] public static IExportedInstanced GetExported () => default; - [JSFunction] public static IImportedInstanced GetImported () => default; + [Export] public static IExported GetExported (IImported inst) => default; + [Import] public static IImported GetImported (IExported inst) => default; } """)); Execute(); Contains( """ - export interface IExportedInstanced { - readonly state: Record; - readonly exported: IExportedInstanced; - imported: IImportedInstanced; + export interface IImported { + state: Info; + readonly imported: IImported; + exported: IExported; } - export interface Record { + export interface Info { value: string; } - export interface IImportedInstanced { - state: Record; - readonly imported: IImportedInstanced; - exported: IExportedInstanced; + export interface IExported { + state: Info; + readonly exported: IExported; + imported: IImported; } export namespace Class { - export function getExported(): IExportedInstanced; - export let getImported: () => IImportedInstanced; + export function getExported(inst: IImported): IExported; + export let getImported: (inst: IExported) => IImported; } - export namespace ExportedStatic { - export let state: Record; - export const optional: Record | null; - export const exported: IExportedInstanced; - export let imported: IImportedInstanced; + """); + } + + [Fact] + public void GeneratesForEventsInStaticInterfaces () + { + AddAssembly(With( + """ + [assembly:Export(typeof(IExported))] + [assembly:Import(typeof(IImported))] + + public record Info (string Value); + + public interface IExported { event Action Evt; } + public interface IImported { event Action Evt; } + + public interface IExportedInstanced {} + public interface IImportedInstanced {} + """)); + Execute(); + Contains( + """ + export interface Info { + value: string; } - export namespace ImportedStatic { - export const state: Record; - export const imported: IImportedInstanced; - export let exported: IExportedInstanced; + export interface IExportedInstanced { + } + export interface IImportedInstanced { + } + + export namespace Exported { + export const evt: EventSubscriber<[arg1: string, arg2: Info, arg3: IExportedInstanced]>; + } + export namespace Imported { + export const evt: EventBroadcaster<[arg1: string, arg2: Info, arg3: IImportedInstanced]>; + } + """); + } + + [Fact] + public void GeneratesForEventsInInstancedInterfaces () + { + AddAssembly(With( + """ + public record Info (string Value); + + public interface IExported { event Action? Changed; event Action? Done; } + 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; + } + """)); + Execute(); + Contains( + """ + export interface IImported { + changed: EventBroadcaster<[arg1: IImported, arg2: Info, arg3: string]>; + } + export interface IExported { + changed: EventSubscriber<[obj: Info]>; + done: EventSubscriber<[]>; + } + export interface Info { + value: string; + } + + export namespace Class { + export function getExported(inst: IImported): IExported; + export let getImported: (inst: IExported) => IImported; } """); } @@ -805,8 +968,8 @@ export namespace ImportedStatic { public void NullableMethodArgumentsUnionWithUndefined () { AddAssembly( - WithClass("[JSInvokable] public static void Foo (string? bar) { }"), - WithClass("[JSFunction] public static void Fun (int? nya) { }")); + WithClass("[Export] public static void Foo (string? bar) { }"), + WithClass("[Import] public static void Fun (int? nya) { }")); Execute(); Contains("export function foo(bar: string | undefined): void;"); Contains("export let fun: (nya: number | undefined) => void;"); @@ -816,11 +979,11 @@ public void NullableMethodArgumentsUnionWithUndefined () public void NullableMethodReturnTypesUnionWithNull () { AddAssembly( - WithClass("[JSInvokable] public static string? Foo () => default;"), - WithClass("[JSInvokable] public static Task? Bar () => default;"), - WithClass("[JSInvokable] public static Task Baz () => default;"), - WithClass("[JSInvokable] public static Task? Quz () => default;"), - WithClass("[JSFunction] public static ValueTask?> Nya () => default;")); + WithClass("[Export] public static string? Foo () => default;"), + WithClass("[Export] public static Task? Bar () => default;"), + WithClass("[Export] public static Task Baz () => default;"), + WithClass("[Export] public static Task? Quz () => default;"), + WithClass("[Import] public static ValueTask?> Nya () => default;")); Execute(); Contains("export function foo(): string | null;"); Contains("export function bar(): Promise | null;"); @@ -834,7 +997,7 @@ public void NullableCollectionElementTypesUnionWithNull () { AddAssembly( With("public class Foo { }"), - WithClass("[JSFunction] public static List? Fun (int?[]? bar, Foo[]?[]? nya, Foo?[]?[]? far) => default;")); + WithClass("[Import] public static List? Fun (int?[]? bar, Foo[]?[]? nya, Foo?[]?[]? far) => default;")); Execute(); Contains( """ @@ -853,7 +1016,7 @@ public void NullableCollectionElementTypesOfCustomTypeUnionWithNull () AddAssembly( With("public interface IFoo { }"), With("public record Foo (List?>?>? Bar, IFoo?[]?[]? Nya) : IFoo;"), - WithClass("[JSFunction] public static IFoo Fun (Foo foo) => default;")); + WithClass("[Import] public static IFoo Fun (Foo foo) => default;")); Execute(); Contains("bar?: Array | null> | null>;"); Contains("nya?: Array | null> | null>;"); @@ -863,7 +1026,7 @@ public void NullableCollectionElementTypesOfCustomTypeUnionWithNull () public void NullableDictionaryValueTypesUnionWithNull () { AddAssembly( - WithClass("[JSFunction] public static Dictionary? Fun (Dictionary? bar) => default;")); + WithClass("[Import] public static Dictionary? Fun (Dictionary? bar) => default;")); Execute(); Contains("export let fun: (bar: Map | undefined) => Map | null;"); } @@ -874,7 +1037,7 @@ public void NullablePropertiesHaveOptionalModificator () AddAssembly( With("n", "public class Foo { public bool? Bool { get; } }"), With("n", "public class Bar { public Foo? Foo { get; } }"), - WithClass("n", "[JSInvokable] public static Foo FooBar (Bar bar) => default;")); + WithClass("n", "[Export] public static Foo FooBar (Bar bar) => default;")); Execute(); Contains( """ @@ -899,7 +1062,7 @@ public void NullableEnumsAreCrawled () AddAssembly( With("n", "public enum Foo { A, B }"), With("n", "public class Bar { public Foo? Foo { get; } }"), - WithClass("n", "[JSInvokable] public static Bar GetBar () => default;")); + WithClass("n", "[Export] public static Bar GetBar () => default;")); Execute(); Contains( """ @@ -926,9 +1089,9 @@ public void WhenTypeReferencedMultipleTimesItsDeclaredOnlyOnce () With("public interface Foo { }"), With("public class Bar : Foo { public Foo Foo { get; } }"), With("public class Far : Bar { public Bar Bar { get; } }"), - WithClass("[JSInvokable] public static Bar TakeFooGiveBar (Foo f) => default;"), - WithClass("[JSInvokable] public static Foo TakeBarGiveFoo (Bar b) => default;"), - WithClass("[JSInvokable] public static Far TakeAllGiveFar (Foo f, Bar b, Far ff) => default;")); + WithClass("[Export] public static Bar TakeFooGiveBar (Foo f) => default;"), + WithClass("[Export] public static Foo TakeBarGiveFoo (Bar b) => default;"), + WithClass("[Export] public static Far TakeAllGiveFar (Foo f, Bar b, Far ff) => default;")); Execute(); Once("export interface Foo"); Once("export interface Bar"); @@ -936,17 +1099,17 @@ public void WhenTypeReferencedMultipleTimesItsDeclaredOnlyOnce () } [Fact] - public void RespectsSpacePreference () + public void RespectsSpacePrefInStaticMembers () { AddAssembly( With( """ - [assembly: Bootsharp.JSPreferences( + [assembly: Bootsharp.Preferences( Space = [@"^Foo\.Bar\.(\S+)", "$1"] )] """), With("Foo.Bar.Nya", "public class Nya { }"), - WithClass("Foo.Bar.Fun", "[JSFunction] public static void OnFun (Nya.Nya nya) { }")); + WithClass("Foo.Bar.Fun", "[Import] public static void OnFun (Nya.Nya nya) { }")); Execute(); Contains( """ @@ -962,101 +1125,20 @@ export namespace Fun.Class { } [Fact] - public void RespectsTypePreference () - { - AddAssembly(With( - """ - [assembly: Bootsharp.JSPreferences( - Type = [@"Record", "Foo", @".+`.+", "Bar"] - )] - - public record Record; - public record Generic; - - public class Class - { - [JSInvokable] public static void Inv (Record r, Generic g) {} - } - """)); - Execute(); - Contains( - """ - export interface Record { - } - export interface Generic { - } - - export namespace Class { - export function inv(r: Foo, g: Bar): void; - } - """); - } - - [Fact] - public void IgnoresBindingsInGeneratedNamespace () - { - AddAssembly(With("Bootsharp.Generated", - """ - public record Record; - public static class Exports { [JSInvokable] public static void Inv (Record r) {} } - public static class Imports { [JSFunction] public static void Fun () {} } - """)); - Execute(); - DoesNotContain("Record"); - DoesNotContain("export function inv"); - DoesNotContain("export let fun"); - } - - [Fact] - public void GeneratesForExportImportInterfaces () - { - AddAssembly(With( - """ - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] - - namespace Space; - - public enum Enum { A, B } - - public interface IExported { void Inv (string s, Enum e); } - public interface IImported { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } - """)); - Execute(); - Contains( - """ - export namespace Space { - export enum Enum { - A, - B - } - } - - export namespace Space.Exported { - export function inv(s: string, e: Space.Enum): void; - } - export namespace Space.Imported { - export let fun: (s: string, e: Space.Enum) => void; - export const onEvt: Event<[s: string, e: Space.Enum]>; - } - """); - } - - [Fact] - public void GeneratesForExportImportInterfacesWithSpacePref () + public void RespectsSpacePrefInStaticInterfaces () { AddAssembly(With( """ - [assembly:JSPreferences(Space = [@".+", "Foo"])] - [assembly:JSExport(typeof(Space.IExported))] - [assembly:JSImport(typeof(Space.IImported))] + [assembly:Preferences(Space = [@".+", "Foo"])] + [assembly:Export(typeof(Space.IExported))] + [assembly:Import(typeof(Space.IImported))] namespace Space; public enum Enum { A, B } public interface IExported { void Inv (string s, Enum e); } - public interface IImported { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } + public interface IImported { void Fun (string s, Enum e); } """)); Execute(); Contains( @@ -1071,103 +1153,54 @@ export enum Enum { export namespace Foo { export function inv(s: string, e: Foo.Enum): void; export let fun: (s: string, e: Foo.Enum) => void; - export const onEvt: Event<[s: string, e: Foo.Enum]>; } """); } [Fact] - public void GeneratesInstancedInterfacesFromStaticMethods () + public void RespectsTypePreference () { AddAssembly(With( """ - public enum Enum { A, B } - public interface IExportedInstancedA { void Inv (string? s, Enum e); } - public interface IExportedInstancedB { Enum? Inv (); } - public interface IImportedInstancedA { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } - public interface IImportedInstancedB { Enum Fun (); void NotifyEvt (); } + [assembly: Bootsharp.Preferences( + Type = [@"Record", "Foo", @".+`.+", "Bar"] + )] + + public record Record; + public record Generic; public class Class { - [JSInvokable] public static IExportedInstancedA CreateExported (string arg, IImportedInstancedB i) => default; - [JSFunction] public static IImportedInstancedA CreateImported (string arg, IExportedInstancedB i) => default; + [Export] public static void Inv (Record r, Generic g) {} } """)); Execute(); Contains( """ - export interface IImportedInstancedB { - fun(): Enum; - onEvt: Event<[]>; - } - export interface IExportedInstancedA { - inv(s: string | undefined, e: Enum): void; - } - export enum Enum { - A, - B - } - export interface IExportedInstancedB { - inv(): Enum | null; + export interface Record { } - export interface IImportedInstancedA { - fun(s: string, e: Enum): void; - onEvt: Event<[s: string, e: Enum]>; + export interface Generic { } export namespace Class { - export function createExported(arg: string, i: IImportedInstancedB): IExportedInstancedA; - export let createImported: (arg: string, i: IExportedInstancedB) => IImportedInstancedA; + export function inv(r: Foo, g: Bar): void; } """); } [Fact] - public void GeneratesInstancedInterfacesFromStaticInterfaces () + public void IgnoresBindingsInGeneratedNamespace () { - AddAssembly(With( + AddAssembly(With("Bootsharp.Generated", """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] - - public interface IExportedStatic { IExportedInstancedA CreateExported (string arg, IImportedInstancedB i); } - public interface IImportedStatic { IImportedInstancedA CreateImported (string arg, IExportedInstancedB i); } - - public enum Enum { A, B } - public interface IExportedInstancedA { void Inv (string s, Enum e); } - public interface IExportedInstancedB { Enum Inv (); } - public interface IImportedInstancedA { void Fun (string s, Enum e); void NotifyEvt (string s, Enum e); } - public interface IImportedInstancedB { Enum Fun (); void NotifyEvt (); } + public record Record; + public static class Exports { [Export] public static void Inv (Record r) {} } + public static class Imports { [Import] public static void Fun () {} } """)); Execute(); - Contains( - """ - export interface IImportedInstancedB { - fun(): Enum; - onEvt: Event<[]>; - } - export interface IExportedInstancedA { - inv(s: string, e: Enum): void; - } - export enum Enum { - A, - B - } - export interface IExportedInstancedB { - inv(): Enum; - } - export interface IImportedInstancedA { - fun(s: string, e: Enum): void; - onEvt: Event<[s: string, e: Enum]>; - } - - export namespace ExportedStatic { - export function createExported(arg: string, i: IImportedInstancedB): IExportedInstancedA; - } - export namespace ImportedStatic { - export let createImported: (arg: string, i: IExportedInstancedB) => IImportedInstancedA; - } - """); + DoesNotContain("Record"); + DoesNotContain("export function inv"); + DoesNotContain("export let fun"); } [Fact] @@ -1175,8 +1208,8 @@ public void IgnoresImplementedInterfaceMethods () { AddAssembly(With( """ - [assembly:JSExport(typeof(IExportedStatic))] - [assembly:JSImport(typeof(IImportedStatic))] + [assembly:Export(typeof(IExportedStatic))] + [assembly:Import(typeof(IImportedStatic))] public interface IExportedStatic { int Foo () => 0; } public interface IImportedStatic { int Foo () => 0; } @@ -1185,8 +1218,8 @@ public interface IImportedInstanced { int Foo () => 0; } public class Class { - [JSInvokable] public static IExportedInstanced GetExported () => default; - [JSFunction] public static IImportedInstanced GetImported () => default; + [Export] public static IExportedInstanced GetExported () => default; + [Import] public static IImportedInstanced GetImported () => default; } """)); Execute(); @@ -1198,7 +1231,9 @@ public void GeneratesJsDocsOverCsDocs () { AddAssembly(With( """ - /// Payload kind. + /// + /// Payload kind. + /// public enum Kind { /// First kind. @@ -1207,7 +1242,9 @@ public enum Kind Second } - /// A payload sent across interop. + /// + /// A payload sent across interop. + /// /// Visible in generated TypeScript. public record Payload { @@ -1215,7 +1252,19 @@ public record Payload public string Name { get; init; } } - /// Exported instance API. + /// + /// Event handler payload. + /// + public class HandlerArgs : EventArgs; + + /// Payload changed callback. + /// Payload from custom delegate. + /// Label from custom delegate. + public delegate void PayloadChanged (Payload payload, string label); + + /// + /// Exported instance API. + /// public interface IExportedInstanced { /// Current state. @@ -1226,31 +1275,40 @@ public interface IExportedInstanced void Inv (string value); } - /// Static interop API. - public class Class + /// + /// Static interop API. + /// + public partial class Class { + /// Exports completion signal. + [Export] public static event Action? ExpEvt; + /// Imports completion signal. + [Import] public static event Action? ImpEvt; + /// Exports payload changes. + [Export] public static event PayloadChanged? PayloadChanged; + /// Imports handler signal. + /// Sender from event handler. + /// Payload from event handler. + [Import] public static event EventHandler? HandlerEvt; + /// Runs foo. /// Function value. /// Names to run. /// Computed value. - [JSInvokable] public static int Foo (List function, string[] names) => 0; + [Export] public static int Foo (List function, string[] names) => 0; /// Gets payload. - [JSInvokable] public static Payload Get (Kind kind) => default; + [Export] public static Payload Get (Kind kind) => default; /// Gets exported instance. - [JSInvokable] public static IExportedInstanced GetExported () => default; + [Export] public static IExportedInstanced GetExported () => default; /// Receives foo. /// Count to receive. - [JSFunction] public static void OnFoo (int count) { } + [Import] public static void OnFoo (int count) { } /// Value without summary. - [JSFunction] public static void OnParamOnly (string value) { } - - /// Signals completion. - /// Whether work is done. - [JSEvent] public static void OnDone (bool done) { } + [Import] public static void OnParamOnly (string value) { } } """)); Execute(); @@ -1305,6 +1363,26 @@ export interface IExportedInstanced { * Static interop API. */ export namespace Class { + /** + * Exports completion signal. + */ + export const expEvt: EventSubscriber<[obj: boolean]>; + /** + * Imports completion signal. + */ + export const impEvt: EventBroadcaster<[arg1: string, arg2: number]>; + /** + * Exports payload changes. + * @param payload Payload from custom delegate. + * @param label Label from custom delegate. + */ + export const payloadChanged: EventSubscriber<[payload: Payload, label: string]>; + /** + * Imports handler signal. + * @param sender Sender from event handler. + * @param e Payload from event handler. + */ + export const handlerEvt: EventBroadcaster<[sender: any | undefined, e: HandlerArgs]>; /** * Runs foo. * @param fn Function value. @@ -1329,11 +1407,6 @@ export namespace Class { * @param value Value without summary. */ export let onParamOnly: (value: string) => void; - /** - * Signals completion. - * @param done Whether work is done. - */ - export const onDone: Event<[done: boolean]>; } """); } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs index 94b50bb1..8e950063 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/SolutionInspectionTest.cs @@ -6,7 +6,7 @@ public class SolutionInspectionTest : PackTest public void AllAssembliesAreInspected () { AddAssembly("foo.dll", - WithClass("[JSInvokable] public static void Inv () {}") + WithClass("[Export] public static void Inv () {}") ); Execute(); Assert.Contains(Engine.Messages, w => w.Contains("foo")); @@ -16,10 +16,10 @@ public void AllAssembliesAreInspected () public void WhenAssemblyInspectionFailsWarningIsLogged () { AddAssembly("foo.dll", - WithClass("[JSInvokable] public static void InvFoo () {}") + WithClass("[Export] public static void InvFoo () {}") ); AddAssembly("bar.dll", - WithClass("[JSInvokable] public static void InvBar () {}") + WithClass("[Export] public static void InvBar () {}") ); File.WriteAllText(Path.Combine(Project.Root, "foo.dll"), "corrupted"); Execute(); @@ -38,10 +38,10 @@ public void IgnoresAssembliesNotPresentInBuildDirectory () File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file)); AddAssembly("foo.dll", - WithClass("[JSInvokable] public static void InvFoo () {}") + WithClass("[Export] public static void InvFoo () {}") ); AddAssembly("bar.dll", - WithClass("[JSInvokable] public static void InvBar () {}") + WithClass("[Export] public static void InvBar () {}") ); Execute(); @@ -63,10 +63,10 @@ public void DoesntIgnoreAssembliesWhenLLVM () File.WriteAllText($"{buildDir}/{Path.GetFileName(file)}", File.ReadAllText(file)); AddAssembly("foo.dll", - WithClass("[JSInvokable] public static void InvFoo () {}") + WithClass("[Export] public static void InvFoo () {}") ); AddAssembly("bar.dll", - WithClass("[JSInvokable] public static void InvBar () {}") + WithClass("[Export] public static void InvBar () {}") ); Execute(); diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 74d3e937..18f1dd09 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -1,4 +1,5 @@ global using static Bootsharp.Publish.GlobalInspection; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; using System.Text.RegularExpressions; @@ -29,6 +30,21 @@ public static bool IsUserType (Type type) return IsUserAssembly(type.Assembly.FullName!); } + public static bool IsAutoProperty (PropertyInfo prop) + { + var backingFieldName = $"<{prop.Name}>k__BackingField"; + var backingField = prop.DeclaringType!.GetField(backingFieldName, + BindingFlags.NonPublic | BindingFlags.Instance); + 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/GlobalText.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs index bd67cbc7..d9abe567 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs @@ -4,12 +4,14 @@ namespace Bootsharp.Publish; internal static class GlobalText { - public static string JoinLines (params string?[] values) => JoinLines(values, 1); - public static string JoinLines (int indent, params string?[] values) => JoinLines(values, indent); - public static string JoinLines (IEnumerable values, int indent = 1, string separator = "\n") + public static string Fmt (params string?[] txt) => Fmt(txt, 1); + public static string Fmt (int indent, params string?[] txt) => Fmt(txt, indent); + public static string Fmt (IEnumerable txt, int indent = 1, string separator = "\n") { var pad = new string(' ', indent * 4); - var padded = values.Where(v => v != null).Select(v => v!.Replace("\n", "\n" + pad)); + var padded = txt.Where(v => v != null).Select(v => + string.Join("\n", v!.Split('\n').Select((line, i) => + i == 0 ? line : string.IsNullOrWhiteSpace(line) ? "" : pad + line))); return string.Join(separator + pad, padded); } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index cf87c7ba..b526d2ae 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -56,15 +56,9 @@ static bool IsDictionary (Type type) => type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } - public static NullabilityInfo GetNullability (PropertyInfo prop) - { - return new NullabilityInfoContext().Create(prop); - } - - public static NullabilityInfo GetNullability (ParameterInfo param) - { - return new NullabilityInfoContext().Create(param); - } + 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 bool IsNullable (Type type) => IsNullable(type, out _); public static bool IsNullable (Type type, NullabilityInfo? info) => IsNullable(type, info, out _); @@ -78,21 +72,6 @@ public static bool IsNullable (Type type, NullabilityInfo? info, [NotNullWhen(tr return value != null; } - public static bool IsAutoProperty (PropertyInfo prop) - { - var backingFieldName = $"<{prop.Name}>k__BackingField"; - var backingField = prop.DeclaringType!.GetField(backingFieldName, - BindingFlags.NonPublic | BindingFlags.Instance); - return backingField != null; - } - - public static bool IsInstancedInteropInterface (Type type, [NotNullWhen(true)] out Type? instanceType) - { - if (IsTaskWithResult(type, out instanceType)) - return IsInstancedInteropInterface(instanceType, out instanceType); - return (instanceType = type.IsInterface && IsUserType(type) ? type : null) != null; - } - public static string BuildJSSpace (Type type, Preferences prefs) { var space = type.Namespace ?? ""; @@ -104,43 +83,12 @@ public static string BuildJSSpace (Type type, Preferences prefs) return WithPrefs(prefs.Space, space, space); } - public static string BuildJSSpaceName (Type type) - { - return type.IsGenericType ? TrimGenericArgs(type.Name) : type.Name; - } - - public static string BuildJSSpaceFullName (Type type, Preferences prefs) - { - var space = BuildJSSpace(type, prefs); - var name = BuildJSSpaceName(type); - return string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; - } - - public static (string space, string name, string full) BuildInterfaceImplName - (Type instanceType, InteropKind interop) - { - var space = "Bootsharp.Generated." + (interop == InteropKind.Export ? "Exports" : "Imports"); - if (instanceType.Namespace != null) space += $".{instanceType.Namespace}"; - var name = "JS" + instanceType.Name[1..]; - return (space, name, $"{space}.{name}"); - } - - public static string PrependInstanceIdArgName (string args) + public static string PrependIdArg (string args) { if (string.IsNullOrEmpty(args)) return "_id"; return $"_id, {args}"; } - public static string PrependInstanceIdArgTypeAndName (string args) - { - return $"{BuildSyntax(typeof(int))} {PrependInstanceIdArgName(args)}"; - } - - public static string BuildJSInteropInstanceClassName (InterfaceMeta inter) - { - return inter.FullName.Replace("Bootsharp.Generated.Exports.", "").Replace(".", "_"); - } - public static string BuildSerializedId (Type type) { var builder = new StringBuilder(); @@ -165,7 +113,7 @@ public static string BuildSyntax (Type type, NullabilityInfo? nul = null, bool f string BuildGeneric (Type type, Type[] args) { if (IsNullable(type, out var value)) return BuildSyntax(value, nul, true); - var name = TrimGenericArgs(ResolveTypeName(type)); + var name = TrimGeneric(ResolveTypeName(type)); var typeArgs = string.Join(", ", args.Select((a, i) => BuildSyntax(a, nul?.GenericTypeArguments[i]))); return $"global::{name}<{typeArgs}>"; } @@ -177,9 +125,10 @@ static string ResolveTypeName (Type type) } } - public static string TrimGenericArgs (string typeName) + public static string TrimGeneric (string typeName) { var delimiterIndex = typeName.IndexOf('`'); + if (delimiterIndex < 0) return typeName; return typeName[..delimiterIndex]; } } diff --git a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs index 250e0c3c..e64f9232 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/InterfaceMeta.cs @@ -1,8 +1,8 @@ namespace Bootsharp.Publish; /// -/// Interface supplied by user under either -/// or representing static interop API, or in +/// Interface supplied by user under either +/// or representing static interop API, or in /// an interop method, representing instanced interop API. /// internal sealed record InterfaceMeta @@ -21,20 +21,23 @@ internal sealed record InterfaceMeta /// public required string TypeSyntax { get; init; } /// - /// Namespace of the generated interop class implementation. + /// C# namespace of the generated interop class implementation. /// public required string Namespace { get; init; } /// - /// Name of the generated interop class implementation. + /// C# name of the generated interop class implementation. /// public required string Name { get; init; } /// - /// Full type name of the generated interop class implementation. + /// Full C# type name of the generated interop class implementation. /// - public string FullName => $"{Namespace}.{Name}"; + 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/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs index 3596f1b7..64e53b1e 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/MemberMeta.cs @@ -1,4 +1,3 @@ -using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Bootsharp.Publish; @@ -71,19 +70,18 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta } /// -/// An interop event declared on a static API surface or interop interface (temporarily shares the method path). +/// An interop event declared on a static API surface or interop interface. /// -internal sealed record EventMeta : MethodMeta +internal sealed record EventMeta (EventInfo Info) : MemberMeta { /// - /// C# interface method name. It may differ from because event methods - /// on interfaces have special names, which are then renamed on the JS side. This will be removed once - /// we add proper support for events. + /// The reflected info of the event. /// - public string MethodName { get; } - - [SetsRequiredMembers] // TODO: Remove after implementing proper support for events. - public EventMeta (MethodMeta method, string methodName) : base(method) => MethodName = methodName; + public override EventInfo Info { get; } = Info; + /// + /// Arguments carried by the event delegate. + /// + public required IReadOnlyList Arguments { get; init; } } /// @@ -106,7 +104,7 @@ internal sealed record PropertyMeta (PropertyInfo Info) : MemberMeta } /// -/// Interop method argument. +/// An interop method or event delegate argument. /// internal sealed record ArgumentMeta (ParameterInfo Info) { diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs index 1ec047fb..c4374cd4 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/Preferences.cs @@ -1,14 +1,12 @@ namespace Bootsharp.Publish; -/// +/// internal sealed record Preferences { - /// + /// public IReadOnlyList Space { get; init; } = []; - /// + /// public IReadOnlyList Type { get; init; } = []; - /// - public IReadOnlyList Event { get; init; } = []; - /// + /// public IReadOnlyList Function { get; init; } = []; } diff --git a/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs b/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs index 2dd1880b..17084891 100644 --- a/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs +++ b/src/cs/Bootsharp.Publish/Common/Preferences/PreferencesResolver.cs @@ -16,16 +16,15 @@ public Preferences Resolve (string outDir) private CustomAttributeData? FindPreferencesAttribute (Assembly assembly) { foreach (var attr in assembly.CustomAttributes) - if (attr.AttributeType.FullName == typeof(JSPreferencesAttribute).FullName) + if (attr.AttributeType.FullName == typeof(PreferencesAttribute).FullName) return attr; return null; } private Preferences CreatePreferences (CustomAttributeData? attr) => new() { - Space = CreatePreferences(nameof(JSPreferencesAttribute.Space), attr) ?? [], - Type = CreatePreferences(nameof(JSPreferencesAttribute.Type), attr) ?? [], - Event = CreatePreferences(nameof(JSPreferencesAttribute.Event), attr) ?? [new(@"^Notify(\S+)", "On$1")], - Function = CreatePreferences(nameof(JSPreferencesAttribute.Function), attr) ?? [] + Space = CreatePreferences(nameof(PreferencesAttribute.Space), attr) ?? [], + Type = CreatePreferences(nameof(PreferencesAttribute.Type), attr) ?? [], + Function = CreatePreferences(nameof(PreferencesAttribute.Function), attr) ?? [] }; private Preference[]? CreatePreferences (string name, CustomAttributeData? attr) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs index 98b9226e..f78cc280 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InspectionReporter.cs @@ -8,17 +8,17 @@ internal sealed class InspectionReporter (TaskLoggingHelper logger) public void Report (SolutionInspection inspection) { logger.LogMessage(MessageImportance.Normal, "Bootsharp assembly inspection result:"); - logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered assemblies:", - JoinLines(GetDiscoveredAssemblies(inspection)))); - logger.LogMessage(MessageImportance.Normal, JoinLines("Discovered interop members:", - JoinLines(GetDiscoveredMembers(inspection)))); + logger.LogMessage(MessageImportance.Normal, Fmt("Discovered assemblies:", + Fmt(GetDiscoveredAssemblies(inspection)))); + logger.LogMessage(MessageImportance.Normal, Fmt("Discovered interop members:", + Fmt(GetDiscoveredMembers(inspection)))); foreach (var warning in inspection.Warnings) logger.LogWarning(warning); } private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) { - return inspection.StaticMethods.Select(m => m.Assembly) + 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))) .ToHashSet(); @@ -26,7 +26,7 @@ private HashSet GetDiscoveredAssemblies (SolutionInspection inspection) private HashSet GetDiscoveredMembers (SolutionInspection inspection) { - return inspection.StaticMethods + return inspection.StaticMembers .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Members)) .Select(m => m.ToString()) diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs index 4927e73a..517a0672 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/InterfaceInspector.cs @@ -2,58 +2,75 @@ namespace Bootsharp.Publish; -internal sealed class InterfaceInspector (Preferences prefs, MemberInspector members, string entryAssemblyName) +internal sealed class InterfaceInspector (MemberInspector members, string entryAssemblyName) { private InteropKind interop; - private (string space, string name, string full) impl; + private string memberSpace = null!; public InterfaceMeta Inspect (Type interfaceType, InteropKind interopKind) { - interop = interopKind; - impl = BuildInterfaceImplName(interfaceType, interop); + var space = BuildInterfaceSpace(interfaceType, interopKind); + var name = BuildInterfaceName(interfaceType); return new InterfaceMeta { - Interop = interop, + Interop = interop = interopKind, Type = interfaceType, TypeSyntax = BuildSyntax(interfaceType), - Namespace = impl.space, - Name = impl.name, - Members = interfaceType.GetProperties().Where(ShouldInspectProperty).Select(CreateProperty) - .Concat(interfaceType.GetMethods().Where(ShouldInspectMethod).Select(CreateMethod)).ToArray() + 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 bool ShouldInspectProperty (PropertyInfo prop) + 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) { - if (prop.GetIndexParameters().Length != 0) return false; - return prop.GetMethod?.IsAbstract == true || prop.SetMethod?.IsAbstract == true; + var space = "Bootsharp.Generated." + (interop == InteropKind.Export ? "Exports" : "Imports"); + if (type.Namespace != null) space += $".{type.Namespace}"; + return space; } - private MemberMeta CreateMethod (MethodInfo info) + private static string BuildInterfaceName (Type type) { - var name = WithPrefs(prefs.Event, info.Name, info.Name); - var method = members.Inspect(info, interop) with { - Assembly = entryAssemblyName, - Space = impl.full, - Name = name, - JSName = ToFirstLower(name) - }; - if (interop == InteropKind.Import && name != info.Name) - return new EventMeta(method, info.Name); - return method; + return "JS" + type.Name[1..]; } - private MemberMeta CreateProperty (PropertyInfo info) + private static string BuildInterfaceJSName (Type type) { - return members.Inspect(info, interop) with { - Assembly = entryAssemblyName, - Space = impl.full, - CanGet = info.GetMethod?.IsAbstract == true, - CanSet = info.SetMethod?.IsAbstract == true - }; + var name = BuildInterfaceName(type); + if (type.Namespace == null) return name; + return $"{type.Namespace}.{name}".Replace(".", "_"); } } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs index 2776a3a0..4d886799 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MemberInspector.cs @@ -4,6 +4,33 @@ 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!, @@ -21,7 +48,7 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, S Assembly = method.DeclaringType!.Assembly.GetName().Name!, Space = method.DeclaringType.FullName!, Name = method.Name, - Arguments = method.GetParameters().Select(CreateArgument).ToArray(), + 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)), @@ -29,15 +56,15 @@ internal sealed class MemberInspector (Preferences prefs, TypeInspector types, S Async = IsTaskLike(method.ReturnParameter.ParameterType) }; - private ArgumentMeta CreateArgument (ParameterInfo param) => new(param) { + private ArgumentMeta CreateArg (ParameterInfo param, NullabilityInfo nil) => new(param) { Name = param.Name!, JSName = param.Name == "function" ? "fn" : param.Name!, - Value = CreateValue(param.ParameterType, GetNullability(param)) + Value = CreateValue(param.ParameterType, nil) }; private ValueMeta CreateValue (Type type, NullabilityInfo nil) { - IsInstancedInteropInterface(type, out var instanceType); + IsInstancedInterface(type, out var instanceType); return new() { Type = types.Inspect(type), TypeSyntax = BuildSyntax(type, nil), @@ -48,11 +75,11 @@ private ValueMeta CreateValue (Type type, NullabilityInfo nil) }; } - private string BuildJSSpace (Type type) + private string BuildJSSpace (Type decl) { - var space = type.Namespace ?? ""; - var name = BuildJSSpaceName(type); - if (type.IsInterface) name = name[1..]; + 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/SolutionInspector/SerializedInspector.cs index cd5e883e..175637c0 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SerializedInspector.cs @@ -40,7 +40,7 @@ private static bool ShouldSerialize (Type type) if (IsVoid(type)) return false; if (IsNullable(type, out var value)) return ShouldSerialize(value); if (IsTaskWithResult(type, out var result)) return ShouldSerialize(result); - if (IsInstancedInteropInterface(type, out _)) return false; + if (IsInstancedInterface(type, out _)) return false; return !native.Contains(type.FullName!); } @@ -100,7 +100,7 @@ private SerializedPropertyMeta BuildProperty (PropertyInfo prop, bool ctor) ConstructorParameter = ctor, Kind = canInit ? SerializedPropertyKind.Init : canSet ? SerializedPropertyKind.Set : canSetField ? SerializedPropertyKind.Field : SerializedPropertyKind.None, - FieldAccessorName = canSetField ? $"Set_{BuildSerializedId(prop.DeclaringType!)}_{prop.Name}" : null + FieldAccessorName = canSetField ? $"Access_{BuildSerializedId(prop.DeclaringType!)}_{prop.Name}" : null }; } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index 4159aa11..bd6e1af5 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -14,22 +14,22 @@ namespace Bootsharp.Publish; internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable { /// - /// Interop interfaces specified under or - /// for which static bindings have to be emitted. + /// 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. + /// and can be sources of the instanced interfaces. /// public required IReadOnlyCollection InstancedInterfaces { get; init; } /// - /// Static interop methods, ie methods with - /// and similar interop attributes found on user-defined static classes. + /// Static interop members, ie methods or events with + /// or found on user-defined static classes. /// - public required IReadOnlyCollection StaticMethods { get; init; } + public required IReadOnlyCollection StaticMembers { get; init; } /// /// All the types that cross the interop boundary or referenced by them. /// diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs index 671a912f..2f3eb1f9 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspector.cs @@ -7,7 +7,7 @@ internal sealed class SolutionInspector { private readonly List staticInterfaces = []; private readonly List instancedInterfaces = []; - private readonly List staticMethods = []; + private readonly List staticMembers = []; private readonly List docs = []; private readonly List warnings = []; private readonly TypeInspector typeInspector = new(); @@ -18,7 +18,7 @@ internal sealed class SolutionInspector public SolutionInspector (Preferences prefs, string entryAssemblyName) { memberInspector = new(prefs, typeInspector, serdeInspector); - interfaceInspector = new(prefs, memberInspector, entryAssemblyName); + interfaceInspector = new(memberInspector, entryAssemblyName); } /// @@ -54,7 +54,7 @@ private void AddSkippedAssemblyWarning (string assemblyPath, Exception exception private SolutionInspection CreateInspection (MetadataLoadContext ctx) => new(ctx) { StaticInterfaces = staticInterfaces.DistinctBy(i => i.FullName).ToArray(), InstancedInterfaces = instancedInterfaces.DistinctBy(i => i.FullName).ToArray(), - StaticMethods = staticMethods.ToArray(), + StaticMembers = staticMembers.ToArray(), Types = typeInspector.Collect(), Serialized = serdeInspector.Collect(), Documentation = docs.ToArray(), @@ -78,45 +78,52 @@ private void InspectAssembly (Assembly assembly) 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)) - InspectExportedStaticMethod(method); + InspectStaticMethod(method); } - private void InspectAssemblyAttribute (CustomAttributeData attribute) + private void InspectAssemblyAttribute (CustomAttributeData attr) { var interop = default(InteropKind); - var name = attribute.AttributeType.FullName; - if (name == typeof(JSExportAttribute).FullName) interop = InteropKind.Export; - else if (name == typeof(JSImportAttribute).FullName) interop = InteropKind.Import; + 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)attribute.ConstructorArguments[0].Value!) - InspectStaticInteropInterface((Type)arg.Value!, interop); + foreach (var arg in (IEnumerable)attr.ConstructorArguments[0].Value!) + InspectStaticInterface((Type)arg.Value!, interop); } - private void InspectExportedStaticMethod (MethodInfo info) + private void InspectStaticMethod (MethodInfo info) { var interop = default(InteropKind?); - var @event = false; foreach (var attr in info.CustomAttributes.Select(a => a.AttributeType.FullName)) - if (attr == typeof(JSInvokableAttribute).FullName) interop = InteropKind.Export; - else if (attr == typeof(JSFunctionAttribute).FullName) interop = InteropKind.Import; - else if (attr == typeof(JSEventAttribute).FullName) - { - interop = InteropKind.Import; - @event = true; - } - if (interop.HasValue) InspectStaticInteropMethod(info, interop.Value, @event); + 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 InspectStaticInteropMethod (MethodInfo info, InteropKind interop, bool @event) + private void InspectStaticEvent (EventInfo info) { - var method = memberInspector.Inspect(info, interop); - if (@event) method = new EventMeta(method, info.Name); - staticMethods.Add(method); - InspectMember(method); + 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 InspectStaticInteropInterface (Type type, InteropKind interop) + private void InspectStaticInterface (Type type, InteropKind interop) { var interfaceMeta = interfaceInspector.Inspect(type, interop); staticInterfaces.Add(interfaceMeta); @@ -139,11 +146,14 @@ private void InspectMember (MemberMeta meta) 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 (IsInstancedInteropInterface(type, out var instanceType)) + if (IsInstancedInterface(type, out var instanceType)) instancedInterfaces.Add(interfaceInspector.Inspect(instanceType, interop)); } } diff --git a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs index d1bb9b5a..c374d76d 100644 --- a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs @@ -25,7 +25,7 @@ namespace Bootsharp.Generated; public static class Dependencies { [System.Runtime.CompilerServices.ModuleInitializer] - {{JoinLines(added)}} + {{Fmt(added)}} internal static void RegisterDynamicDependencies () { } } """; @@ -39,17 +39,17 @@ private void AddGeneratedCommon () private void AddGeneratedInteropClasses (SolutionInspection inspection) { - foreach (var inter in inspection.StaticInterfaces) - Add(All, inter.FullName, entryAssembly); - foreach (var inter in inspection.InstancedInterfaces) - if (inter.Interop == InteropKind.Import) - Add(All, inter.FullName, entryAssembly); + foreach (var it in inspection.StaticInterfaces) + Add(All, it.FullName, entryAssembly); + foreach (var it in inspection.InstancedInterfaces) + if (it.Interop == InteropKind.Import) + Add(All, it.FullName, entryAssembly); } private void AddClassesWithInteropMethods (SolutionInspection inspection) { - foreach (var method in inspection.StaticMethods) - Add(All, method.Space, method.Assembly); + foreach (var member in inspection.StaticMembers) + Add(All, member.Space, member.Assembly); } private void Add (DynamicallyAccessedMemberTypes types, string name, string assembly) diff --git a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs index 6bab5e46..b5d15192 100644 --- a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs @@ -5,8 +5,11 @@ namespace Bootsharp.Publish; /// 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)); @@ -26,24 +29,24 @@ internal static class InterfaceRegistrations [System.Runtime.CompilerServices.ModuleInitializer] internal static void RegisterInterfaces () { - {{JoinLines(inspection.StaticInterfaces.Select(EmitRegistration), 3)}} + {{Fmt(inspection.StaticInterfaces.Select(EmitRegistration), 3)}} } } } - {{JoinLines(classes, 0, "\n\n")}} + {{Fmt(classes, 0, "\n\n")}} """; } private string EmitRegistration (InterfaceMeta i) { - var inter = i.Interop == InteropKind.Import + 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}, {inter});"; + return $"Interfaces.Register({key}, {it});"; } private string EmitExportClass (InterfaceMeta i) => @@ -56,10 +59,13 @@ public class {{i.Name}} public {{i.Name}} ({{i.TypeSyntax}} handler) { - {{i.Name}}.handler = handler; + {{Fmt([ + $"{i.Name}.handler = handler;", + ..i.Members.OfType().Select(e => $"handler.{e.Name} += {e.Name}.Invoke;") + ], 3)}} } - {{JoinLines(i.Members.Select(EmitExport), 2)}} + {{Fmt(i.Members.Select(EmitExport), 2)}} } } """; @@ -70,7 +76,7 @@ namespace {{i.Namespace}} { public class {{i.Name}} : {{i.TypeSyntax}} { - {{JoinLines(i.Members.Select(m => EmitImport(i, m)), 2)}} + {{Fmt(i.Members.Select(m => EmitImport(i, m)), 2)}} } } """; @@ -79,94 +85,97 @@ private string EmitInstancedImportClass (InterfaceMeta i) => $$""" namespace {{i.Namespace}} { - public class {{i.Name}}(global::System.Int32 _id) : {{i.TypeSyntax}} + public class {{i.Name}} (global::System.Int32 id) : {{i.TypeSyntax}} { - ~{{i.Name}}() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); + internal readonly global::System.Int32 _id = id; - {{JoinLines(i.Members.Select(m => EmitInstancedImport(i, m)), 2)}} + ~{{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 EmitInstancedImport (InterfaceMeta i, MemberMeta member) => member switch { - PropertyMeta prop => EmitInstancedPropertyImport(i, prop), - _ => EmitInstancedMethodImport(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 EmitPropertyExport (PropertyMeta prop) + private string EmitEventImport (EventMeta evt) { - var name = prop.Name; - var type = prop.Value.TypeSyntax; - var get = $"[JSInvokable] public static {type} GetProperty{name} () => handler.{name};"; - var set = $"[JSInvokable] public static void SetProperty{name} ({type} value) => handler.{name} = value;"; - return JoinLines(0, prop.CanGet ? get : null, prop.CanSet ? set : null); + 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 EmitMethodExport (MethodMeta method) + private string EmitPropertyExport (PropertyMeta prop) { - 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 $"[JSInvokable] {sig} => handler.{method.Name}({args});"; + 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 space = $"global::Bootsharp.Generated.Interop.Proxy_{prop.Space.Replace('.', '_')}"; + 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}} { - {{JoinLines( - prop.CanGet ? $"get => {space}_GetProperty{prop.Name}();" : null, - prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(value);" : null + {{Fmt( + prop.CanGet ? $"get => {space}_GetProperty{prop.Name}({getArgs});" : null, + prop.CanSet ? $"set => {space}_SetProperty{prop.Name}({setArgs});" : null )}} } """; } - private string EmitMethodImport (InterfaceMeta i, MethodMeta method) + private string EmitMethodExport (MethodMeta method) { var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var contract = method is EventMeta @event ? @event.MethodName : method.Name; + var sig = $"public static {method.Value.TypeSyntax} {method.Name} ({sigArgs})"; var args = string.Join(", ", method.Arguments.Select(a => a.Name)); - var name = $"Proxy_{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Value.TypeSyntax} {i.TypeSyntax}.{contract} ({sigArgs}) => " + - $"global::Bootsharp.Generated.Interop.{name}({args});"; + return $"[Export] {sig} => handler.{method.Name}({args});"; } - private string EmitInstancedPropertyImport (InterfaceMeta i, PropertyMeta prop) + private string EmitMethodImport (InterfaceMeta i, MethodMeta method) { - var space = $"global::Bootsharp.Generated.Interop.Proxy_{prop.Space.Replace('.', '_')}"; - return - $$""" - {{prop.Value.TypeSyntax}} {{i.TypeSyntax}}.{{prop.Name}} - { - {{JoinLines( - prop.CanGet ? $"get => {space}_GetProperty{prop.Name}(_id);" : null, - prop.CanSet ? $"set => {space}_SetProperty{prop.Name}(_id, value);" : null - )}} - } - """; + 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 string EmitInstancedMethodImport (InterfaceMeta i, MethodMeta method) + private bool IsInstanced (MemberMeta member) { - var sigArgs = string.Join(", ", method.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var contract = method is EventMeta @event ? @event.MethodName : method.Name; - var args = PrependInstanceIdArgName(string.Join(", ", method.Arguments.Select(a => a.Name))); - var name = $"Proxy_{method.Space.Replace('.', '_')}_{method.Name}"; - return $"{method.Value.TypeSyntax} {i.TypeSyntax}.{contract} ({sigArgs}) => " + - $"global::Bootsharp.Generated.Interop.{name}({args});"; + 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 d6efc115..fddaea77 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -7,22 +7,12 @@ namespace Bootsharp.Publish; /// internal sealed class InteropGenerator { - private readonly InteropInitializerGenerator initGenerator = new(); - private readonly HashSet methods = []; + private readonly HashSet registered = []; private IReadOnlyCollection instanced = []; public string Generate (SolutionInspection inspection) { instanced = inspection.InstancedInterfaces; - foreach (var method in inspection.StaticMethods) - if (method.Interop == InteropKind.Export) AddMethodExport(method); - else AddMethodImport(method); - foreach (var inter in inspection.StaticInterfaces) - foreach (var member in inter.Members) - AddMember(member); - foreach (var inter in inspection.InstancedInterfaces) - foreach (var member in inter.Members) - AddMember(member); return $$""" #nullable enable @@ -35,175 +25,177 @@ namespace Bootsharp.Generated; public static partial class Interop { - [System.Runtime.InteropServices.JavaScript.JSExport] internal static void DisposeExportedInstance (int id) => Instances.Dispose(id); - [System.Runtime.InteropServices.JavaScript.JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); + [JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id); + [JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); - {{initGenerator.Generate(inspection.StaticMethods)}} + {{new InteropInitializerGenerator().Generate(inspection)}} - {{JoinLines(methods)}} + {{Fmt(inspection.StaticMembers.SelectMany(EmitMember))}} + {{Fmt(inspection.StaticInterfaces.SelectMany(i => i.Members.SelectMany(EmitMember)))}} + {{Fmt(inspection.InstancedInterfaces.SelectMany(i => i.Members.SelectMany(EmitMember)))}} } """; } - private void AddMember (MemberMeta member) + private IEnumerable EmitMember (MemberMeta member) => member switch { + EventMeta { Interop: InteropKind.Export } e => EmitEventExport(e), + EventMeta { Interop: InteropKind.Import } e => EmitEventImport(e), + PropertyMeta { Interop: InteropKind.Export } p => EmitPropertyExport(p), + PropertyMeta { Interop: InteropKind.Import } p => EmitPropertyImport(p), + MethodMeta { Interop: InteropKind.Export } m => EmitMethodExport(m), + _ => EmitMethodImport((MethodMeta)member) + }; + + private IEnumerable EmitEventExport (EventMeta evt) { - switch (member) - { - case PropertyMeta { Interop: InteropKind.Export } p: AddPropertyExport(p); break; - case PropertyMeta { Interop: InteropKind.Import } p: AddPropertyImport(p); break; - case MethodMeta { Interop: InteropKind.Export } m: AddMethodExport(m); break; - case MethodMeta { Interop: InteropKind.Import } m: AddMethodImport(m); break; - } + 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)}"; + 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}"; + var sigArgs = string.Join(", ", evt.Arguments.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); + var invArgs = string.Join(", ", evt.Arguments.Select(Serialize)); + yield return $"private static void {handler} ({sigArgs}) => {name}({invArgs});"; } - private void AddPropertyExport (PropertyMeta prop) + private IEnumerable EmitEventImport (EventMeta evt) { - var instanced = TryInstanced(prop, out var instance); - if (prop.CanGet) - { - var marshalAs = MarshalAmbiguous(prop.Value, true); - var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; - var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; - var body = instanced - ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{prop.Name}" - : $"global::{prop.Space}.GetProperty{prop.Name}()"; - if (prop.Value.IsInstance) body = $"Instances.Register({body})"; - else if (Serialized(prop.Value, out var id)) body = $"Serializer.Serialize({body}, {id})"; - methods.Add($"{attr}internal static {BuildValueSyntax(prop.Value)} {name} ({args}) => {body};"); - } - if (prop.CanSet) - { - var attr = "[System.Runtime.InteropServices.JavaScript.JSExport] "; - var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; - var args = BuildParameter(prop.Value, "value"); - if (instanced) args = PrependInstanceIdArgTypeAndName(args); - var value = prop.Value.InstanceType is { } it - ? $"new global::{BuildInterfaceImplName(it, InteropKind.Import).full}(value)" - : Serialized(prop.Value, out var id) ? $"Serializer.Deserialize(value, {id})" : "value"; - var body = instanced - ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{prop.Name} = {value}" - : $"global::{prop.Space}.SetProperty{prop.Name}({value})"; - methods.Add($"{attr}internal static void {name} ({args}) => {body};"); - } + var inst = TryInstanced(evt, out var instance); + var name = $"{evt.Space.Replace('.', '_')}_Invoke{evt.Name}"; + var args = string.Join(", ", evt.Arguments.Select(a => BuildParameter(a.Value, a.Name))); + if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var invName = evt.Info.DeclaringType is { IsInterface: true } it + ? inst + ? $"Instances.Import(_id, static id => new global::{instance!.FullName}(id)).Invoke{evt.Name}" + : $"((global::{evt.Space})Interfaces.Imports[typeof({BuildSyntax(it)})].Instance).Invoke{evt.Name}" + : $"global::{evt.Info.DeclaringType!.FullName!.Replace('+', '.')}.Bootsharp_Invoke_{evt.Name}"; + var invArgs = string.Join(", ", evt.Arguments.Select(Deserialize)); + yield return $"[JSExport] internal static void {name} ({args}) => {invName}({invArgs});"; } - private void AddPropertyImport (PropertyMeta prop) + private IEnumerable EmitPropertyExport (PropertyMeta prop) { - var instanced = TryInstanced(prop, out _); + var inst = TryInstanced(prop, out var instance); if (prop.CanGet) { - var endpoint = $"""("{prop.JSSpace}.getProperty{prop.Name}Serialized", "Bootsharp")"""; - var marshalAs = MarshalAmbiguous(prop.Value, true); - var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] {marshalAs}"; + var attr = $"[JSExport] {MarshalAmbiguous(prop.Value, true)}"; var name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; - methods.Add($"{attr}internal static partial {BuildValueSyntax(prop.Value)} {name} ({args});"); + 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};"; } if (prop.CanSet) { - var endpoint = $"""("{prop.JSSpace}.setProperty{prop.Name}Serialized", "Bootsharp")"""; - var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] "; var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; var args = BuildParameter(prop.Value, "value"); - if (instanced) args = PrependInstanceIdArgTypeAndName(args); - methods.Add($"{attr}internal static partial void {name} ({args});"); + 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})"; + yield return $"[JSExport] internal static void {name} ({args}) => {body};"; } - AddPropertyImportProxy(prop); } - private void AddPropertyImportProxy (PropertyMeta prop) + private IEnumerable EmitPropertyImport (PropertyMeta prop) { - var instanced = TryInstanced(prop, out _); + 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 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 name = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var args = instanced ? PrependInstanceIdArgTypeAndName("") : ""; - var body = instanced ? $"{name}(_id)" : $"{name}()"; - if (prop.Value.InstanceType is { } it) - body = $"({BuildSyntax(it)})new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({body})"; - else if (Serialized(prop.Value, out var id)) body = $"Serializer.Deserialize({body}, {id})"; - methods.Add($"public static {prop.Value.TypeSyntax} Proxy_{name}({args}) => {body};"); + var body = Deserialize(prop.Value, inst ? $"{serdeName}(_id)" : $"{serdeName}()"); + yield return $"public static {prop.Value.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)}"; + yield return $"{attr}internal static partial void {serdeName} ({serdeArgs});"; + var name = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; var args = $"{prop.Value.TypeSyntax} value"; - if (instanced) args = PrependInstanceIdArgTypeAndName(args); - var value = prop.Value.IsInstance ? "Instances.Register(value)" : - Serialized(prop.Value, out var id) ? $"Serializer.Serialize(value, {id})" : "value"; - var body = instanced ? $"{name}(_id, {value})" : $"{name}({value})"; - methods.Add($"public static void Proxy_{name}({args}) => {body};"); + if (inst) args = $"{BuildSyntax(typeof(int))} {PrependIdArg(args)}"; + var value = Serialize(prop.Value, "value"); + var body = inst ? $"{serdeName}(_id, {value})" : $"{serdeName}({value})"; + yield return $"public static void {name}({args}) => {body};"; } } - private void AddMethodExport (MethodMeta method) + private IEnumerable EmitMethodExport (MethodMeta method) { - var instanced = TryInstanced(method, out var instance); + var inst = TryInstanced(method, out var instance); var wait = ShouldWait(method); - var marshalAs = MarshalAmbiguous(method.Value, true); - var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; + var attr = $"[JSExport] {MarshalAmbiguous(method.Value, true)}"; var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; var @return = BuildValueSyntax(method.Value); if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (instanced) sigArgs = PrependInstanceIdArgTypeAndName(sigArgs); - var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); - var body = instanced - ? $"(({instance!.TypeSyntax})Instances.Get(_id)).{method.Name}({callArgs})" - : $"global::{method.Space}.{method.Name}({callArgs})"; - if (wait) body = $"await {body}"; - if (method.Value.IsInstance) body = $"Instances.Register({body})"; - else if (Serialized(method.Value, out var id)) body = $"Serializer.Serialize({body}, {id})"; - methods.Add($"{attr}internal static {@return} {name} ({sigArgs}) => {body};"); - - string BuildCallArg (ArgumentMeta arg) - { - if (arg.Value.InstanceType is { } it) - return $"new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({arg.Name})"; - if (Serialized(arg.Value, out var id)) return $"Serializer.Deserialize({arg.Name}, {id})"; - return arg.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})"); + yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; } - private void AddMethodImport (MethodMeta method) + private IEnumerable EmitMethodImport (MethodMeta method) { - var instanced = TryInstanced(method, out _); + var inst = TryInstanced(method, out _); var marshalAs = MarshalAmbiguous(method.Value, true); - var endpoint = $"""("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")"""; - var attr = $"[System.Runtime.InteropServices.JavaScript.JSImport{endpoint}] {marshalAs}"; + var attr = $"""[JSImport("{method.JSSpace}.{method.JSName}Serialized", "Bootsharp")] {marshalAs}"""; var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; var @return = BuildValueSyntax(method.Value); if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; var args = string.Join(", ", method.Arguments.Select(a => BuildParameter(a.Value, a.Name))); - if (instanced) args = PrependInstanceIdArgTypeAndName(args); - methods.Add($"{attr}internal static partial {@return} {name} ({args});"); - AddMethodImportProxy(method); + if (inst) 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}"; + 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})"); + yield return $"public static {@return} {name} ({sigArgs}) => {body};"; } - private void AddMethodImportProxy (MethodMeta method) + private string? EmitInstanceRegistrar (InterfaceMeta instance) { - var instanced = TryInstanced(method, out _); - var wait = ShouldWait(method); - var @return = $"{(wait ? "async " : "")}{method.Value.TypeSyntax}"; - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var sigArgs = string.Join(", ", method.Arguments.Select(arg => $"{arg.Value.TypeSyntax} {arg.Name}")); - if (instanced) sigArgs = PrependInstanceIdArgTypeAndName(sigArgs); - var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); - if (instanced) callArgs = PrependInstanceIdArgName(callArgs); - var body = $"{name}({callArgs})"; - if (wait) body = $"await {body}"; - if (method.Value.InstanceType is { } it) - body = $"({BuildSyntax(it)})new global::{BuildInterfaceImplName(it, InteropKind.Import).full}({body})"; - else if (Serialized(method.Value, out var id)) body = $"Serializer.Deserialize({body}, {id})"; - methods.Add($"public static {@return} Proxy_{name} ({sigArgs}) => {body};"); - - string BuildCallArg (ArgumentMeta arg) - { - if (arg.Value.IsInstance) return $"Instances.Register({arg.Name})"; - if (Serialized(arg.Value, out var id)) return $"Serializer.Serialize({arg.Name}, {id})"; - return arg.Name; - } + 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) @@ -212,24 +204,33 @@ private string BuildParameter (ValueMeta value, string name) return $"{MarshalAmbiguous(value, false)}{type} {name}"; } - private string BuildValueSyntax (ValueMeta value) + private string Serialize (ArgumentMeta arg) => Serialize(arg.Value, arg.Name); + private string Serialize (ValueMeta value, string exp) { - var nil = value.Nullable && !value.IsSerialized ? "?" : ""; - if (value.IsInstance) return $"global::System.Int32{nil}"; - if (value.IsSerialized) return $"global::System.Int64{nil}"; - return value.TypeSyntax; + if (value.IsInstance) return RegisterInstance(value, exp); + if (Serialized(value, out var id)) return $"Serializer.Serialize({exp}, {id})"; + return exp; } - private bool TryInstanced (MemberMeta member, [NotNullWhen(true)] out InterfaceMeta? instance) + private string Deserialize (ArgumentMeta arg) => Deserialize(arg.Value, arg.Name); + private string Deserialize (ValueMeta value, string exp) { - instance = instanced.FirstOrDefault(i => i.Members.Contains(member)); - return instance is not null; + 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 bool ShouldWait (MethodMeta method) + private string BuildValueSyntax (ValueMeta value) { - if (!method.Async) return false; - return method.Value.IsSerialized || method.Value.IsInstance; + var nil = value.Nullable && !value.IsSerialized ? "?" : ""; + if (value.IsInstance) return $"global::System.Int32{nil}"; + if (value.IsSerialized) return $"global::System.Int64{nil}"; + return value.TypeSyntax; } private static string MarshalAmbiguous (ValueMeta value, bool @return) @@ -254,4 +255,24 @@ private static bool Serialized (ValueMeta meta, [NotNullWhen(true)] out string? 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; + } } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs index 5bfceb41..01aaab86 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs @@ -2,60 +2,49 @@ namespace Bootsharp.Publish; internal sealed class InteropInitializerGenerator { - public string Generate (IEnumerable methods) + public string Generate (SolutionInspection inspection) { - var interop = methods.Where(m => m.Interop == InteropKind.Import) - .OrderBy(BuildProxyName).ToArray(); - if (interop.Length == 0) return ""; + 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 $$""" - {{JoinLines(interop.Select(BuildAccessor))}} + {{Fmt(methods.Select(BuildMethodAccessor))}} [ModuleInitializer] internal static unsafe void Initialize () { - {{JoinLines(interop.Select(BuildAssignment), 2)}} + {{Fmt(2, [ + ..events.Select(BuildEventSubscription), + ..methods.Select(BuildMethodAssignment) + ])}} } """; } - private static string BuildAccessor (MethodMeta method) + private static string BuildMethodAccessor (MethodMeta method) { - var proxy = BuildProxyName(method); - var ptrType = BuildPointerType(method); - var target = BuildTargetName(method); - var accessor = BuildAccessorName(method); + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + var argType = string.Join(", ", [..method.Arguments.Select(a => a.Value.TypeSyntax), method.Value.TypeSyntax]); + var ptrType = $"delegate* managed<{argType}>"; + var accessor = $"""[UnsafeAccessorType("{method.Space}, {method.Assembly}")]"""; return $""" - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "{proxy}")] - private static extern unsafe ref {ptrType} {accessor} ([UnsafeAccessorType("{target}")] object? _); + [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_{method.Name}")] + private static extern unsafe ref {ptrType} Access_{name} ({accessor} object? _); """; } - private static string BuildAssignment (MethodMeta method) + private static string BuildEventSubscription (EventMeta evt) { - var proxy = BuildProxyName(method); - return $"{BuildAccessorName(method)}(default) = &{proxy};"; + var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; + return $"global::{evt.Space}.{evt.Name} += {handler};"; } - private static string BuildPointerType (MethodMeta method) + private static string BuildMethodAssignment (MethodMeta method) { - var args = method.Arguments.Select(a => a.Value.TypeSyntax).ToList(); - args.Add(method.Value.TypeSyntax); - return $"delegate* managed<{string.Join(", ", args)}>"; - } - - private static string BuildAccessorName (MethodMeta method) - { - return $"Get_{BuildProxyName(method)}"; - } - - private static string BuildProxyName (MethodMeta method) - { - return string.Concat($"Proxy_{method.Space}_{method.Name}" - .Select(c => char.IsLetterOrDigit(c) || c == '_' ? c : '_')); - } - - private static string BuildTargetName (MethodMeta method) - { - return $"{method.Space}, {method.Assembly}"; + var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; + return $"Access_{name}(default) = &{name};"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index cc538339..b27d9c53 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -13,9 +13,9 @@ namespace Bootsharp.Generated; internal static class SerializerContext { - {{JoinLines(serialized.Select(EmitFactory))}} + {{Fmt(serialized.Select(EmitFactory))}} - {{JoinLines(serialized.SelectMany(EmitHelpers), separator: "\n\n")}} + {{Fmt(serialized.SelectMany(EmitHelpers), separator: "\n\n")}} } """; } @@ -26,8 +26,8 @@ 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.{TrimGenericArgs(list.Type.Name)}({list.Element.Id})", - SerializedDictionaryMeta dic => $"Serializer.{TrimGenericArgs(dic.Type.Name)}({dic.Key.Id}, {dic.Value.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) }};"; @@ -46,13 +46,13 @@ private IEnumerable EmitHelpers (SerializedMeta meta) yield return $$""" private static void Write_{{obj.Id}} (ref Writer writer, {{obj.Syntax}} value) { - {{JoinLines(EmitObjectWrite(obj))}} + {{Fmt(EmitObjectWrite(obj))}} } """; yield return $$""" private static {{obj.Syntax}} Read_{{obj.Id}} (ref Reader reader) { - {{JoinLines(EmitObjectRead(obj))}} + {{Fmt(EmitObjectRead(obj))}} } """; foreach (var prop in obj.Properties.Where(p => p.Kind == SerializedPropertyKind.Field)) diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs index f502e27e..6741c3ed 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingClassGenerator.cs @@ -5,32 +5,44 @@ internal sealed class BindingClassGenerator public string Generate (IReadOnlyCollection instanced) { var exported = instanced.Where(i => i.Interop == InteropKind.Export); - return JoinLines(exported.Select(EmitClass), 0) + '\n'; + return Fmt(exported.Select(EmitClass), 0) + '\n'; } - private string EmitClass (InterfaceMeta inter) => + private string EmitClass (InterfaceMeta instance) => $$""" - class {{BuildJSInteropInstanceClassName(inter)}} { - constructor(_id) { this._id = _id; disposeOnFinalize(this, _id); } - {{JoinLines(inter.Members.Select(EmitMember))}} + class {{instance.JSName}} { + {{Fmt([ + "constructor(_id) { this._id = _id; }", + ..instance.Members.Select(EmitMember) + ])}} } """; private string EmitMember (MemberMeta member) => member switch { + EventMeta evt => EmitEvent(evt), PropertyMeta prop => EmitProperty(prop), _ => EmitMethod((MethodMeta)member) }; + private string EmitEvent (EventMeta evt) + { + var args = string.Join(", ", evt.Arguments.Select(a => a.JSName)); + return Fmt(0, + $"{evt.JSName} = new Event();", + $"broadcast{evt.Name}({args}) {{ this.{evt.JSName}.broadcast({args}); }}" + ); + } + private string EmitMethod (MethodMeta method) { var sigArgs = string.Join(", ", method.Arguments.Select(a => a.Name)); - var callArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; - var body = $"{method.JSSpace}.{method.JSName}({callArgs})"; + var invArgs = sigArgs.Length > 0 ? $"this._id, {sigArgs}" : "this._id"; + var body = $"{method.JSSpace}.{method.JSName}({invArgs})"; if (!method.Void) body = $"return {body}"; return $"{method.JSName}({sigArgs}) {{ {body}; }}"; } - private string EmitProperty (PropertyMeta p) => JoinLines(0, + private string EmitProperty (PropertyMeta p) => Fmt(0, p.CanGet ? $"get {p.JSName}() {{ return {p.JSSpace}.getProperty{p.Name}(this._id); }}" : null, p.CanSet ? $"set {p.JSName}(value) {{ {p.JSSpace}.setProperty{p.Name}(this._id, value); }}" : null ); diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 41381998..0d2725a6 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using System.Text; namespace Bootsharp.Publish; @@ -6,22 +7,19 @@ internal sealed class BindingGenerator (Preferences prefs, bool debug) { private record Binding (MemberMeta? Member, Type? Enum, string Namespace); - private readonly StringBuilder builder = new(); - private readonly BindingClassGenerator classGenerator = new(); - private readonly BindingSerializerGenerator serdeGenerator = new(); - private IReadOnlyCollection instanced = []; - private Binding binding => bindings[index]; private Binding? prevBinding => index == 0 ? null : bindings[index - 1]; private Binding? nextBinding => index == bindings.Length - 1 ? null : bindings[index + 1]; - private Binding[] bindings = null!; + private readonly StringBuilder builder = new(); + private IReadOnlyCollection instanced = []; + private Binding[] bindings = []; private int index, level; public string Generate (SolutionInspection inspection) { instanced = inspection.InstancedInterfaces; - bindings = inspection.StaticMethods + 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)))) @@ -41,11 +39,20 @@ public string Generate (SolutionInspection inspection) builder.Append("\n\n"); } - builder.Append(serdeGenerator.Generate(inspection.Serialized)); - builder.Append('\n'); + EmitHelpers(); + builder.Append("\n\n"); + + builder.Append(new BindingSerializerGenerator().Generate(inspection.Serialized)); + builder.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"); if (inspection.InstancedInterfaces.Count > 0) - builder.Append(classGenerator.Generate(inspection.InstancedInterfaces)); + builder.Append(new BindingClassGenerator().Generate(inspection.InstancedInterfaces)); + for (index = 0; index < bindings.Length; index++) EmitBinding(); @@ -58,7 +65,7 @@ private void EmitImports () """ import { exports } from "./exports"; import { Event } from "./event"; - import { registerInstance, getInstance, disposeOnFinalize } from "./instances"; + import { instances } from "./instances"; import { serialize, deserialize, binary, types } from "./serialization"; """ ); @@ -88,6 +95,20 @@ function getImport(handler, serializedHandler, name) { ); } + private void EmitHelpers () + { + builder.Append( + """ + function importEvent(handler) { + const event = new Event(); + const broadcast = event.broadcast.bind(event); + event.broadcast = (...args) => { broadcast(...args); handler(...args); }; + return event; + } + """ + ); + } + private void EmitBinding () { if (ShouldOpenNamespace()) OpenNamespace(); @@ -143,7 +164,8 @@ private void EmitMember (MemberMeta member) { switch (member) { - case EventMeta e: EmitEvent(e); break; + case EventMeta { Interop: InteropKind.Export } e: EmitEventExport(e); break; + case EventMeta { Interop: InteropKind.Import } e: EmitEventImport(e); break; case PropertyMeta { Interop: InteropKind.Export } p: EmitPropertyExport(p); break; case PropertyMeta { Interop: InteropKind.Import } p: EmitPropertyImport(p); break; case MethodMeta { Interop: InteropKind.Export } m: EmitMethodExport(m); break; @@ -151,132 +173,114 @@ 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) + { + var invName = $"instances.export(_id, id => new {instance!.JSName}(id)).broadcast{evt.Name}"; + builder.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})"); + } + } + + 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}"; + 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}))"); + } + private void EmitPropertyExport (PropertyMeta prop) { - var instanced = this.instanced.Any(i => i.Members.Contains(prop)); + var inst = TryInstanced(prop, out _); if (prop.CanGet) { var fnName = $"{prop.Space.Replace('.', '_')}_GetProperty{prop.Name}"; - var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var body = instanced ? $"{endpoint}(_id)" : $"{endpoint}()"; - if (prop.Value.InstanceType is { } it) - body = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({body})"; - else if (prop.Value.IsSerialized) body = $"deserialize({body}, {prop.Value.Serialized.Id})"; - if (instanced) builder.Append($"{Br}getProperty{prop.Name}(_id) {{ return {body}; }}"); + 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}; }}"); } if (prop.CanSet) { var fnName = $"{prop.Space.Replace('.', '_')}_SetProperty{prop.Name}"; - var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var value = prop.Value.IsInstance ? "registerInstance(value)" : - prop.Value.IsSerialized ? $"serialize(value, {prop.Value.Serialized.Id})" : "value"; - var body = instanced ? $"{endpoint}(_id, {value})" : $"{endpoint}({value})"; - if (instanced) builder.Append($"{Br}setProperty{prop.Name}(_id, value) {{ {body}; }}"); + 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}; }}"); } } private void EmitPropertyImport (PropertyMeta prop) { - var instanced = this.instanced.Any(i => i.Members.Contains(prop)); + var inst = TryInstanced(prop, out _); if (prop.CanGet) { - if (!instanced) builder.Append($"{Br}get {prop.JSName}() {{ return this._{prop.JSName}; }}"); - var args = instanced ? "_id" : ""; - var body = instanced ? $"getInstance(_id).{prop.JSName}" : $"this.{prop.JSName}"; - if (prop.Value.IsInstance) body = $"registerInstance({body})"; - else if (prop.Value.IsSerialized) body = $"serialize({body}, {prop.Value.Serialized.Id})"; + 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 (prop.CanSet) { - if (!instanced) builder.Append($"{Br}set {prop.JSName}(value) {{ this._{prop.JSName} = value; }}"); - var value = prop.Value.InstanceType is { } it - ? $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}(value)" - : prop.Value.IsSerialized ? $"deserialize(value, {prop.Value.Serialized.Id})" : "value"; - var args = instanced ? "_id, value" : "value"; - var body = instanced ? $"getInstance(_id).{prop.JSName} = {value}" : $"this.{prop.JSName} = {value}"; + 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}; }}"); } } - private void EmitEvent (EventMeta method) - { - var instanced = this.instanced.Any(i => i.Members.Contains(method)); - var name = method.JSName; - if (!instanced) builder.Append($"{Br}{name}: new Event()"); - var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); - var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); - var handler = instanced ? "getInstance(_id)" : method.JSSpace; - builder.Append($"{Br}{name}Serialized: ({sigArgs}) => {handler}.{name}.broadcast({callArgs})"); - - string BuildCallArg (ArgumentMeta arg) - { - if (!arg.Value.IsSerialized) return arg.JSName; - // 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'. - var toUndefined = arg.Value.Nullable ? " ?? undefined" : ""; - return $"deserialize({arg.JSName}, {arg.Value.Serialized.Id}){toUndefined}"; - } - } - private void EmitMethodExport (MethodMeta method) { - var instanced = this.instanced.Any(i => i.Members.Contains(method)); + var inst = TryInstanced(method, out _); var wait = ShouldWait(method); var fnName = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var endpoint = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; - var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); - var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); - if (instanced) callArgs = PrependInstanceIdArgName(callArgs); - var body = $"{(wait ? "await " : "")}{endpoint}({callArgs})"; - if (method.Value.InstanceType is { } it) - body = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({body})"; - else if (method.Value.IsSerialized) body = $"deserialize({body}, {method.Value.Serialized.Id})"; - builder.Append($"{Br}{method.JSName}: {(wait ? "async " : "")}({sigArgs}) => {body}"); - - string BuildCallArg (ArgumentMeta arg) - { - var name = arg.JSName; - if (arg.Value.IsInstance) name = $"registerInstance({name})"; - else if (arg.Value.IsSerialized) name = $"serialize({name}, {arg.Value.Serialized.Id})"; - return 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}"); } private void EmitMethodImport (MethodMeta method) { - var instanced = this.instanced.Any(i => i.Members.Contains(method)); + var inst = TryInstanced(method, out _); var wait = ShouldWait(method); - var fnName = method.JSName; - var sigArgs = string.Join(", ", method.Arguments.Select(a => a.JSName)); - if (instanced) sigArgs = PrependInstanceIdArgName(sigArgs); - var callArgs = string.Join(", ", method.Arguments.Select(BuildCallArg)); - var handler = instanced ? $"getInstance(_id).{fnName}" : $"this.{fnName}Handler"; - var body = $"{(wait ? "await " : "")}{handler}({callArgs})"; - if (method.Value.IsInstance) body = $"registerInstance({body})"; - else if (method.Value.IsSerialized) body = $"serialize({body}, {method.Value.Serialized.Id})"; - var serdeHandler = $"{(wait ? "async " : "")}({sigArgs}) => {body}"; - if (instanced) builder.Append($"{Br}{fnName}Serialized: {serdeHandler}"); + 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})"); + var serdeHandler = $"{(wait ? "async " : "")}({args}) => {body}"; + if (inst) builder.Append($"{Br}{name}Serialized: {serdeHandler}"); else { - var serde = $"this.{fnName}SerializedHandler"; - var serdeExp = debug ? $"getImport({handler}, {serde}, \"{binding.Namespace}.{fnName}\")" : serde; - builder.Append($"{Br}get {fnName}() {{ return {handler}; }}"); - builder.Append($"{Br}set {fnName}(handler) {{ {handler} = handler; {serde} = {serdeHandler}; }}"); - builder.Append($"{Br}get {fnName}Serialized() {{ return {serdeExp}; }}"); - } - - string BuildCallArg (ArgumentMeta arg) - { - var name = arg.JSName; - if (arg.Value.InstanceType is { } it) - name = $"new {BuildJSInteropInstanceClassName(this.instanced.First(i => i.Type == it))}({name})"; - else if (arg.Value.IsSerialized) name = $"deserialize({name}, {arg.Value.Serialized.Id})"; - return name; + 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}; }}"); } } @@ -289,6 +293,71 @@ private void EmitEnum (Type @enum) builder.Append($"{Br}{@enum.Name}: {{ {fields} }}"); } + private void EmitInstanceRegistrar (InterfaceMeta instance) + { + var events = instance.Members.OfType().ToArray(); + builder.Append( + $$""" + function {{BuildRegistrarName(instance)}}(instance) { + return instances.import(instance, _id => { + {{Fmt(events.Select(e => $"instance.{e.JSName}.subscribe(handle{e.Name});"))}} + return () => { + {{Fmt(events.Select(e => $"instance.{e.JSName}.unsubscribe(handle{e.Name});"), 2)}} + }; + + {{Fmt(events.Select(e => { + var fnName = $"{e.Space.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))); + return $"function handle{e.Name}({args}) {{ {invName}({invArgs}); }}"; + }))}} + }); + } + """ + ); + } + + 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; diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs index c49515cb..e486ce6f 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingSerializerGenerator.cs @@ -6,9 +6,9 @@ public string Generate (IReadOnlyCollection serialized) { if (serialized.Count == 0) return ""; return $""" - {JoinLines(serialized.Select(EmitFactory), 0)} + {Fmt(serialized.Select(EmitFactory), 0)} - {JoinLines(serialized.SelectMany(EmitHelpers), 0, "\n\n")} + {Fmt(serialized.SelectMany(EmitHelpers), 0, "\n\n")} """; } @@ -37,12 +37,12 @@ private IEnumerable EmitHelpers (SerializedMeta meta) if (meta is not SerializedObjectMeta obj) yield break; yield return $$""" function write_{{obj.Id}}(writer, value) { - {{JoinLines(EmitObjectWrite(obj))}} + {{Fmt(EmitObjectWrite(obj))}} } """; yield return $$""" function read_{{obj.Id}}(reader) { - {{JoinLines(EmitObjectRead(obj))}} + {{Fmt(EmitObjectRead(obj))}} } """; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs index e05ff893..056af12d 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DeclarationGenerator.cs @@ -5,8 +5,8 @@ internal sealed class DeclarationGenerator (Preferences prefs) private readonly MemberDeclarationGenerator membersGenerator = new(prefs); private readonly TypeDeclarationGenerator typesGenerator = new(prefs); - public string Generate (SolutionInspection inspection) => JoinLines(0, - """import type { Event } from "./event";""", + public string Generate (SolutionInspection inspection) => Fmt(0, + """import type { EventBroadcaster, EventSubscriber } from "./event";""", typesGenerator.Generate(inspection), membersGenerator.Generate(inspection) ) + "\n"; diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs index f4039f12..9f051d02 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/DocumentationBuilder.cs @@ -13,6 +13,26 @@ public string BuildType (Type type, int indent) return GetXml(asm, key) is { } xml ? Build(GetSummary(xml), indent) : ""; } + public string BuildEvent (EventMeta evt, int indent) + { + var asm = evt.Info.DeclaringType!.Assembly.GetName().Name!; + var key = $"E:{GetXmlKey(evt.Info.DeclaringType!)}.{evt.Name}"; + if (GetXml(asm, key) is not { } xml) return ""; + + var sum = GetSummary(xml); + foreach (var arg in evt.Arguments) + if ((GetArgXml(xml, arg) ?? GetDelegateArgXml(arg)) is { } x) + sum.Add($"@param {arg.JSName} {x.Value}"); + return Build(sum, indent); + + XElement? GetDelegateArgXml (ArgumentMeta arg) + { + var asm = evt.Info.EventHandlerType!.Assembly.GetName().Name!; + var key = $"T:{GetXmlKey(evt.Info.EventHandlerType!)}"; + return GetXml(asm, key) is { } x ? GetArgXml(x, arg) : null; + } + } + public string BuildProperty (MemberInfo member, int indent) { var asm = member.DeclaringType!.Assembly.GetName().Name!; @@ -27,7 +47,7 @@ public string BuildFunction (MethodMeta method, int indent) var sum = GetSummary(xml); foreach (var arg in method.Arguments) - if (xml.Elements("param").FirstOrDefault(e => e.Attribute("name")!.Value == arg.Info.Name) is { } x) + if (GetArgXml(xml, arg) is { } x) sum.Add($"@param {arg.JSName} {x.Value}"); if (xml.Element("returns") is { } returns) sum.Add($"@returns {returns.Value}"); @@ -53,11 +73,6 @@ string GetArgKey (Type type) } } - public string BuildEvent (EventMeta @event, int indent) - { - return BuildFunction(@event, indent); - } - private string Build (IReadOnlyList summary, int indent) { var pad = new string(' ', indent * 4); @@ -83,8 +98,13 @@ private static string GetXmlKey (Type type) .FirstOrDefault(e => e.Attribute("name")!.Value == key); } + private static XElement? GetArgXml (XElement xml, ArgumentMeta arg) + { + return xml.Elements("param").FirstOrDefault(e => e.Attribute("name")!.Value == arg.Info.Name); + } + private List GetSummary (XElement xml) { - return xml.Elements("summary").Select(e => e.Value).ToList(); + return xml.Elements("summary").Select(e => e.Value.Trim()).ToList(); } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs index 277f6471..1bb6583d 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/MemberDeclarationGenerator.cs @@ -18,7 +18,7 @@ internal sealed class MemberDeclarationGenerator (Preferences prefs) public string Generate (SolutionInspection inspection) { docs = new(inspection.Documentation); - members = inspection.StaticMethods + members = inspection.StaticMembers .Concat(inspection.StaticInterfaces.SelectMany(i => i.Members)) .OrderBy(m => m.JSSpace).ToArray(); for (index = 0; index < members.Length; index++) @@ -62,12 +62,21 @@ private void CloseNamespace () builder.Append("\n}"); } + private void DeclareEvent (EventMeta evt) + { + builder.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("]>;"); + } + 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(" | null"); + if (prop.Value.Nullable) builder.Append(" | undefined"); builder.Append(';'); } @@ -86,12 +95,4 @@ private void DeclareMethodImport (MethodMeta method) builder.AppendJoin(", ", method.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); builder.Append($") => {typeBuilder.BuildReturn(method)};"); } - - private void DeclareEvent (EventMeta @event) - { - builder.Append(docs.BuildEvent(@event, 1)); - builder.Append($"\n export const {@event.JSName}: Event<["); - builder.AppendJoin(", ", @event.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append("]>;"); - } } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs index 11ef6987..1444cb92 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeDeclarationGenerator.cs @@ -129,6 +129,16 @@ private void AppendProperty (string name, Type type, NullabilityInfo? nullabilit builder.Append(';'); } + 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("]>;"); + } + private void AppendInstancedProperty (PropertyMeta prop) { builder.Append(docs.BuildProperty(prop.Info, indent + 1)); @@ -136,15 +146,6 @@ private void AppendInstancedProperty (PropertyMeta prop) AppendProperty(name, prop.Value.Type.Clr, prop.Value.Nullability); } - private void AppendInstancedEvent (EventMeta meta) - { - builder.Append(docs.BuildEvent(meta, indent + 1)); - AppendLine(meta.JSName, indent + 1); - builder.Append(": Event<["); - builder.AppendJoin(", ", meta.Arguments.Select(a => $"{a.JSName}: {typeBuilder.BuildArg(a)}")); - builder.Append("]>;"); - } - private void AppendInstancedFunction (MethodMeta meta) { builder.Append(docs.BuildFunction(meta, indent + 1)); @@ -171,8 +172,8 @@ private void Append (string content, int level) private string BuildTypeName (Type type) { - var name = BuildJSSpaceName(type); - if (!type.IsGenericType) return name; + if (!type.IsGenericType) return type.Name; + var name = TrimGeneric(type.Name); var args = string.Join(", ", type.GetGenericArguments().Select(BuildTypeName)); return $"{name}<{args}>"; } diff --git a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs index 38af90ad..8a367327 100644 --- a/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs +++ b/src/cs/Bootsharp.Publish/Pack/DeclarationGenerator/TypeSyntaxBuilder.cs @@ -75,13 +75,13 @@ private string BuildDictionary (Type key, Type value) private string BuildUser (Type type) { - if (type.IsGenericType) - { - EnterNullability(); - var args = string.Join(", ", type.GenericTypeArguments.Select(Build)); - return $"{BuildJSSpaceFullName(type, prefs)}<{args}>"; - } - return BuildJSSpaceFullName(type, prefs); + var space = BuildJSSpace(type, prefs); + var name = TrimGeneric(type.Name); + var full = string.IsNullOrEmpty(space) ? name : $"{space}.{name}"; + if (!type.IsGenericType) return full; + EnterNullability(); + var args = string.Join(", ", type.GenericTypeArguments.Select(Build)); + return $"{full}<{args}>"; } private string BuildPrimitive (Type type) diff --git a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs index 428f1ec3..911814a4 100644 --- a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs @@ -30,16 +30,16 @@ public string Generate (string buildDir, string debugDir) export default { wasm: {{wasm}}, assemblies: [ - {{JoinLines(assemblies, 2, ",\n")}} + {{Fmt(assemblies, 2, ",\n")}} ], icu: [ - {{JoinLines(icu, 2, ",\n")}} + {{Fmt(icu, 2, ",\n")}} ], symbols: [ - {{JoinLines(symbols, 2, ",\n")}} + {{Fmt(symbols, 2, ",\n")}} ], pdb: [ - {{JoinLines(pdb, 2, ",\n")}} + {{Fmt(pdb, 2, ",\n")}} ], entryAssemblyName: "{{entryAssemblyName}}" }; diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index d6b6ab34..2cc82bae 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.93 + 0.8.0-alpha.151 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/src/imports.ts b/src/js/src/imports.ts index 49d05172..0f44ca48 100644 --- a/src/js/src/imports.ts +++ b/src/js/src/imports.ts @@ -1,11 +1,10 @@ import * as bindings from "./bindings.g"; -import { disposeInstance, disposeOnFinalize } from "./instances"; +import { instances } from "./instances"; import type { RuntimeAPI } from "./modules"; export function bindImports(runtime: RuntimeAPI) { runtime.setModuleImports("Bootsharp", { ...bindings, - disposeInstance, - disposeOnFinalize + instances }); } diff --git a/src/js/src/instances.ts b/src/js/src/instances.ts index 741d8129..758c1da7 100644 --- a/src/js/src/instances.ts +++ b/src/js/src/instances.ts @@ -1,43 +1,54 @@ import { exports } from "./exports"; -const finalizer = new FinalizationRegistry(finalizeInstance); -const idToInstance = new Map(); +const exportedFinalizer = new FinalizationRegistry(finalizeExported); +const exportedById = new Map>(); +const importedById = new Map(); +const idByImported = new Map(); +const onDisposeById = new Map void>(); const idPool = new Array(); let nextId = -2147483648; // Number.MIN_SAFE_INTEGER is below C#'s Int32.MinValue -/** Registers specified imported (JS -> C#) interop instance and associates it with unique ID. - * @param instance Interop instance to resolve ID for. - * @return Unique identifier of the registered instance. */ -export function registerInstance(instance: object): number { - const id = idPool.length > 0 ? idPool.pop()! : nextId++; - idToInstance.set(id, instance); - return id; -} - -/** Resolves registered imported (JS -> C#) interop instance from specified ID. - * @param id Unique identifier of the instance. */ -export function getInstance(id: number): object { - return idToInstance.get(id)!; -} - -/** Invoked from C# to notify that the imported (JS-> C #) interop instance is no longer - * used (eg, was garbage collected) and can be released on the JavaScript side as well. - * @param id Unique identifier of the disposed interop instance. */ -export function disposeInstance(id: number): void { - idToInstance.delete(id); - idPool.push(id); -} - -/** Registers specified exported (C# -> JS) instance to invoke disposal on the C# side - * when it's collected (finalized) by JavaScript runtime GC. - * @param instance Interop instance to register. - * @param id Unique identifier of the interop instance. */ -export function disposeOnFinalize(instance: object, id: number): void { - finalizer.register(instance, id); -} +export const instances = { + /** Invokes the specified factory to create and register an exported instance wrapper associated with the ID, + * unless an exported instance is already registered under the ID, in which case returns its wrapper. */ + export(id: number, factory: (id: number) => object): object { + const instance = exportedById.get(id)?.deref(); + if (instance != null) return instance; + const exported = factory(id); + exportedById.set(id, new WeakRef(exported)); + exportedFinalizer.register(exported, id); + return exported; + }, + /** Registers specified imported instance and associates it with a unique ID, unless it's already registered, + * in which case the ID of the registered instance is returned. */ + import(instance: object, factory?: (id: number) => () => void): number { + const registered = idByImported.get(instance); + if (registered !== undefined) return registered; + const id = idPool.length > 0 ? idPool.pop()! : nextId++; + importedById.set(id, instance); + idByImported.set(instance, id); + if (factory != null) onDisposeById.set(id, factory(id)); + return id; + }, + /** Returns a registered imported instance associated with the specified ID. */ + imported(id: number): object { + return importedById.get(id)!; + }, + /** Invoked from C# to notify that the imported (JS -> C#) instance is no longer used + * (eg, was garbage collected) and can be released on the JavaScript side as well. + * @param id Unique identifier of the disposed instance. */ + disposeImported(id: number): void { + idByImported.delete(importedById.get(id)!); + importedById.delete(id); + onDisposeById.get(id)?.(); + onDisposeById.delete(id); + idPool.push(id); + } +}; /* v8 ignore start -- @preserve */ // Uncoverable, as finalization in Node is not controllable. -function finalizeInstance(id: number) { +function finalizeExported(id: number) { + exportedById.delete(id); (<{ DisposeExportedInstance: (id: number) => void }>exports).DisposeExportedInstance(id); } /* v8 ignore stop -- @preserve */ diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs index c2e30668..24f22421 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedInstanced.cs @@ -4,7 +4,9 @@ namespace Test.Types; public class ExportedInstanced (string instanceArg) : IExportedInstanced { - public Record? Record { get; set; } + public event RecordChanged? OnRecordChanged; + + public Record? Record { get; set => OnRecordChanged?.Invoke(this, field = value); } public string GetInstanceArg () => instanceArg; diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs index 2a7c2601..e42ae1ee 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs @@ -1,10 +1,13 @@ +using System; using System.Threading.Tasks; namespace Test.Types; public class ExportedStatic : IExportedStatic { - public Record? Record { get; set; } + public event Action? OnRecordChanged; + + public Record? Record { get; set => OnRecordChanged?.Invoke(field = value); } public async Task GetInstanceAsync (string arg) { diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs index 0e1fb6eb..52e0d86a 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedInstanced.cs @@ -4,7 +4,10 @@ namespace Test.Types; public interface IExportedInstanced { + event RecordChanged OnRecordChanged; + Record? Record { get; set; } + 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/IExportedStatic.cs index 5bde3d25..c74374dd 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs @@ -1,9 +1,13 @@ +using System; using System.Threading.Tasks; namespace Test.Types; public interface IExportedStatic { + event Action OnRecordChanged; + Record? Record { get; set; } + Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs index 9a1ddb71..0d9b77fc 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedInstanced.cs @@ -4,7 +4,10 @@ namespace Test.Types; public interface IImportedInstanced { + event RecordChanged OnRecordChanged; + Record? Record { get; set; } + 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/IImportedStatic.cs index f6fa7912..dbec6f05 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs @@ -1,9 +1,13 @@ +using System; using System.Threading.Tasks; namespace Test.Types; public interface IImportedStatic { + event Action OnRecordChanged; + Record? Record { get; set; } - Task GetInstanceAsync (string arg); + + Task GetInstanceAsync (string arg); } diff --git a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs index 7786f071..b5572e26 100644 --- a/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs +++ b/src/js/test/cs/Test.Types/Interfaces/Interfaces.cs @@ -6,14 +6,17 @@ namespace Test.Types; public static class Interfaces { - [JSInvokable] + [Export] public static event Action? OnImportedStaticRecordEchoed; + [Export] public static event Action? OnImportedInstanceRecordEchoed; + + [Export] public static async Task GetImportedArgAndRecordIdAsync (Record record, string arg) { var instance = await GetImportedStatic().GetInstanceAsync(arg); return await instance.GetRecordIdAsync(record) + instance.GetInstanceArg(); } - [JSInvokable] + [Export] public static string GetImportedStaticRecordIdAndSet (Record record) { var imported = GetImportedStatic(); @@ -22,7 +25,7 @@ public static string GetImportedStaticRecordIdAndSet (Record record) return currentRecordId; } - [JSInvokable] + [Export] public static async Task GetImportedInstanceArgAndRecordIdAsync (Record record, string arg) { var instance = await GetImportedStatic().GetInstanceAsync(arg); @@ -31,7 +34,7 @@ public static async Task GetImportedInstanceArgAndRecordIdAsync (Record return instance.GetInstanceArg() + currentRecordId + instance.Record.Id; } - [JSInvokable] + [Export] public static async Task GetImportedArgsAndFinalize (string arg1, string arg2) { var imported = GetImportedStatic(); @@ -46,6 +49,37 @@ public static async Task GetImportedArgsAndFinalize (string arg1, stri return result; } + [Export] + public static Task EchoImportedStaticRecordEventAsync () + { + var imported = GetImportedStatic(); + var tcs = new TaskCompletionSource(); + imported.OnRecordChanged += Handle; + return tcs.Task; + + void Handle (Record? record) + { + imported.OnRecordChanged -= Handle; + OnImportedStaticRecordEchoed?.Invoke(record); + tcs.SetResult(); + } + } + + [Export] + public static Task EchoImportedInstanceRecordEventAsync (IImportedInstanced imported) + { + var tcs = new TaskCompletionSource(); + imported.OnRecordChanged += Handle; + return tcs.Task; + + void Handle (IImportedInstanced caller, Record? record) + { + imported.OnRecordChanged -= Handle; + OnImportedInstanceRecordEchoed?.Invoke(record?.Id); + tcs.SetResult(); + } + } + private static IImportedStatic GetImportedStatic () { return (IImportedStatic)Bootsharp.Interfaces.Imports[typeof(IImportedStatic)].Instance; diff --git a/src/js/test/cs/Test.Types/RecordChanged.cs b/src/js/test/cs/Test.Types/RecordChanged.cs new file mode 100644 index 00000000..03ab642c --- /dev/null +++ b/src/js/test/cs/Test.Types/RecordChanged.cs @@ -0,0 +1,3 @@ +namespace Test.Types; + +public delegate void RecordChanged (TCaller caller, Record? record); diff --git a/src/js/test/cs/Test.Types/Vehicle/Registry.cs b/src/js/test/cs/Test.Types/Vehicle/Registry.cs index a7d6b451..683e8691 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Registry.cs +++ b/src/js/test/cs/Test.Types/Vehicle/Registry.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -7,20 +8,17 @@ namespace Test.Types; public partial class Registry { + [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!; - [JSInvokable] - public static Registry EchoRegistry (Registry registry) => registry; - - [JSInvokable] - public static Vehicle?[]? EchoVehicles (Vehicle?[]? value) => value; + [Export] public static Registry EchoRegistry (Registry registry) => registry; + [Export] public static Vehicle?[]? EchoVehicles (Vehicle?[]? value) => value; + [Export] public static Record?[]? EchoRecords (Record?[]? value) => value; - [JSInvokable] - public static Record?[]? EchoRecords (Record?[]? value) => value; - - [JSInvokable] + [Export] public static float CountTotalSpeed () { var registry = Provider.GetRegistry(); @@ -28,32 +26,23 @@ public static float CountTotalSpeed () registry.Wheeled.Sum(t => t?.MaxSpeed ?? 0); } - [JSInvokable] + [Export] public static async Task> ConcatRegistriesAsync (IReadOnlyList registries) { await Task.Delay(1); return registries.Concat(Provider.GetRegistries()).ToArray(); } - [JSInvokable] - public static async Task> MapRegistriesAsync (IReadOnlyDictionary map) + [Export] + public static async Task> MapRegistriesAsync + (IReadOnlyDictionary map) { await Task.Delay(1); return map.Concat(Provider.GetRegistryMap()).ToDictionary(kv => kv.Key, kv => kv.Value); } - [JSInvokable] - public static Vehicle GetWithEmptyId () => new() { Id = "" }; - - [JSFunction] - public static partial Vehicle CreateVehicle (string id, float maxSpeed); - - [JSEvent] - public static partial void OnVehicleBroadcast (Vehicle vehicle); - - [JSInvokable] - public static Vehicle GetVehicle (string id, float maxSpeed) => CreateVehicle(id, maxSpeed); - - [JSInvokable] - public static void BroadcastVehicle (Vehicle vehicle) => OnVehicleBroadcast(vehicle); + [Export] public static void BroadcastVehicle (Vehicle? vehicle) => OnVehicleBroadcast?.Invoke(vehicle); + [Export] public static Vehicle GetVehicleWithEmptyId () => new() { Id = "" }; + [Export] public static Vehicle GetVehicle (string id, float maxSpeed) => CreateVehicle(id, maxSpeed); + [Import] public static partial Vehicle CreateVehicle (string id, float maxSpeed); } diff --git a/src/js/test/cs/Test/Event.cs b/src/js/test/cs/Test/Event.cs index c85e71d5..592190e3 100644 --- a/src/js/test/cs/Test/Event.cs +++ b/src/js/test/cs/Test/Event.cs @@ -1,19 +1,36 @@ +using System; +using System.Threading.Tasks; using Bootsharp; using Test.Types; namespace Test; +public delegate void VehicleEvent (byte num, Vehicle? vehicle, TrackType type); + public static partial class Event { - [JSInvokable] - public static void BroadcastEvent (string value) => OnEvent(value); + [Export] public static event VehicleEvent? OnVehicleEvent; + [Export] public static event Action? OnImportedEventEchoed; + [Import] public static event Action? OnImportedEvent; - [JSInvokable] - public static void BroadcastEventMultiple (byte num, Vehicle? vehicle, TrackType type) => OnEventMultiple(num, vehicle, type); + [Export] + public static void BroadcastVehicleEvent (byte num, Vehicle? vehicle, TrackType type) + { + OnVehicleEvent?.Invoke(num, vehicle, type); + } - [JSEvent] - public static partial string OnEvent (string value); + [Export] + public static Task EchoImportedEventAsync () + { + var tcs = new TaskCompletionSource(); + OnImportedEvent += Handle; + return tcs.Task; - [JSEvent] - public static partial string OnEventMultiple (byte num, Vehicle? vehicle, TrackType type); + void Handle (string value) + { + OnImportedEvent -= Handle; + OnImportedEventEchoed?.Invoke(value); + tcs.SetResult(); + } + } } diff --git a/src/js/test/cs/Test/Functions.cs b/src/js/test/cs/Test/Functions.cs index e0a71dfa..c229ff5c 100644 --- a/src/js/test/cs/Test/Functions.cs +++ b/src/js/test/cs/Test/Functions.cs @@ -6,71 +6,22 @@ namespace Test; public static partial class Functions { - [JSFunction] - public static partial void JSFunction (); - - [JSInvokable] - public static void InvokeJSFunction () => JSFunction(); - - [JSInvokable] - public static string EchoString () => GetString(); - - [JSFunction] - public static partial string GetString (); - - [JSInvokable] - public static async Task EchoStringAsync () - { - await Task.Delay(1); - return await GetStringAsync(); - } - - [JSFunction] - public static partial Task GetStringAsync (); - - [JSInvokable] - public static byte[] EchoBytes () => GetBytes(); - - [JSFunction] - public static partial byte[] GetBytes (); - - [JSInvokable] - public static async Task EchoBytesAsync (byte[] arr) - { - await Task.Delay(1); - return arr; - } - - [JSInvokable] - public static IList EchoColExprString (IList list) - { - return [..list]; - } - - [JSInvokable] - public static IReadOnlyList EchoColExprDouble (IReadOnlyList list) - { - return [..list]; - } - - [JSInvokable] - public static ICollection EchoColExprInt (ICollection list) - { - return [..list]; - } - - [JSInvokable] - public static IReadOnlyCollection EchoColExprByte (IReadOnlyCollection list) - { - return [..list]; - } - - [JSInvokable] - public static string[] EchoStringArray (string[] arr) => arr; - [JSInvokable] - public static double[] EchoDoubleArray (double[] arr) => arr; - [JSInvokable] - public static int[] EchoIntArray (int[] arr) => arr; - [JSInvokable] - public static byte[] EchoByteArray (byte[] arr) => arr; + [Import] public static partial string GetString (); + [Import] public static partial void JSFunction (); + [Import] public static partial Task GetStringAsync (); + [Import] public static partial byte[] GetBytes (); + + [Export] public static void InvokeJSFunction () => JSFunction(); + [Export] public static string EchoString () => GetString(); + [Export] public static Task EchoStringAsync () => Task.Delay(1).ContinueWith(_ => GetStringAsync()).Unwrap(); + [Export] public static byte[] EchoBytes () => GetBytes(); + [Export] public static Task EchoBytesAsync (byte[] arr) => Task.Delay(1).ContinueWith(_ => arr); + [Export] public static IList EchoColExprString (IList list) => [..list]; + [Export] public static IReadOnlyList EchoColExprDouble (IReadOnlyList list) => [..list]; + [Export] public static ICollection EchoColExprInt (ICollection list) => [..list]; + [Export] public static IReadOnlyCollection EchoColExprByte (IReadOnlyCollection list) => [..list]; + [Export] public static string[] EchoStringArray (string[] arr) => arr; + [Export] public static double[] EchoDoubleArray (double[] arr) => arr; + [Export] public static int[] EchoIntArray (int[] arr) => arr; + [Export] public static byte[] EchoByteArray (byte[] arr) => arr; } diff --git a/src/js/test/cs/Test/Invokable.cs b/src/js/test/cs/Test/Invokable.cs index 51011ea2..5641a0c5 100644 --- a/src/js/test/cs/Test/Invokable.cs +++ b/src/js/test/cs/Test/Invokable.cs @@ -5,35 +5,22 @@ namespace Test; -/// Invokable test API. +/// +/// Invokable test API. +/// public static class Invokable { - [JSInvokable] - public static void InvokeVoid () { } - - /// Joins two strings. + /// + /// Joins two strings. + /// /// First string. /// Second string. /// Joined string. - [JSInvokable] - public static string JoinStrings (string a, string b) => a + b; - - [JSInvokable] - public static double SumDoubles (double a, double b) => a + b; - - [JSInvokable] - public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); - - [JSInvokable] - public static async Task JoinStringsAsync (string a, string b) - { - await Task.Delay(1).ConfigureAwait(false); - return a + b; - } - - [JSInvokable] - public static string BytesToString (byte[] bytes) => Encoding.UTF8.GetString(bytes); - - [JSInvokable] - public static IdxEnum GetIdxEnumOne () => IdxEnum.One; + [Export] public static string JoinStrings (string a, string b) => a + b; + [Export] public static void InvokeVoid () { } + [Export] public static double SumDoubles (double a, double b) => a + b; + [Export] public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); + [Export] public static Task JoinStringsAsync (string a, string b) => Task.Delay(1).ContinueWith(_ => a + b); + [Export] public static string BytesToString (byte[] bytes) => Encoding.UTF8.GetString(bytes); + [Export] public static IdxEnum GetIdxEnumOne () => IdxEnum.One; } diff --git a/src/js/test/cs/Test/Platform.cs b/src/js/test/cs/Test/Platform.cs index cd87781b..a0870ec4 100644 --- a/src/js/test/cs/Test/Platform.cs +++ b/src/js/test/cs/Test/Platform.cs @@ -10,17 +10,17 @@ namespace Test; public static partial class Platform { - [JSInvokable] + [Export] public static string GetGuid () => Guid.NewGuid().ToString(); - [JSInvokable] + [Export] public static string FormatDate (string culture, int month, int day, string format) { var info = new CultureInfo(culture, false); return new DateTime(2024, month, day).ToString(format, info); } - [JSInvokable] + [Export] public static string? CatchException () { try { ThrowJS(); } @@ -28,13 +28,13 @@ public static string FormatDate (string culture, int month, int day, string form return null; } - [JSInvokable] + [Export] public static void ThrowCS (string message) => throw new Exception(message); - [JSFunction] + [Import] public static partial void ThrowJS (); - [JSInvokable] + [Export] public static async Task EchoWebSocket (string uri, string message, int timeout) { using var cts = new CancellationTokenSource(timeout); diff --git a/src/js/test/cs/Test/Program.cs b/src/js/test/cs/Test/Program.cs index 15d64a7d..24ebc03a 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: JSExport(typeof(IExportedStatic))] -[assembly: JSImport(typeof(IImportedStatic), typeof(IRegistryProvider))] +[assembly: Export(typeof(IExportedStatic))] +[assembly: Import(typeof(IImportedStatic), typeof(IRegistryProvider))] namespace Test; @@ -24,6 +24,6 @@ public static void Main () OnMainInvoked(); } - [JSFunction] + [Import] public static partial void OnMainInvoked (); } diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index d74bc954..7223cb11 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -55,25 +55,25 @@ public sealed record Computed public static class Serialization { - [JSInvokable] public static Primitives?[]? EchoPrimitives (Primitives?[]? value) => value; - [JSInvokable] public static Union?[]? EchoUnions (Union?[]? value) => value; - [JSInvokable] public static Computed EchoComputed (Computed value) => value; - [JSInvokable] public static Computed?[]? EchoComputedArray (Computed?[]? value) => value; - [JSInvokable] public static byte[]? EchoBytes (byte[]? value) => value; - [JSInvokable] public static int[]? EchoIntArray (int[]? value) => value; - [JSInvokable] public static double[]? EchoDoubleArray (double[]? value) => value; - [JSInvokable] public static string?[]? EchoStringArray (string?[]? value) => value; - [JSInvokable] public static int?[]? EchoNullableIntArray (int?[]? value) => value; - [JSInvokable] public static int[]?[]? EchoNestedIntArray (int[]?[]? value) => value; - [JSInvokable] public static List? EchoIntList (List? value) => value; - [JSInvokable] public static List? EchoStringList (List? value) => value; - [JSInvokable] public static List?[]? EchoNestedIntList (List?[]? value) => value; - [JSInvokable] public static Dictionary? EchoDictionary (Dictionary? value) => value; - [JSInvokable] public static Dictionary?[]? EchoNestedDictionary (Dictionary?[]? value) => value; - [JSInvokable] public static IList EchoListInterface (IList value) => value; - [JSInvokable] public static IReadOnlyList EchoReadOnlyList (IReadOnlyList value) => value; - [JSInvokable] public static ICollection EchoCollection (ICollection value) => value; - [JSInvokable] public static IReadOnlyCollection EchoReadOnlyCollection (IReadOnlyCollection value) => value; - [JSInvokable] public static IDictionary EchoDictionaryInterface (IDictionary value) => value; - [JSInvokable] public static IReadOnlyDictionary EchoReadOnlyDictionary (IReadOnlyDictionary value) => value; + [Export] public static Primitives?[]? EchoPrimitives (Primitives?[]? value) => value; + [Export] public static Union?[]? EchoUnions (Union?[]? value) => value; + [Export] public static Computed EchoComputed (Computed value) => value; + [Export] public static Computed?[]? EchoComputedArray (Computed?[]? value) => value; + [Export] public static byte[]? EchoBytes (byte[]? value) => value; + [Export] public static int[]? EchoIntArray (int[]? value) => value; + [Export] public static double[]? EchoDoubleArray (double[]? value) => value; + [Export] public static string?[]? EchoStringArray (string?[]? value) => value; + [Export] public static int?[]? EchoNullableIntArray (int?[]? value) => value; + [Export] public static int[]?[]? EchoNestedIntArray (int[]?[]? value) => value; + [Export] public static List? EchoIntList (List? value) => value; + [Export] public static List? EchoStringList (List? value) => value; + [Export] public static List?[]? EchoNestedIntList (List?[]? value) => value; + [Export] public static Dictionary? EchoDictionary (Dictionary? value) => value; + [Export] public static Dictionary?[]? EchoNestedDictionary (Dictionary?[]? value) => value; + [Export] public static IList EchoListInterface (IList value) => value; + [Export] public static IReadOnlyList EchoReadOnlyList (IReadOnlyList value) => value; + [Export] public static ICollection EchoCollection (ICollection value) => value; + [Export] public static IReadOnlyCollection EchoReadOnlyCollection (IReadOnlyCollection value) => value; + [Export] public static IDictionary EchoDictionaryInterface (IDictionary value) => value; + [Export] public static IReadOnlyDictionary EchoReadOnlyDictionary (IReadOnlyDictionary value) => value; } diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index a91fda77..fd39faab 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -1,8 +1,19 @@ -import { describe, it, beforeAll, expect } from "vitest"; -import { Test, bootSideload } from "../cs"; +import { describe, it, beforeAll, expect, vi } from "vitest"; +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]>(); + getInstanceArg() { return this.arg; } + async getRecordIdAsync(record: Test.Types.Record) { + await new Promise(res => setTimeout(res, 1)); + return record.id; + } +} + 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/); @@ -90,6 +101,12 @@ describe("while bootsharp is booted", () => { const actual = Test.Types.Registry.echoRegistry(expected); expect(actual).toStrictEqual(expected); }); + it("empty string of a struct is transferred correctly", () => { + const id = Test.Types.Registry.getVehicleWithEmptyId().id; + expect(id).not.toBeNull(); + expect(id).not.toBeUndefined(); + expect(id).toStrictEqual(""); + }); it("can transfer lists as arrays", async () => { Test.Types.RegistryProvider.getRegistries = () => [{ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], @@ -146,39 +163,41 @@ describe("while bootsharp is booted", () => { Test.Types.Registry.createVehicle = (id, maxSpeed) => ({ id, maxSpeed }); expect(Test.Types.Registry.getVehicle("foo", 42)).toStrictEqual({ id: "foo", maxSpeed: 42 }); }); - it("can subscribe to events", () => { - let eventArg1, multipleArg1, multipleArg2, multipleArg3; - Test.Event.onEvent.subscribe(v => eventArg1 = v); - Test.Event.onEventMultiple.subscribe((a1, a2, a3) => { - multipleArg1 = a1; - multipleArg2 = a2; - multipleArg3 = a3; - }); - Test.Event.broadcastEvent("foo"); - expect(eventArg1).toStrictEqual("foo"); - Test.Event.broadcastEventMultiple(1, { id: "foo", maxSpeed: 50 }, TrackType.Rubber); - expect(multipleArg1).toStrictEqual(1); - expect(multipleArg2).toStrictEqual({ id: "foo", maxSpeed: 50 }); - expect(multipleArg3).toStrictEqual(TrackType.Rubber); - Test.Event.broadcastEventMultiple(255, undefined, TrackType.Chain); - expect(multipleArg1).toStrictEqual(255); - expect(multipleArg2).toBeUndefined(); - expect(multipleArg3).toStrictEqual(TrackType.Chain); + it("can subscribe to exported events", () => { + const handler = vi.fn(); + Test.Event.onVehicleEvent.subscribe(handler); + Test.Event.broadcastVehicleEvent(1, { id: "foo", maxSpeed: 50 }, TrackType.Rubber); + expect(handler).toHaveBeenCalledWith(1, { id: "foo", maxSpeed: 50 }, TrackType.Rubber); + Test.Event.broadcastVehicleEvent(255, undefined, TrackType.Chain); + expect(handler).toHaveBeenCalledWith(255, undefined, TrackType.Chain); + }); + it("can broadcast imported events", async () => { + const handler = vi.fn(); + Test.Event.onImportedEventEchoed.subscribe(handler); + + const pending = Test.Event.echoImportedEventAsync(); + Test.Event.onImportedEvent.broadcast("imported"); + await pending; + + expect(handler).toHaveBeenCalledWith("imported"); + Test.Event.onImportedEventEchoed.unsubscribe(handler); }); it("can subscribe to events from library assembly", () => { - let result: Test.Types.Vehicle | undefined; - Test.Types.Registry.onVehicleBroadcast.subscribe(v => result = v); + const handler = vi.fn(); + Test.Types.Registry.onVehicleBroadcast.subscribe(handler); Test.Types.Registry.broadcastVehicle({ id: "foo", maxSpeed: 42 }); - expect(result).toStrictEqual({ id: "foo", maxSpeed: 42 }); + expect(handler).toHaveBeenCalledWith({ id: "foo", maxSpeed: 42 }); + Test.Types.Registry.broadcastVehicle(undefined); + expect(handler).toHaveBeenCalledWith(undefined); }); it("can un-subscribe from events", () => { - let result = ""; - const assigner = (v: string) => result = v; - Test.Event.onEvent.subscribe(assigner); - Test.Event.broadcastEvent("foo"); - Test.Event.onEvent.unsubscribe(assigner); - Test.Event.broadcastEvent("bar"); - expect(result).toStrictEqual("foo"); + const handler = vi.fn(); + Test.Event.onVehicleEvent.subscribe(handler); + Test.Event.broadcastVehicleEvent(0, undefined, TrackType.Chain); + Test.Event.onVehicleEvent.unsubscribe(handler); + Test.Event.broadcastVehicleEvent(1, undefined, TrackType.Chain); + expect(handler).toHaveBeenCalledWith(0, undefined, TrackType.Chain); + expect(handler).not.toHaveBeenCalledWith(1, undefined, TrackType.Chain); }); it("can catch js exception", () => { Test.Platform.throwJS = function () { throw new Error("foo"); }; @@ -204,17 +223,25 @@ 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 interfaces", async () => { - class Imported { - constructor(private arg: string) { } - record: Test.Types.Record = { id: "foo" }; - getInstanceArg() { return this.arg; } - async getRecordIdAsync(record: Test.Types.Record) { - await new Promise(res => setTimeout(res, 1)); - return record.id; - } - } + 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(); + const handler = vi.fn(); + Test.Types.Interfaces.onImportedStaticRecordEchoed.subscribe(handler); + let echo = Test.Types.Interfaces.echoImportedStaticRecordEventAsync(); + Test.Types.ImportedStatic.onRecordChanged.broadcast({ id: "static" }); + await echo; + expect(handler).toHaveBeenCalledWith({ id: "static" }); + echo = Test.Types.Interfaces.echoImportedStaticRecordEventAsync(); + Test.Types.ImportedStatic.onRecordChanged.broadcast(undefined); + await echo; + expect(handler).toHaveBeenCalledWith(undefined); + Test.Types.Interfaces.onImportedStaticRecordEchoed.unsubscribe(handler); + }); + it("can interop with imported interface instances", async () => { Test.Types.ImportedStatic.getInstanceAsync = async (arg) => { await new Promise(res => setTimeout(res, 1)); return new Imported(arg); @@ -223,45 +250,54 @@ describe("while bootsharp is booted", () => { const result2 = await Test.Types.Interfaces.getImportedArgAndRecordIdAsync({ id: "baz" }, "nya"); expect(result1).toStrictEqual("foobar"); expect(result2).toStrictEqual("baznya"); - expect(Test.Types.Interfaces.getImportedStaticRecordIdAndSet({ id: "qux" })).toStrictEqual("baz"); - expect(Test.Types.ImportedStatic.record).toStrictEqual({ id: "qux" }); expect(await Test.Types.Interfaces.getImportedInstanceArgAndRecordIdAsync({ id: "zip" }, "qux")) .toStrictEqual("quxfoozip"); + const imported = new Imported("evt"); + const handler = vi.fn(); + Test.Types.Interfaces.onImportedInstanceRecordEchoed.subscribe(handler); + let echo = Test.Types.Interfaces.echoImportedInstanceRecordEventAsync(imported); + imported.onRecordChanged.broadcast(imported, { id: "instance" }); + await echo; + expect(handler).toHaveBeenCalledWith("instance"); + echo = Test.Types.Interfaces.echoImportedInstanceRecordEventAsync(imported); + imported.onRecordChanged.broadcast(imported, undefined); + await echo; + expect(handler).toHaveBeenCalledWith(undefined); + Test.Types.Interfaces.onImportedInstanceRecordEchoed.unsubscribe(handler); }); - it("can interop with exported interfaces", async () => { + 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" }); - Test.Types.ExportedStatic.record = null; - expect(Test.Types.ExportedStatic.record).toBeNull(); - + 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"); + const handler = vi.fn(); expect(exported.getInstanceArg()).toStrictEqual("bar"); expect(await exported.getRecordIdAsync({ id: "foo" })).toStrictEqual("foo"); - expect(exported.record).toBeNull(); + expect(exported.record).toBeUndefined(); + exported.onRecordChanged.subscribe(handler); exported.record = { id: "qux" }; expect(exported.record).toStrictEqual({ id: "qux" }); + expect(handler).toHaveBeenCalledWith(exported, { id: "qux" }); + exported.record = undefined; + expect(exported.record).toBeUndefined(); + expect(handler).toHaveBeenCalledWith(exported, undefined); + exported.onRecordChanged.unsubscribe(handler); }); it("releases interface instances after use", async () => { - class Imported { - constructor(private arg: string) { } - record?: Test.Types.Record; - getInstanceArg() { return this.arg; } - async getRecordIdAsync(record: Test.Types.Record) { - await new Promise(res => setTimeout(res, 1)); - return record.id; - } - } Test.Types.ImportedStatic.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"]); }); - it("empty string of a struct is transferred correctly", () => { - const id = Test.Types.Registry.getWithEmptyId().id; - expect(id).not.toBeNull(); - expect(id).not.toBeUndefined(); - expect(id).toStrictEqual(""); - }); }); From 0749e5ce255a9ebecc5169b1af6c6d28c9853c22 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:02:08 +0300 Subject: [PATCH 03/10] fmt --- src/cs/Bootsharp.Common/Interop/Instances.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Bootsharp.Common/Interop/Instances.cs b/src/cs/Bootsharp.Common/Interop/Instances.cs index 770f3fdd..659444d6 100644 --- a/src/cs/Bootsharp.Common/Interop/Instances.cs +++ b/src/cs/Bootsharp.Common/Interop/Instances.cs @@ -44,7 +44,7 @@ public static T Exported (int id) where T : class /// /// Invokes the specified factory to create and register an imported instance wrapper associated with the ID, - /// unless an imported instance is already registered under the ID, in which case returns its wrapper. + /// unless an imported instance is already registered under the ID, in which case returns its wrapper. /// public static T Import (int id, Func factory) where T : class { From 41ff668e1414bf5edf9973f63c90ceddecaa5a2a Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Wed, 29 Apr 2026 22:15:09 +0300 Subject: [PATCH 04/10] stable dll order --- src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs | 2 +- .../Bootsharp.Publish/Common/Global/GlobalInspection.cs | 5 +++-- src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs | 2 +- src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs | 8 +++++--- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs index 348cb6e5..f3ee2472 100644 --- a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs +++ b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs @@ -49,7 +49,7 @@ private static CSharpCompilation CreateCompilation (string assemblyPath, string private static PortableExecutableReference[] GatherReferences (string directory) { - var paths = Directory.GetFiles(directory, "*.dll"); + var paths = Directory.GetFiles(directory, "*.dll").Order(); return paths.Select(p => MetadataReference.CreateFromFile(p)).ToArray(); } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 18f1dd09..f3727bfa 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -10,8 +10,9 @@ internal static class GlobalInspection { public static MetadataLoadContext CreateLoadContext (string directory) { - var assemblyPaths = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll").ToList(); - foreach (var path in Directory.GetFiles(directory, "*.dll")) + var runtimeDir = RuntimeEnvironment.GetRuntimeDirectory(); + var assemblyPaths = Directory.GetFiles(runtimeDir, "*.dll").Order().ToList(); + foreach (var path in Directory.GetFiles(directory, "*.dll").Order()) if (assemblyPaths.All(p => Path.GetFileName(p) != Path.GetFileName(path))) assemblyPaths.Add(path); var resolver = new PathAssemblyResolver(assemblyPaths); diff --git a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs index 520891d7..6c6fb344 100644 --- a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs +++ b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs @@ -32,7 +32,7 @@ private Preferences ResolvePreferences () private SolutionInspection InspectSolution (Preferences prefs) { var inspector = new SolutionInspector(prefs, EntryAssemblyName); - var inspected = Directory.GetFiles(InspectedDirectory, "*.dll"); + var inspected = Directory.GetFiles(InspectedDirectory, "*.dll").Order(); var inspection = inspector.Inspect(InspectedDirectory, inspected); new InspectionReporter(Log).Report(inspection); return inspection; diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs index baec7ecc..cd8c0da9 100644 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs @@ -41,11 +41,13 @@ private SolutionInspection InspectSolution (Preferences prefs) IEnumerable GetFiles () { - if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll"); + if (LLVM) return Directory.GetFiles(InspectedDirectory, "*.dll").Order(); // Assemblies in publish dir are trimmed and don't contain some data (eg, method arg names). // While the inspected dir contains extra assemblies we don't need in build. Hence the filtering. - var included = Directory.GetFiles(BuildDirectory, "*.wasm").Select(Path.GetFileNameWithoutExtension).ToHashSet(); - return Directory.GetFiles(InspectedDirectory, "*.dll").Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); + var included = Directory.GetFiles(BuildDirectory, "*.wasm") + .Select(Path.GetFileNameWithoutExtension).ToHashSet(); + return Directory.GetFiles(InspectedDirectory, "*.dll").Order() + .Where(p => included.Contains(Path.GetFileNameWithoutExtension(p))); } } From b137dc2e608383e4bab2a758a556b16039307cfc Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:17:43 +0300 Subject: [PATCH 05/10] fix nested delegates --- .../Emit/InterfacesTest.cs | 18 ++++++++++++++++++ .../Bootsharp.Publish.Test/Pack/BindingTest.cs | 2 +- .../Common/Global/GlobalType.cs | 1 + src/cs/Directory.Build.props | 2 +- .../cs/Test.Types/Interfaces/ExportedStatic.cs | 3 +-- .../Test.Types/Interfaces/IExportedStatic.cs | 5 +++-- .../Test.Types/Interfaces/IImportedStatic.cs | 5 +++-- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs index 894c4a68..f41ee662 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs @@ -15,7 +15,10 @@ public record Record; public interface IExported { + delegate void SomethingChanged(); + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; Record? Record { get; set; } @@ -51,9 +54,11 @@ public JSExported (global::IExported handler) { JSExported.handler = handler; handler.OnRecordChanged += OnRecordChanged.Invoke; + handler.OnSomethingChanged += OnSomethingChanged.Invoke; } [Export] public static event global::System.Action OnRecordChanged; + [Export] public static event global::IExported.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); @@ -77,7 +82,10 @@ public record Record; public interface IImported { + delegate void SomethingChanged(); + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; Record? Record { get; set; } @@ -109,6 +117,8 @@ 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(); @@ -133,7 +143,10 @@ public record Record; public interface IImported { + delegate void SomethingChanged(); + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; Record? Record { get; set; } @@ -162,6 +175,8 @@ public class JSImported (global::System.Int32 id) : 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); @@ -182,7 +197,10 @@ public record Record; public interface IExported { + delegate void SomethingChanged(); + event Action OnRecordChanged; + event SomethingChanged OnSomethingChanged; Record? Record { get; set; } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 3228b2e0..772399e2 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -465,7 +465,7 @@ public void ExportedEnumsAreDeclaredInJS () """ export const n = { Class: { - getFoo: () => deserialize(exports.n_Class_GetFoo(), n_Foo), + getFoo: () => deserialize(exports.n_Class_GetFoo(), n_Class_Foo), Foo: { "0": "A", "1": "B", "A": 0, "B": 1 } } }; diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index b526d2ae..faf5f9d1 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -120,6 +120,7 @@ string BuildGeneric (Type type, Type[] args) static string ResolveTypeName (Type type) { + if (type.IsNested) return $"{ResolveTypeName(type.DeclaringType!)}.{type.Name}"; if (type.Namespace is null) return type.Name; return $"{type.Namespace}.{type.Name}"; } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 2cc82bae..459541eb 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.151 + 0.8.0-alpha.152 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs index e42ae1ee..c9eebc73 100644 --- a/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/ExportedStatic.cs @@ -1,11 +1,10 @@ -using System; using System.Threading.Tasks; namespace Test.Types; public class ExportedStatic : IExportedStatic { - public event Action? OnRecordChanged; + public event IExportedStatic.RecordChanged? OnRecordChanged; public Record? Record { get; set => OnRecordChanged?.Invoke(field = value); } diff --git a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs index c74374dd..67a526d5 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IExportedStatic.cs @@ -1,11 +1,12 @@ -using System; using System.Threading.Tasks; namespace Test.Types; public interface IExportedStatic { - event Action OnRecordChanged; + delegate void RecordChanged (Record? record); + + event RecordChanged OnRecordChanged; Record? Record { get; set; } diff --git a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs b/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs index dbec6f05..9fdccc22 100644 --- a/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs +++ b/src/js/test/cs/Test.Types/Interfaces/IImportedStatic.cs @@ -1,11 +1,12 @@ -using System; using System.Threading.Tasks; namespace Test.Types; public interface IImportedStatic { - event Action OnRecordChanged; + delegate void RecordChanged (Record? record); + + event RecordChanged OnRecordChanged; Record? Record { get; set; } From ef23a52ed66be29b13195ad21b9fe5d6c5ceb36a Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 1 May 2026 17:08:20 +0300 Subject: [PATCH 06/10] trim deps --- .../Emit/DependencyGenerator.cs | 30 ++++++++++++------- src/cs/Directory.Build.props | 2 +- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs index c374d76d..1183a2e3 100644 --- a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs @@ -1,6 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; - namespace Bootsharp.Publish; /// @@ -24,6 +21,19 @@ namespace Bootsharp.Generated; public static class Dependencies { + private const DynamicallyAccessedMemberTypes types = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.PublicFields | + DynamicallyAccessedMemberTypes.NonPublicFields | + DynamicallyAccessedMemberTypes.PublicNestedTypes | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicProperties | + DynamicallyAccessedMemberTypes.NonPublicProperties | + DynamicallyAccessedMemberTypes.PublicEvents | + DynamicallyAccessedMemberTypes.NonPublicEvents | + DynamicallyAccessedMemberTypes.Interfaces; + [System.Runtime.CompilerServices.ModuleInitializer] {{Fmt(added)}} internal static void RegisterDynamicDependencies () { } @@ -33,28 +43,28 @@ internal static void RegisterDynamicDependencies () { } private void AddGeneratedCommon () { - Add(All, "Bootsharp.Generated.Dependencies", entryAssembly); - Add(All, "Bootsharp.Generated.Interop", entryAssembly); + Add("Bootsharp.Generated.Dependencies", entryAssembly); + Add("Bootsharp.Generated.Interop", entryAssembly); } private void AddGeneratedInteropClasses (SolutionInspection inspection) { foreach (var it in inspection.StaticInterfaces) - Add(All, it.FullName, entryAssembly); + Add(it.FullName, entryAssembly); foreach (var it in inspection.InstancedInterfaces) if (it.Interop == InteropKind.Import) - Add(All, it.FullName, entryAssembly); + Add(it.FullName, entryAssembly); } private void AddClassesWithInteropMethods (SolutionInspection inspection) { foreach (var member in inspection.StaticMembers) - Add(All, member.Space, member.Assembly); + Add(member.Space, member.Assembly); } - private void Add (DynamicallyAccessedMemberTypes types, string name, string assembly) + private void Add (string name, string assembly) { var asm = assembly.EndsWith(".dll", StringComparison.Ordinal) ? assembly[..^4] : assembly; - added.Add($"""[DynamicDependency(DynamicallyAccessedMemberTypes.{types}, "{name}", "{asm}")]"""); + added.Add($"""[DynamicDependency(types, "{name}", "{asm}")]"""); } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 459541eb..e24c9e55 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.152 + 0.8.0-alpha.154 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com From 6c95186429b95e5ead0eabb60f4fd40be68ffac0 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 1 May 2026 17:08:34 +0300 Subject: [PATCH 07/10] update cover script --- src/cs/.scripts/cover.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/cs/.scripts/cover.sh b/src/cs/.scripts/cover.sh index fe24fd9d..d2bcef62 100644 --- a/src/cs/.scripts/cover.sh +++ b/src/cs/.scripts/cover.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash set -e -trap 'rm -rf .cover' EXIT - out="../.cover/" json="../.cover/coverage.json" +url="http://localhost:3000" + +trap 'kill "$serve_pid" 2>/dev/null || true; rm -rf .cover' EXIT dotnet test Bootsharp.Common.Test/Bootsharp.Common.Test.csproj -p:CollectCoverage=true -p:ExcludeByAttribute=GeneratedCodeAttribute -p:CoverletOutput="$out" dotnet test Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj -p:CollectCoverage=true -p:ExcludeByAttribute=GeneratedCodeAttribute -p:CoverletOutput="$out" -p:MergeWith="$json" @@ -12,5 +13,14 @@ dotnet test Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj -p:CollectCoverag dotnet test Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj -p:CollectCoverage=true -p:CoverletOutputFormat='json%2copencover' -p:ExcludeByAttribute=GeneratedCodeAttribute -p:CoverletOutput="$out" -p:MergeWith="$json" reportgenerator '-reports:*/*.xml' '-targetdir:.cover' -reporttypes:HTML -python -m webbrowser http://localhost:3000 -npx serve .cover + +serve .cover -l 3000 & +serve_pid=$! + +until curl -fsS "$url" >/dev/null; do + sleep 0.1 +done + +python -m webbrowser "$url" + +wait "$serve_pid" From 117f1b9ea67cc1537c61fe3baffbdc6f64806a7c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 1 May 2026 17:08:44 +0300 Subject: [PATCH 08/10] update deps --- .../Bootsharp.Common.Test.csproj | 6 +-- .../Bootsharp.Generate.Test.csproj | 8 ++-- .../Bootsharp.Generate.csproj | 2 +- .../Bootsharp.Inject.Test.csproj | 8 ++-- .../Bootsharp.Inject/Bootsharp.Inject.csproj | 2 +- .../Bootsharp.Publish.Test.csproj | 10 ++--- .../Emit/DependenciesTest.cs | 44 +++++++------------ .../Bootsharp.Publish.csproj | 6 +-- 8 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj b/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj index c261af11..6fc7948d 100644 --- a/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj +++ b/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj @@ -11,17 +11,17 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj index 00d1bd20..03385704 100644 --- a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj +++ b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj @@ -11,19 +11,19 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj b/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj index 562f9aa0..33242040 100644 --- a/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj +++ b/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj index 20182bad..931c520c 100644 --- a/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj +++ b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj @@ -12,18 +12,18 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj b/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj index 35cd1fdc..8eaf402f 100644 --- a/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj +++ b/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj b/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj index e1249e0b..8039f798 100644 --- a/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj +++ b/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj @@ -11,20 +11,20 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs index ae7b9573..3b0c3f2d 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs @@ -1,6 +1,3 @@ -using System.Diagnostics.CodeAnalysis; -using static System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes; - namespace Bootsharp.Publish.Test; public class DependenciesTest : EmitTest @@ -13,17 +10,10 @@ public void AddsCommonDependencies () Execute(); Contains( """ - using System.Diagnostics.CodeAnalysis; - - namespace Bootsharp.Generated; - - public static class Dependencies - { [System.Runtime.CompilerServices.ModuleInitializer] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Bootsharp.Generated.Dependencies", "System.Runtime")] - [DynamicDependency(DynamicallyAccessedMemberTypes.All, "Bootsharp.Generated.Interop", "System.Runtime")] + [DynamicDependency(types, "Bootsharp.Generated.Dependencies", "System.Runtime")] + [DynamicDependency(types, "Bootsharp.Generated.Interop", "System.Runtime")] internal static void RegisterDynamicDependencies () { } - } """); } @@ -38,10 +28,10 @@ public void AddsStaticInterfaceImplementations () With("Space", "public interface IExported {}"), With("Space", "public interface IImported {}")); Execute(); - Added(All, "Bootsharp.Generated.Exports.JSExported"); - Added(All, "Bootsharp.Generated.Exports.Space.JSExported"); - Added(All, "Bootsharp.Generated.Imports.JSImported"); - Added(All, "Bootsharp.Generated.Imports.Space.JSImported"); + Added("Bootsharp.Generated.Exports.JSExported"); + Added("Bootsharp.Generated.Exports.Space.JSExported"); + Added("Bootsharp.Generated.Imports.JSImported"); + Added("Bootsharp.Generated.Imports.Space.JSImported"); } [Fact] @@ -67,10 +57,10 @@ public class Class } """)); Execute(); - Added(All, "Bootsharp.Generated.Exports.JSExportedStatic"); - Added(All, "Bootsharp.Generated.Imports.JSImportedStatic"); - Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedA"); - Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedB"); + Added("Bootsharp.Generated.Exports.JSExportedStatic"); + Added("Bootsharp.Generated.Imports.JSImportedStatic"); + Added("Bootsharp.Generated.Imports.JSImportedInstancedA"); + Added("Bootsharp.Generated.Imports.JSImportedInstancedB"); // Export interop instances are not generated in C#; they're authored by user. DoesNotContain("Bootsharp.Generated.Exports.JSExportedInstanced"); } @@ -83,16 +73,16 @@ public void AddsClassesWithStaticInteropMembers () With("SpaceB.SpaceC", "public class ClassB { [Import] public static void Foo () {} }"), With("public class ClassC { [Export] public static event Action? Evt; }")); Execute(); - Added(All, "SpaceA.ClassA", "Assembly.With.Dots"); - Added(All, "SpaceB.SpaceC.ClassB", "Assembly.With.Dots"); - Added(All, "ClassC", "Assembly.With.Dots"); + Added("SpaceA.ClassA", "Assembly.With.Dots"); + Added("SpaceB.SpaceC.ClassB", "Assembly.With.Dots"); + Added("ClassC", "Assembly.With.Dots"); } - private void Added (DynamicallyAccessedMemberTypes types, string name) => - Added(types, name, Path.GetFileNameWithoutExtension(Task.EntryAssemblyName)); + private void Added (string name) => + Added(name, Path.GetFileNameWithoutExtension(Task.EntryAssemblyName)); - private void Added (DynamicallyAccessedMemberTypes types, string name, string assembly) + private void Added (string name, string assembly) { - Contains($"""[DynamicDependency(DynamicallyAccessedMemberTypes.{types}, "{name}", "{assembly}")]"""); + Contains($"""[DynamicDependency(types, "{name}", "{assembly}")]"""); } } diff --git a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj index 356758d3..ac917327 100644 --- a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj +++ b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj @@ -9,9 +9,9 @@ - - - + + + From 2b74bf20f3784ecdde24d2e8a29a1afe349f3fc3 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 1 May 2026 17:13:20 +0300 Subject: [PATCH 09/10] fix sg test --- src/cs/Bootsharp.Generate.Test/GeneratorTest.cs | 2 +- src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs b/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs index 22f8ae40..98d71a2b 100644 --- a/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs +++ b/src/cs/Bootsharp.Generate.Test/GeneratorTest.cs @@ -87,7 +87,7 @@ private async Task Verify (string source, params (string file, string content)[] { IncludeCommonExpected(ref expected[i].content); verifier.TestState.GeneratedSources.Add((typeof(SourceGenerator), expected[i].file, - SourceText.From(expected[i].content, Encoding.UTF8))); + SourceText.From(expected[i].content, Encoding.UTF8, SourceHashAlgorithm.Sha256))); } await verifier.RunAsync(); } diff --git a/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj b/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj index 33242040..562f9aa0 100644 --- a/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj +++ b/src/cs/Bootsharp.Generate/Bootsharp.Generate.csproj @@ -9,7 +9,7 @@ - + From 4e25b68a2ae102c48d1fb28e4360f60abfe76e4f Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 1 May 2026 17:55:41 +0300 Subject: [PATCH 10/10] remove dyn deps --- samples/trimming/README.md | 11 +-- .../ImportMethodTest.cs | 16 ++-- src/cs/Bootsharp.Generate/ImportMethod.cs | 2 +- .../Emit/DependenciesTest.cs | 88 ------------------- .../Bootsharp.Publish.Test/Emit/EmitTest.cs | 3 - .../Emit/InteropTest.cs | 13 +-- .../Bootsharp.Publish/Emit/BootsharpEmit.cs | 9 -- .../Emit/DependencyGenerator.cs | 70 --------------- .../Emit/InteropGenerator.cs | 2 +- .../Emit/InteropInitializerGenerator.cs | 16 +--- src/cs/Bootsharp/Build/Bootsharp.targets | 4 - src/cs/Directory.Build.props | 2 +- 12 files changed, 22 insertions(+), 214 deletions(-) delete mode 100644 src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs delete mode 100644 src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs diff --git a/samples/trimming/README.md b/samples/trimming/README.md index 7aa1686d..fae98506 100644 --- a/samples/trimming/README.md +++ b/samples/trimming/README.md @@ -1,13 +1,14 @@ Example on producing minimal possible build size by disabling binaries embedding and utilizing aggressive trimming. To test and measure build size: + - Run `dotnet publish cs`; - Run `node main.mjs`. ### Measurements (KB) -| | Raw | Brotli | -|--------------|-------|--------| -| .NET 8 | 2,298 | 739 | -| .NET 9 LLVM | 1,737 | 518 | -| .NET 10 LLVM | 1,610 | 482 | +| Bootsharp | Raw | Brotli | +|-------------|-------|--------| +| 0.1 .NET 8 | 2,298 | 739 | +| 0.7 .NET 9 | 1,737 | 518 | +| 0.8 .NET 10 | 1,610 | 482 | diff --git a/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs b/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs index 215a3b13..8617cc85 100644 --- a/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs +++ b/src/cs/Bootsharp.Generate.Test/ImportMethodTest.cs @@ -14,7 +14,7 @@ partial class Foo """ unsafe partial class Foo { - private static delegate* managed Bootsharp_Bar; + public static delegate* managed Bootsharp_Bar; partial void Bar () => Bootsharp_Bar(); } """ @@ -38,7 +38,7 @@ namespace File.Scoped; public static unsafe partial class Foo { - private static delegate* managed Bootsharp_BarAsync; + public static delegate* managed Bootsharp_BarAsync; private static partial global::System.Threading.Tasks.Task BarAsync (global::System.String[] a, global::System.Int32? b) => Bootsharp_BarAsync(a, b); } """ @@ -62,7 +62,7 @@ namespace File.Scoped; public static unsafe partial class Foo { - private static delegate* managed> Bootsharp_BarAsync; + public static delegate* managed> Bootsharp_BarAsync; private static partial global::System.Threading.Tasks.Task BarAsync () => Bootsharp_BarAsync(); } """ @@ -80,7 +80,7 @@ partial class Foo """ unsafe partial class Foo { - private static delegate* managed Bootsharp_Bar; + public static delegate* managed Bootsharp_Bar; partial void Bar (global::Record a) => Bootsharp_Bar(a); } """ @@ -108,9 +108,9 @@ namespace Classic { unsafe partial class Foo { - private static delegate* managed Bootsharp_GetTime; + public static delegate* managed Bootsharp_GetTime; public partial global::System.DateTime GetTime (global::System.DateTime time) => Bootsharp_GetTime(time); - private static delegate* managed> Bootsharp_GetTimeAsync; + public static delegate* managed> Bootsharp_GetTimeAsync; public partial global::System.Threading.Tasks.Task GetTimeAsync (global::System.DateTime time) => Bootsharp_GetTimeAsync(time); } } @@ -131,7 +131,7 @@ partial class Foo unsafe partial class Foo { - private static delegate* managed Bootsharp_Bar; + public static delegate* managed Bootsharp_Bar; partial void Bar () => Bootsharp_Bar(); } """ @@ -147,7 +147,7 @@ unsafe partial class Foo """ unsafe partial class Foo { - private static delegate* managed Bootsharp_Bar; + public static delegate* managed Bootsharp_Bar; partial void Bar () => Bootsharp_Bar(); } """ diff --git a/src/cs/Bootsharp.Generate/ImportMethod.cs b/src/cs/Bootsharp.Generate/ImportMethod.cs index 2d204da4..c0849fcf 100644 --- a/src/cs/Bootsharp.Generate/ImportMethod.cs +++ b/src/cs/Bootsharp.Generate/ImportMethod.cs @@ -12,7 +12,7 @@ public string EmitSource (Compilation cmp) { method = cmp.GetSemanticModel(stx.SyntaxTree).GetDeclaredSymbol(stx)!; return $""" - private static delegate* managed<{EmitPointerSignature()}> Bootsharp_{method.Name}; + public static delegate* managed<{EmitPointerSignature()}> Bootsharp_{method.Name}; {stx.Modifiers} {EmitMethodSignature()} => Bootsharp_{method.Name}({EmitArgs()}); """; } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs deleted file mode 100644 index 3b0c3f2d..00000000 --- a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace Bootsharp.Publish.Test; - -public class DependenciesTest : EmitTest -{ - protected override string TestedContent => GeneratedDependencies; - - [Fact] - public void AddsCommonDependencies () - { - Execute(); - Contains( - """ - [System.Runtime.CompilerServices.ModuleInitializer] - [DynamicDependency(types, "Bootsharp.Generated.Dependencies", "System.Runtime")] - [DynamicDependency(types, "Bootsharp.Generated.Interop", "System.Runtime")] - internal static void RegisterDynamicDependencies () { } - """); - } - - [Fact] - public void AddsStaticInterfaceImplementations () - { - AddAssembly( - With("[assembly:Export(typeof(IExported), typeof(Space.IExported))]"), - With("[assembly:Import(typeof(IImported), typeof(Space.IImported))]"), - With("public interface IExported {}"), - With("public interface IImported {}"), - With("Space", "public interface IExported {}"), - With("Space", "public interface IImported {}")); - Execute(); - Added("Bootsharp.Generated.Exports.JSExported"); - Added("Bootsharp.Generated.Exports.Space.JSExported"); - Added("Bootsharp.Generated.Imports.JSImported"); - Added("Bootsharp.Generated.Imports.Space.JSImported"); - } - - [Fact] - public void AddsInstancedInterfaceImplementations () - { - AddAssembly(With( - """ - [assembly:Export(typeof(IExportedStatic))] - [assembly:Import(typeof(IImportedStatic))] - - public interface IExportedStatic { IExportedInstancedA CreateExported (); } - public interface IImportedStatic { IImportedInstancedA CreateImported (); } - - public interface IExportedInstancedA { } - public interface IExportedInstancedB { } - public interface IImportedInstancedA { } - public interface IImportedInstancedB { } - - public class Class - { - [Export] public static IExportedInstancedB CreateExported () => default; - [Import] public static IImportedInstancedB CreateImported () => default; - } - """)); - Execute(); - Added("Bootsharp.Generated.Exports.JSExportedStatic"); - Added("Bootsharp.Generated.Imports.JSImportedStatic"); - Added("Bootsharp.Generated.Imports.JSImportedInstancedA"); - Added("Bootsharp.Generated.Imports.JSImportedInstancedB"); - // Export interop instances are not generated in C#; they're authored by user. - DoesNotContain("Bootsharp.Generated.Exports.JSExportedInstanced"); - } - - [Fact] - public void AddsClassesWithStaticInteropMembers () - { - AddAssembly("Assembly.With.Dots.dll", - With("SpaceA", "public class ClassA { [Export] public static void Foo () {} }"), - With("SpaceB.SpaceC", "public class ClassB { [Import] public static void Foo () {} }"), - With("public class ClassC { [Export] public static event Action? Evt; }")); - Execute(); - Added("SpaceA.ClassA", "Assembly.With.Dots"); - Added("SpaceB.SpaceC.ClassB", "Assembly.With.Dots"); - Added("ClassC", "Assembly.With.Dots"); - } - - private void Added (string name) => - Added(name, Path.GetFileNameWithoutExtension(Task.EntryAssemblyName)); - - private void Added (string name, string assembly) - { - Contains($"""[DynamicDependency(types, "{name}", "{assembly}")]"""); - } -} diff --git a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs index 2b089686..d02cc70d 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/EmitTest.cs @@ -4,12 +4,10 @@ public class EmitTest : TaskTest { protected BootsharpEmit Task { get; } protected string GeneratedInterfaces => ReadProjectFile(interfacesPath); - protected string GeneratedDependencies => ReadProjectFile(dependenciesPath); protected string GeneratedSerializer => ReadProjectFile(serializerPath); protected string GeneratedInterop => ReadProjectFile(interopPath); private string interfacesPath => $"{Project.Root}/Interfaces.g.cs"; - private string dependenciesPath => $"{Project.Root}/Dependencies.g.cs"; private string serializerPath => $"{Project.Root}/Serializer.g.cs"; private string interopPath => $"{Project.Root}/Interop.g.cs"; @@ -29,7 +27,6 @@ public override void Execute () InspectedDirectory = Project.Root, EntryAssemblyName = "System.Runtime.dll", InterfacesFilePath = interfacesPath, - DependenciesFilePath = dependenciesPath, SerializerFilePath = serializerPath, InteropFilePath = interopPath, BuildEngine = Engine diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index 61cca39c..3a65a631 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -35,7 +35,7 @@ public static class Registry { [Export] public static event Action? Evt; // (the fields are emitted by the source generators) - private static unsafe delegate* managed Bootsharp_GetLabel; + public static unsafe delegate* managed Bootsharp_GetLabel; [Import] public static string GetLabel (int count) => default!; } """)); @@ -45,24 +45,19 @@ public static class Registry public static class App { // (the field is emitted by the source generators) - private static unsafe delegate* managed Bootsharp_GetName; + public static unsafe delegate* managed Bootsharp_GetName; [Import] public static string GetName () => default!; } """)); Execute(); Contains( """ - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_GetName")] - private static extern unsafe ref delegate* managed Access_Entry_App_GetName ([UnsafeAccessorType("Entry.App, Entry")] object? _); - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_GetLabel")] - private static extern unsafe ref delegate* managed Access_Library_Registry_GetLabel ([UnsafeAccessorType("Library.Registry, Library")] object? _); - [ModuleInitializer] internal static unsafe void Initialize () { global::Library.Registry.Evt += Handle_Library_Registry_Evt; - Access_Entry_App_GetName(default) = &Entry_App_GetName; - Access_Library_Registry_GetLabel(default) = &Library_Registry_GetLabel; + global::Entry.App.Bootsharp_GetName = &Entry_App_GetName; + global::Library.Registry.Bootsharp_GetLabel = &Library_Registry_GetLabel; } """); } diff --git a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs index 6c6fb344..40ec5793 100644 --- a/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs +++ b/src/cs/Bootsharp.Publish/Emit/BootsharpEmit.cs @@ -8,7 +8,6 @@ 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 DependenciesFilePath { get; set; } public required string SerializerFilePath { get; set; } public required string InteropFilePath { get; set; } @@ -17,7 +16,6 @@ public override bool Execute () var prefs = ResolvePreferences(); using var inspection = InspectSolution(prefs); GenerateInterfaces(inspection); - GenerateDependencies(inspection); GenerateSerializer(inspection); GenerateInterop(inspection); return true; @@ -45,13 +43,6 @@ private void GenerateInterfaces (SolutionInspection inspection) WriteGenerated(InterfacesFilePath, content); } - private void GenerateDependencies (SolutionInspection inspection) - { - var generator = new DependencyGenerator(EntryAssemblyName); - var content = generator.Generate(inspection); - WriteGenerated(DependenciesFilePath, content); - } - private void GenerateSerializer (SolutionInspection inspection) { var generator = new SerializerGenerator(); diff --git a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs b/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs deleted file mode 100644 index 1183a2e3..00000000 --- a/src/cs/Bootsharp.Publish/Emit/DependencyGenerator.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Bootsharp.Publish; - -/// -/// Generates hints for .NET to not trim specified dynamic dependencies, ie -/// members that are not explicitly accessed in the user source code. -/// -internal sealed class DependencyGenerator (string entryAssembly) -{ - private readonly HashSet added = []; - - public string Generate (SolutionInspection inspection) - { - AddGeneratedCommon(); - AddGeneratedInteropClasses(inspection); - AddClassesWithInteropMethods(inspection); - return - $$""" - using System.Diagnostics.CodeAnalysis; - - namespace Bootsharp.Generated; - - public static class Dependencies - { - private const DynamicallyAccessedMemberTypes types = - DynamicallyAccessedMemberTypes.PublicMethods | - DynamicallyAccessedMemberTypes.NonPublicMethods | - DynamicallyAccessedMemberTypes.PublicFields | - DynamicallyAccessedMemberTypes.NonPublicFields | - DynamicallyAccessedMemberTypes.PublicNestedTypes | - DynamicallyAccessedMemberTypes.NonPublicNestedTypes | - DynamicallyAccessedMemberTypes.PublicProperties | - DynamicallyAccessedMemberTypes.NonPublicProperties | - DynamicallyAccessedMemberTypes.PublicEvents | - DynamicallyAccessedMemberTypes.NonPublicEvents | - DynamicallyAccessedMemberTypes.Interfaces; - - [System.Runtime.CompilerServices.ModuleInitializer] - {{Fmt(added)}} - internal static void RegisterDynamicDependencies () { } - } - """; - } - - private void AddGeneratedCommon () - { - Add("Bootsharp.Generated.Dependencies", entryAssembly); - Add("Bootsharp.Generated.Interop", entryAssembly); - } - - private void AddGeneratedInteropClasses (SolutionInspection inspection) - { - foreach (var it in inspection.StaticInterfaces) - Add(it.FullName, entryAssembly); - foreach (var it in inspection.InstancedInterfaces) - if (it.Interop == InteropKind.Import) - Add(it.FullName, entryAssembly); - } - - private void AddClassesWithInteropMethods (SolutionInspection inspection) - { - foreach (var member in inspection.StaticMembers) - Add(member.Space, member.Assembly); - } - - private void Add (string name, string assembly) - { - var asm = assembly.EndsWith(".dll", StringComparison.Ordinal) ? assembly[..^4] : assembly; - added.Add($"""[DynamicDependency(types, "{name}", "{asm}")]"""); - } -} diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index fddaea77..9014aad1 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -28,7 +28,7 @@ public static partial class Interop [JSExport] internal static void DisposeExportedInstance (int id) => Instances.DisposeExported(id); [JSImport("instances.disposeImported", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); - {{new InteropInitializerGenerator().Generate(inspection)}} + {{new InteropInitializerGenerator().Generate(inspection)}} {{Fmt(inspection.StaticMembers.SelectMany(EmitMember))}} {{Fmt(inspection.StaticInterfaces.SelectMany(i => i.Members.SelectMany(EmitMember)))}} diff --git a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs index 01aaab86..44c6be98 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropInitializerGenerator.cs @@ -11,8 +11,6 @@ public string Generate (SolutionInspection inspection) .Where(m => m.Interop == InteropKind.Import).ToArray(); if (methods.Length == 0 && events.Length == 0) return ""; return $$""" - {{Fmt(methods.Select(BuildMethodAccessor))}} - [ModuleInitializer] internal static unsafe void Initialize () { @@ -24,18 +22,6 @@ internal static unsafe void Initialize () """; } - private static string BuildMethodAccessor (MethodMeta method) - { - var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - var argType = string.Join(", ", [..method.Arguments.Select(a => a.Value.TypeSyntax), method.Value.TypeSyntax]); - var ptrType = $"delegate* managed<{argType}>"; - var accessor = $"""[UnsafeAccessorType("{method.Space}, {method.Assembly}")]"""; - return $""" - [UnsafeAccessor(UnsafeAccessorKind.StaticField, Name = "Bootsharp_{method.Name}")] - private static extern unsafe ref {ptrType} Access_{name} ({accessor} object? _); - """; - } - private static string BuildEventSubscription (EventMeta evt) { var handler = $"Handle_{evt.Space.Replace('.', '_')}_{evt.Name}"; @@ -45,6 +31,6 @@ private static string BuildEventSubscription (EventMeta evt) private static string BuildMethodAssignment (MethodMeta method) { var name = $"{method.Space.Replace('.', '_')}_{method.Name}"; - return $"Access_{name}(default) = &{name};"; + return $"global::{method.Space}.Bootsharp_{method.Name} = &{name};"; } } diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 8f394f82..0b3c9bcf 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -7,7 +7,6 @@ $(BsRoot)/tasks/Bootsharp.Publish.dll $(IntermediateOutputPath)bootsharp $(BsIntermediateDir)/Interfaces.g.cs - $(BsIntermediateDir)/Dependencies.g.cs $(BsIntermediateDir)/Serializer.g.cs $(BsIntermediateDir)/Interop.g.cs $(AssemblyName).dll @@ -62,16 +61,13 @@ - - diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e24c9e55..ffad2bc0 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.154 + 0.8.0-alpha.158 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com