From 093ebe56bfb44e61f3f47959a51153fd34ec89fe Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 27 Mar 2025 02:50:04 +0300 Subject: [PATCH 01/12] optimize serde --- src/cs/Bootsharp.Common.Test/SerializerTest.cs | 10 +++++----- src/cs/Bootsharp.Common/Interop/Serializer.cs | 2 +- src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs | 2 +- src/cs/Directory.Build.props | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/cs/Bootsharp.Common.Test/SerializerTest.cs b/src/cs/Bootsharp.Common.Test/SerializerTest.cs index fd26c9c7..9b0c72b9 100644 --- a/src/cs/Bootsharp.Common.Test/SerializerTest.cs +++ b/src/cs/Bootsharp.Common.Test/SerializerTest.cs @@ -21,7 +21,7 @@ public void WhenInfoResolverNotAssignedThrowsError () TypeInfoResolver = null }; Assert.Contains("Serializer info resolver is not assigned", - Assert.Throws(() => Serialize("", null)).Message); + Assert.Throws(() => Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))).Message); } [Fact] @@ -30,15 +30,15 @@ public void WhenTypeInfoNotAvailableThrowsError () Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { TypeInfoResolver = new MockResolver() }; - Assert.Contains("Failed to resolve serializer info", - Assert.Throws(() => Serialize("", null)).Message); + Assert.Contains("JsonTypeInfo metadata for type 'Bootsharp.Common.Test.Mocks+MockRecord' was not provided", + Assert.Throws(() => Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))).Message); } [Fact] public void CanSerialize () { Assert.Equal("""{"items":[{"id":"foo"},{"id":"bar"}]}""", - Serialize(new MockRecord(new MockItem[] { new("foo"), new("bar") }), typeof(MockRecord))); + Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))); } [Fact] @@ -50,7 +50,7 @@ public void SerializesNullAsNull () [Fact] public void CanDeserialize () { - Assert.Equal(new MockItem[] { new("foo"), new("bar") }, + Assert.Equal([new("foo"), new("bar")], Deserialize("""{"items":[{"id":"foo"},{"id":"bar"}]}""").Items); } diff --git a/src/cs/Bootsharp.Common/Interop/Serializer.cs b/src/cs/Bootsharp.Common/Interop/Serializer.cs index a15b5a5a..493192be 100644 --- a/src/cs/Bootsharp.Common/Interop/Serializer.cs +++ b/src/cs/Bootsharp.Common/Interop/Serializer.cs @@ -41,7 +41,7 @@ private static JsonTypeInfo GetInfo (Type type) { if (Options.TypeInfoResolver is null) throw new Error("Serializer info resolver is not assigned."); - return Options.TypeInfoResolver.GetTypeInfo(type, Options) ?? + return Options.GetTypeInfo(type) ?? throw new Error($"Failed to resolve serializer info for '{type}'."); } } diff --git a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs index acc8f81d..d91b3a39 100644 --- a/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs +++ b/src/cs/Bootsharp.Publish.Test/Mock/MockCompiler.cs @@ -42,7 +42,7 @@ private static CSharpCompilation CreateCompilation (string assemblyPath, string var tree = CSharpSyntaxTree.ParseText(text); var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary); var refs = GatherReferences(Path.GetDirectoryName(assemblyPath)); - return CSharpCompilation.Create(assemblyName, new[] { tree }, refs, options); + return CSharpCompilation.Create(assemblyName, [tree], refs, options); } private static PortableExecutableReference[] GatherReferences (string directory) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 6437eb84..6cc52ec5 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0 + 0.5.0-alpha.2 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com From 4a617baa2abf5a5771414f543ad671afd1b3ee06 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:38:33 +0300 Subject: [PATCH 02/12] optimize serde --- src/cs/Bootsharp.Common.Test/MockResolver.cs | 9 -- src/cs/Bootsharp.Common.Test/Mocks.cs | 2 - .../Bootsharp.Common.Test/SerializerTest.cs | 83 ++++++------------- src/cs/Bootsharp.Common/Interop/Serializer.cs | 25 +----- .../Emit/InteropTest.cs | 8 +- .../Emit/SerializerTest.cs | 2 +- .../Common/Global/GlobalType.cs | 17 ++++ .../Common/Meta/ValueMeta.cs | 4 + .../SolutionInspector/MethodInspector.cs | 1 + .../Emit/InteropGenerator.cs | 19 ++--- .../Emit/SerializerGenerator.cs | 63 +++++++------- src/cs/Directory.Build.props | 2 +- 12 files changed, 91 insertions(+), 144 deletions(-) delete mode 100644 src/cs/Bootsharp.Common.Test/MockResolver.cs diff --git a/src/cs/Bootsharp.Common.Test/MockResolver.cs b/src/cs/Bootsharp.Common.Test/MockResolver.cs deleted file mode 100644 index 7b9831e3..00000000 --- a/src/cs/Bootsharp.Common.Test/MockResolver.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; - -namespace Bootsharp.Common.Test; - -public class MockResolver : IJsonTypeInfoResolver -{ - public JsonTypeInfo GetTypeInfo (Type type, JsonSerializerOptions options) => null; -} diff --git a/src/cs/Bootsharp.Common.Test/Mocks.cs b/src/cs/Bootsharp.Common.Test/Mocks.cs index e207f5bb..d2dd0a4c 100644 --- a/src/cs/Bootsharp.Common.Test/Mocks.cs +++ b/src/cs/Bootsharp.Common.Test/Mocks.cs @@ -9,8 +9,6 @@ public interface IFrontend; public class Backend : IBackend; public class Frontend : IFrontend; - public enum MockEnum { Foo, Bar } public record MockItem (string Id); - public record MockItemWithEnum (MockEnum? Enum); public record MockRecord (IReadOnlyList Items); } diff --git a/src/cs/Bootsharp.Common.Test/SerializerTest.cs b/src/cs/Bootsharp.Common.Test/SerializerTest.cs index 9b0c72b9..9e90fae8 100644 --- a/src/cs/Bootsharp.Common.Test/SerializerTest.cs +++ b/src/cs/Bootsharp.Common.Test/SerializerTest.cs @@ -1,94 +1,59 @@ using System.Text.Json; using System.Text.Json.Serialization; -using System.Text.Json.Serialization.Metadata; using static Bootsharp.Serializer; namespace Bootsharp.Common.Test; -public class SerializerTest +public partial class SerializerTest { - public SerializerTest () - { - Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - }; - } - - [Fact] - public void WhenInfoResolverNotAssignedThrowsError () - { - Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - TypeInfoResolver = null - }; - Assert.Contains("Serializer info resolver is not assigned", - Assert.Throws(() => Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))).Message); - } - - [Fact] - public void WhenTypeInfoNotAvailableThrowsError () - { - Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - TypeInfoResolver = new MockResolver() - }; - Assert.Contains("JsonTypeInfo metadata for type 'Bootsharp.Common.Test.Mocks+MockRecord' was not provided", - Assert.Throws(() => Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))).Message); - } + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(MockItem))] + [JsonSerializable(typeof(MockRecord))] + [JsonSourceGenerationOptions( + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + )] + internal partial class SerializerContext : JsonSerializerContext; [Fact] public void CanSerialize () { Assert.Equal("""{"items":[{"id":"foo"},{"id":"bar"}]}""", - Serialize(new MockRecord([new("foo"), new("bar")]), typeof(MockRecord))); + Serialize(new MockRecord([new("foo"), new("bar")]), SerializerContext.Default.MockRecord)); } [Fact] public void SerializesNullAsNull () { - Assert.Equal("null", Serialize(null, null)); + Assert.Equal("null", Serialize(null, SerializerContext.Default.MockRecord)); } [Fact] public void CanDeserialize () { Assert.Equal([new("foo"), new("bar")], - Deserialize("""{"items":[{"id":"foo"},{"id":"bar"}]}""").Items); + Deserialize("""{"items":[{"id":"foo"},{"id":"bar"}]}""", SerializerContext.Default.MockRecord).Items); } [Fact] public void DeserializesNullAndUndefinedAsDefault () { - Assert.Null(Deserialize(null)); - Assert.Null(Deserialize("null")); - Assert.Null(Deserialize("undefined")); - Assert.Null(Deserialize(null)); - Assert.Null(Deserialize("null")); - Assert.Null(Deserialize("undefined")); - Assert.False(Deserialize(null)); - Assert.False(Deserialize("null")); - Assert.False(Deserialize("undefined")); + Assert.Null(Deserialize(null, SerializerContext.Default.MockItem)); + Assert.Null(Deserialize("null", SerializerContext.Default.MockItem)); + Assert.Null(Deserialize("undefined", SerializerContext.Default.MockItem)); + Assert.Null(Deserialize(null, SerializerContext.Default.NullableInt32)); + Assert.Null(Deserialize("null", SerializerContext.Default.NullableInt32)); + Assert.Null(Deserialize("undefined", SerializerContext.Default.NullableInt32)); + Assert.False(Deserialize(null, SerializerContext.Default.Boolean)); + Assert.False(Deserialize("null", SerializerContext.Default.Boolean)); + Assert.False(Deserialize("undefined", SerializerContext.Default.Boolean)); } [Fact] public void WhenDeserializationFailsErrorIsThrown () { - Assert.Throws(() => Deserialize("")); - } - - [Fact] - public void RespectsOptions () - { - Assert.Equal("{\"enum\":0}", Serialize(new MockItemWithEnum(MockEnum.Foo), typeof(MockItemWithEnum))); - Assert.Equal("{\"enum\":null}", Serialize(new MockItemWithEnum(null), typeof(MockItemWithEnum))); - Assert.Equal(MockEnum.Foo, Deserialize("{\"enum\":0}").Enum); - Assert.Null((Deserialize("{\"enum\":null}")).Enum); - Options = new JsonSerializerOptions(JsonSerializerDefaults.Web) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter() }, - TypeInfoResolver = new DefaultJsonTypeInfoResolver() - }; - Assert.Equal("{\"enum\":\"Foo\"}", Serialize(new MockItemWithEnum(MockEnum.Foo), typeof(MockItemWithEnum))); - Assert.Equal("{}", Serialize(new MockItemWithEnum(null), typeof(MockItemWithEnum))); - Assert.Equal(MockEnum.Foo, (Deserialize("{\"enum\":\"Foo\"}")).Enum); - Assert.Null((Deserialize("{}")).Enum); + Assert.Throws(() => Deserialize("", SerializerContext.Default.Int32)); } } diff --git a/src/cs/Bootsharp.Common/Interop/Serializer.cs b/src/cs/Bootsharp.Common/Interop/Serializer.cs index 493192be..cb2d5248 100644 --- a/src/cs/Bootsharp.Common/Interop/Serializer.cs +++ b/src/cs/Bootsharp.Common/Interop/Serializer.cs @@ -1,5 +1,4 @@ using System.Text.Json; -using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; namespace Bootsharp; @@ -10,38 +9,22 @@ namespace Bootsharp; public static class Serializer { /// - /// Options for used under the hood. + /// Serializes specified object to JSON string using specified serialization context info. /// - public static JsonSerializerOptions Options { get; set; } = new(JsonSerializerDefaults.Web) { - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull - }; - - /// - /// Serializes specified object to JSON string using specified serialization context type. - /// - public static string Serialize (object? @object, Type type) + public static string Serialize (T? @object, JsonTypeInfo info) { if (@object is null) return "null"; - return JsonSerializer.Serialize(@object, GetInfo(type)); + return JsonSerializer.Serialize(@object, info); } /// /// Deserializes specified JSON string to the object of specified type. /// - public static T? Deserialize (string? json) + public static T? Deserialize (string? json, JsonTypeInfo info) { if (json is null || json.Equals("null", StringComparison.Ordinal) || json.Equals("undefined", StringComparison.Ordinal)) return default; - var info = (JsonTypeInfo)GetInfo(typeof(T)); return JsonSerializer.Deserialize(json, info); } - - private static JsonTypeInfo GetInfo (Type type) - { - if (Options.TypeInfoResolver is null) - throw new Error("Serializer info resolver is not assigned."); - return Options.GetTypeInfo(type) ?? - throw new Error($"Failed to resolve serializer info for '{type}'."); - } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index f1c8ffe3..efc1f245 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -206,10 +206,10 @@ public class Class } """)); Execute(); - Contains("""Proxies.Set("Space.Class.FunA", (global::Space.Record a) => Deserialize(Space_Class_FunA(Serialize(a, typeof(global::Space.Record)))));"""); - Contains("""Proxies.Set("Space.Class.FunB", async (global::Space.Record?[]? a) => Deserialize(await Space_Class_FunB(Serialize(a, typeof(global::Space.Record[])))));"""); - Contains("JSExport] internal static global::System.String Space_Class_InvA (global::System.String a) => Serialize(global::Space.Class.InvA(Deserialize(a)), typeof(global::Space.Record));"); - Contains("JSExport] internal static async global::System.Threading.Tasks.Task Space_Class_InvB (global::System.String? a) => Serialize(await global::Space.Class.InvB(Deserialize(a)), typeof(global::Space.Record[]));"); + Contains("""Proxies.Set("Space.Class.FunA", (global::Space.Record a) => Deserialize(Space_Class_FunA(Serialize(a, SerializerContext.Default.X17062DAD)), SerializerContext.Default.X17062DAD));"""); + Contains("""Proxies.Set("Space.Class.FunB", async (global::Space.Record?[]? a) => Deserialize(await Space_Class_FunB(Serialize(a, SerializerContext.Default.X6E3181CF)), SerializerContext.Default.X6E3181CF));"""); + Contains("JSExport] internal static global::System.String Space_Class_InvA (global::System.String a) => Serialize(global::Space.Class.InvA(Deserialize(a, SerializerContext.Default.X17062DAD)), SerializerContext.Default.X17062DAD);"); + Contains("JSExport] internal static async global::System.Threading.Tasks.Task Space_Class_InvB (global::System.String? a) => Serialize(await global::Space.Class.InvB(Deserialize(a, SerializerContext.Default.X6E3181CF)), SerializerContext.Default.X6E3181CF);"); Contains("""JSImport("Space.Class.funASerialized", "Bootsharp")] internal static partial global::System.String Space_Class_FunA (global::System.String a);"""); Contains("""JSImport("Space.Class.funBSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB (global::System.String? a);"""); diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 55c38188..572c0b62 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -62,7 +62,7 @@ public void AddsOnlyTopLevelTypesAndCrawledDuplicates () WithClass("n", "[JSInvokable] public static Baz? GetBaz () => default;")); Execute(); Assert.Equal(2, Matches("JsonSerializable").Count); - Contains("[JsonSerializable(typeof(global::n.Baz)"); + Contains("[JsonSerializable(typeof(global::n.Baz?)"); Contains("[JsonSerializable(typeof(global::y.Struct)"); } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 80d90fea..2c29e9a7 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -168,6 +168,12 @@ public static string BuildJSInteropInstanceClassName (InterfaceMeta inter) return inter.FullName.Replace("Bootsharp.Generated.Exports.", "").Replace(".", "_"); } + public static string BuildTypeInfo (Type type) + { + var syntax = IsTaskWithResult(type, out var result) ? BuildSyntax(result) : BuildSyntax(type); + return $"X{HashStable(syntax):X}"; + } + public static string BuildSyntax (Type type) => BuildSyntax(type, null, false); public static string BuildSyntax (Type type, ParameterInfo info) => BuildSyntax(type, GetNullability(info)); @@ -194,4 +200,15 @@ static string ResolveTypeName (Type type) return $"{type.Namespace}.{type.Name}"; } } + + private static int HashStable (this string str) + { + unchecked + { + var hash = 17; + foreach (char c in str) + hash = (hash * 31) + c; + return hash; + } + } } diff --git a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs index 98b10c7f..7be6dfb2 100644 --- a/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Meta/ValueMeta.cs @@ -20,6 +20,10 @@ internal sealed record ValueMeta /// public required string JSTypeSyntax { get; init; } /// + /// Serialization info handle for the type. + /// + public required string TypeInfo { get; init; } + /// /// Whether the value is optional/nullable. /// public required bool Nullable { get; init; } diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs index b25db09c..c761bfe0 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/MethodInspector.cs @@ -35,6 +35,7 @@ public MethodMeta Inspect (MethodInfo method, MethodKind kind) Type = param.ParameterType, TypeSyntax = BuildSyntax(param.ParameterType, param), JSTypeSyntax = converter.ToTypeScript(param.ParameterType, GetNullability(param)), + TypeInfo = BuildTypeInfo(param.ParameterType), Nullable = @return ? IsNullable(method) : IsNullable(param), Async = @return && IsTaskLike(param.ParameterType), Void = @return && IsVoid(param.ParameterType), diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 5768fb86..a6e4b3c2 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -72,7 +72,7 @@ string BuildBody () : $"global::{inv.Space}.{inv.Name}({args})"; if (wait) body = $"await {body}"; if (inv.ReturnValue.Instance) body = $"global::Bootsharp.Instances.Register({body})"; - else if (inv.ReturnValue.Serialized) body = $"Serialize({body}, {BuildSerdeType(inv.ReturnValue)})"; + else if (inv.ReturnValue.Serialized) body = $"Serialize({body}, {BuildTypeInfo(inv.ReturnValue)})"; return body; } @@ -83,7 +83,7 @@ string BuildBodyArg (ArgumentMeta arg) var (_, _, full) = BuildInteropInterfaceImplementationName(arg.Value.InstanceType, InterfaceKind.Import); return $"new global::{full}({arg.Name})"; } - if (arg.Value.Serialized) return $"Deserialize<{arg.Value.TypeSyntax}>({arg.Name})"; + if (arg.Value.Serialized) return $"Deserialize({arg.Name}, {BuildTypeInfo(arg.Value)})"; return arg.Name; } } @@ -110,13 +110,13 @@ string BuildBody () return $"({BuildSyntax(method.ReturnValue.InstanceType)})new global::{full}({body})"; } if (!method.ReturnValue.Serialized) return body; - return $"Deserialize<{StripTaskSyntax(method.ReturnValue)}>({body})"; + return $"Deserialize({body}, {BuildTypeInfo(method.ReturnValue)})"; } string BuildBodyArg (ArgumentMeta arg) { if (arg.Value.Instance) return $"global::Bootsharp.Instances.Register({arg.Name})"; - if (arg.Value.Serialized) return $"Serialize({arg.Name}, {BuildSerdeType(arg.Value)})"; + if (arg.Value.Serialized) return $"Serialize({arg.Name}, {BuildTypeInfo(arg.Value)})"; return arg.Name; } } @@ -170,15 +170,8 @@ private bool ShouldWait (ValueMeta value) return value.Async && (value.Serialized || ShouldMarshalAsAny(value.Type) || value.Instance); } - private string BuildSerdeType (ValueMeta value) + private static string BuildTypeInfo (ValueMeta meta) { - return $"typeof({StripTaskSyntax(value)})".Replace("?", ""); - } - - private string StripTaskSyntax (ValueMeta value) - { - return value.Async - ? value.TypeSyntax[36..^1] - : value.TypeSyntax; + return $"SerializerContext.Default.{meta.TypeInfo}"; } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index 9a1a2eae..34dcf691 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -11,49 +11,46 @@ internal sealed class SerializerGenerator public string Generate (SolutionInspection inspection) { - var metas = inspection.StaticMethods - .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) - .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)); - foreach (var meta in metas) - CollectAttributes(meta); + CollectAttributes(inspection); CollectDuplicates(inspection); if (attributes.Count == 0) return ""; return - $$""" - using System.Text.Json; - using System.Text.Json.Serialization; + $""" + using System.Text.Json.Serialization; + + namespace Bootsharp.Generated; - namespace Bootsharp.Generated; + {JoinLines(attributes, 0)} + [JsonSourceGenerationOptions( + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + )] + internal partial class SerializerContext : JsonSerializerContext; + """; + } - {{JoinLines(attributes, 0)}} - internal partial class SerializerContext : JsonSerializerContext - { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void InjectTypeInfoResolver () - { - Serializer.Options.TypeInfoResolverChain.Add(SerializerContext.Default); - } - } - """; + private void CollectAttributes (SolutionInspection inspection) + { + var metas = inspection.StaticMethods + .Concat(inspection.StaticInterfaces.SelectMany(i => i.Methods)) + .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)); + foreach (var meta in metas) + CollectFromMethod(meta); } - private void CollectAttributes (MethodMeta method) + private void CollectFromMethod (MethodMeta method) { if (method.ReturnValue.Serialized) - CollectAttributes(method.ReturnValue.TypeSyntax, method.ReturnValue.Type); + CollectFromValue(method.ReturnValue); foreach (var arg in method.Arguments) if (arg.Value.Serialized) - CollectAttributes(arg.Value.TypeSyntax, arg.Value.Type); + CollectFromValue(arg.Value); } - private void CollectAttributes (string syntax, Type type) + private void CollectFromValue (ValueMeta meta) { - if (IsTaskWithResult(type, out var result)) - // Task<> produces trim warnings, so hacking with a proxy tuple. - // Passing just the result may conflict with a type inferred by - // .NET's generator from other types (it throws on duplicates). - syntax = $"({BuildSyntax(result)}, byte)"; - attributes.Add(BuildAttribute(syntax)); + attributes.Add(BuildAttribute(meta.TypeSyntax, meta.TypeInfo)); } private void CollectDuplicates (SolutionInspection inspection) @@ -61,13 +58,11 @@ private void CollectDuplicates (SolutionInspection inspection) var names = new HashSet(); foreach (var type in inspection.Crawled.DistinctBy(t => t.FullName)) if (ShouldSerialize(type) && !names.Add(type.Name)) - CollectAttributes(BuildSyntax(type), type); + attributes.Add(BuildAttribute(BuildSyntax(type), BuildTypeInfo(type))); } - private static string BuildAttribute (string syntax) + private static string BuildAttribute (string typeSyntax, string typeInfo) { - syntax = syntax.Replace("?", ""); - var hint = $"X{syntax.GetHashCode():X}"; - return $"[JsonSerializable(typeof({syntax}), TypeInfoPropertyName = \"{hint}\")]"; + return $"[JsonSerializable(typeof({typeSyntax}), TypeInfoPropertyName = \"{typeInfo}\")]"; } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 6cc52ec5..880d6f01 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.5.0-alpha.2 + 0.5.0-alpha.7 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com From 149f99a44fab22fe39291f37655643956c464d80 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 27 Mar 2025 19:52:51 +0300 Subject: [PATCH 03/12] update docs --- docs/guide/serialization.md | 20 -------------------- docs/package.json | 8 ++++---- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index cedbb7d7..be99af8d 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -96,23 +96,3 @@ import { Program } from "bootsharp"; const map = Program.map(["foo", "bar"], [0, 7]); console.log(map.bar); // 7 ``` - -## Configuring Serialization Behaviour - -To override default JSON serializer options used for marshalling the interop data, use `Bootsharp.Serializer.Options` property before the program entry point is invoked: - -```csharp -static class Program -{ - static Program () // Static constructor is invoked before 'Main' - { - // Make enums serialize as strings. - var converter = new JsonStringEnumConverter(); - Bootsharp.Serializer.Options.Converters.Add(converter); - } - - public static void Main () { } -} -``` - -Refere to .NET docs for the available serialization options: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/overview. diff --git a/docs/package.json b/docs/package.json index 68c2ca61..975ab109 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,10 +7,10 @@ "docs:preview": "vitepress preview" }, "devDependencies": { - "typescript": "5.7.2", - "@types/node": "22.10.5", - "vitepress": "1.5.0", - "typedoc-vitepress-theme": "1.1.1", + "typescript": "5.8.2", + "@types/node": "22.13.14", + "vitepress": "1.6.3", + "typedoc-vitepress-theme": "1.1.2", "imgit": "0.2.1" } } From 48f0bfd9e540e2a1f460a98b4c82d253cc02114c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:01:20 +0300 Subject: [PATCH 04/12] use ordinal in proxies --- src/cs/Bootsharp.Common/Interop/Proxies.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Bootsharp.Common/Interop/Proxies.cs b/src/cs/Bootsharp.Common/Interop/Proxies.cs index f7b95c2b..97902e72 100644 --- a/src/cs/Bootsharp.Common/Interop/Proxies.cs +++ b/src/cs/Bootsharp.Common/Interop/Proxies.cs @@ -28,7 +28,7 @@ /// public static class Proxies { - private static readonly Dictionary map = new(); + private static readonly Dictionary map = new(StringComparer.Ordinal); /// /// Maps specified interop delegate to the specified ID. From f12e694eeee18bce8f40beddf45ea0af7120ad68 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Thu, 27 Mar 2025 21:01:31 +0300 Subject: [PATCH 05/12] upgrade deps --- src/cs/Directory.Build.props | 2 +- src/js/package.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 880d6f01..d97b4824 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.5.0-alpha.7 + 0.5.0-alpha.10 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/package.json b/src/js/package.json index 3d1794ab..aa8113f8 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -6,11 +6,11 @@ "build": "sh scripts/build.sh" }, "devDependencies": { - "typescript": "5.7.2", - "@types/node": "22.10.2", - "@types/ws": "8.5.13", - "vitest": "2.1.8", - "@vitest/coverage-v8": "2.1.8", - "ws": "8.18.0" + "typescript": "5.8.2", + "@types/node": "22.13.14", + "@types/ws": "8.18.0", + "vitest": "3.0.9", + "@vitest/coverage-v8": "3.0.9", + "ws": "8.18.1" } } From 08a8c1775c52cd09b3850b12f764b08d4cec2f5c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 28 Mar 2025 14:34:55 +0300 Subject: [PATCH 06/12] fix tests --- src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs | 4 ++-- src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs | 10 ++++++---- src/cs/Directory.Build.props | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 572c0b62..95916e88 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -59,10 +59,10 @@ public void AddsOnlyTopLevelTypesAndCrawledDuplicates () 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; } public Enum E { get; } }"), - WithClass("n", "[JSInvokable] public static Baz? GetBaz () => default;")); + WithClass("n", "[JSInvokable] public static Task GetBaz () => default;")); Execute(); Assert.Equal(2, Matches("JsonSerializable").Count); - Contains("[JsonSerializable(typeof(global::n.Baz?)"); + Contains("[JsonSerializable(typeof(global::n.Baz)"); Contains("[JsonSerializable(typeof(global::y.Struct)"); } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index 34dcf691..aa17a838 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -50,7 +50,7 @@ private void CollectFromMethod (MethodMeta method) private void CollectFromValue (ValueMeta meta) { - attributes.Add(BuildAttribute(meta.TypeSyntax, meta.TypeInfo)); + attributes.Add(BuildAttribute(meta.Type)); } private void CollectDuplicates (SolutionInspection inspection) @@ -58,11 +58,13 @@ private void CollectDuplicates (SolutionInspection inspection) var names = new HashSet(); foreach (var type in inspection.Crawled.DistinctBy(t => t.FullName)) if (ShouldSerialize(type) && !names.Add(type.Name)) - attributes.Add(BuildAttribute(BuildSyntax(type), BuildTypeInfo(type))); + attributes.Add(BuildAttribute(type)); } - private static string BuildAttribute (string typeSyntax, string typeInfo) + private static string BuildAttribute (Type type) { - return $"[JsonSerializable(typeof({typeSyntax}), TypeInfoPropertyName = \"{typeInfo}\")]"; + var syntax = IsTaskWithResult(type, out var result) ? BuildSyntax(result) : BuildSyntax(type); + var info = BuildTypeInfo(type); + return $"[JsonSerializable(typeof({syntax}), TypeInfoPropertyName = \"{info}\")]"; } } diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index d97b4824..b55a82ea 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.5.0-alpha.10 + 0.5.0-alpha.11 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com From f7c7fa18e50029d7195886eabdd53724635dcfea Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 28 Mar 2025 16:04:20 +0300 Subject: [PATCH 07/12] update readme --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ac9cadb0..b424f020 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,60 @@ In contrast to solutions like Blazor, which attempt to bring the entire web plat Bootsharp itself is built on top of [System.Runtime.InteropServices.JavaScript](https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop?view=aspnetcore-8.0) introduced in .NET 7. -If you're looking to expose simple library API to JavaScript and don't need type declarations, Bootsharp would probably be an overkill. However, .NET's interop is low-level, doesn't support passing custom types by value and requires lots of boilerplate to author the bindings. It's impractical for large API surfaces. - -With Bootsharp, you'll be able to just feed it your domain-specific interfaces and use them seamlessly from the other side, as if they were originally authored in TypeScript (and vice-versa). Additionally, Bootsharp provides an option to bundle all the binaries into single-file ES module and patches .NET's internal JavaScript code to make it compatible with constrained runtime environments, such as VS Code [web extensions](https://code.visualstudio.com/api/extension-guides/web-extensions). +If you need to expose a simple library API to JavaScript and don't require type declarations, Bootsharp is probably overkill. However, .NET's interop is low-level, lacks support for passing custom types by value, and requires extensive boilerplate to define bindings, making it impractical for large API surfaces. + +With Bootsharp, you can simply provide your domain-specific interfaces and use them seamlessly on the other side, as if they were originally authored in TypeScript (and vice versa). This ensures a clear separation of concerns: your domain codebase won't be aware of the JavaScript environment—no need to annotate methods for interop or specify marshalling hints for arguments. + +For example, consider the following abstract domain code: + +```cs +public record Data (string Info, IReadOnlyList Items); +public record Result (View Header, View Content); +public interface IProvider { Data GetData (); } +public interface IGenerator { Result Generate (); } + +public class Generator (IProvider provider) : IGenerator +{ + public Result Generate () + { + var data = provider.GetData(); + // Process the data and generate result. + return result; + } +} +``` +— the code doesn't use any JavaScript-specific APIs, making it fully testable and reusable. To expose it to JavaScript, all we need to do is add the following to `Program.cs` in a separate project for the WASM target: + +```cs +using Bootsharp; +using Bootsharp.Inject; +using Microsoft.Extensions.DependencyInjection; + +[assembly: JSImport(typeof(IProvider))] +[assembly: JSExport(typeof(IGenerator))] + +// Bootsharp auto-injects implementation for 'IProvider' +// from JS and exposes 'Generator' APIs to JS. +new ServiceCollection() + .AddBootsharp() + .AddSingleton() + .BuildServiceProvider() + .RunBootsharp(); +``` + +— we can now provide implementation for `IProvider` and use `Generator` in JavaScript/TypeScript: + +```ts +import bootsharp, { Provider, Generator } from "bootsharp"; + +// Implement 'IProvider'. +Provider.getData = () => ({ + info: "...", + items: [] +}); + +await bootsharp.boot(); + +// Use 'Generator'. +const result = Generator.generate(); +``` From d22d55d71fd019ccb96c08198288a36adadf4b23 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:39:48 +0300 Subject: [PATCH 08/12] direct import bindings --- src/cs/Bootsharp.Generate.Test/EventTest.cs | 14 +++++- .../Bootsharp.Generate.Test/FunctionTest.cs | 49 ++++++++++++++++--- src/cs/Bootsharp.Generate/PartialMethod.cs | 28 +++++------ .../Emit/InterfacesTest.cs | 22 ++++----- .../Emit/InteropTest.cs | 31 +++--------- .../Emit/InterfaceGenerator.cs | 13 +---- .../Emit/InteropGenerator.cs | 40 +++++++-------- src/cs/Bootsharp/Build/Bootsharp.targets | 7 ++- src/cs/Directory.Build.props | 2 +- .../Test.Types/Vehicle/IRegistryProvider.cs | 10 ++++ src/js/test/cs/Test.Types/Vehicle/Registry.cs | 18 ++----- src/js/test/cs/Test/Program.cs | 5 +- src/js/test/spec/interop.spec.ts | 6 +-- 13 files changed, 133 insertions(+), 112 deletions(-) create mode 100644 src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs diff --git a/src/cs/Bootsharp.Generate.Test/EventTest.cs b/src/cs/Bootsharp.Generate.Test/EventTest.cs index 149605ea..df501165 100644 --- a/src/cs/Bootsharp.Generate.Test/EventTest.cs +++ b/src/cs/Bootsharp.Generate.Test/EventTest.cs @@ -14,7 +14,12 @@ partial class Foo """ partial class Foo { - partial void OnBar () => global::Bootsharp.Proxies.Get("Foo.OnBar")(); + partial void OnBar () => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Foo_OnBar(); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ], @@ -33,7 +38,12 @@ namespace Space; public static partial class Foo { - public static partial void OnBar (global::System.String a, global::System.Int32 b) => global::Bootsharp.Proxies.Get>("Space.Foo.OnBar")(a, b); + public static partial void OnBar (global::System.String a, global::System.Int32 b) => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Space_Foo_OnBar(a, b); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ] diff --git a/src/cs/Bootsharp.Generate.Test/FunctionTest.cs b/src/cs/Bootsharp.Generate.Test/FunctionTest.cs index 638380c7..4e12fa90 100644 --- a/src/cs/Bootsharp.Generate.Test/FunctionTest.cs +++ b/src/cs/Bootsharp.Generate.Test/FunctionTest.cs @@ -14,7 +14,12 @@ partial class Foo """ partial class Foo { - partial void Bar () => global::Bootsharp.Proxies.Get("Foo.Bar")(); + partial void Bar () => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Foo_Bar(); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ], @@ -37,7 +42,12 @@ namespace File.Scoped; public static partial class Foo { - private static partial global::System.Threading.Tasks.Task BarAsync (global::System.String[] a, global::System.Int32? b) => global::Bootsharp.Proxies.Get>("File.Scoped.Foo.BarAsync")(a, b); + private static partial global::System.Threading.Tasks.Task BarAsync (global::System.String[] a, global::System.Int32? b) => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_File_Scoped_Foo_BarAsync(a, b); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ], @@ -60,7 +70,12 @@ namespace File.Scoped; public static partial class Foo { - private static partial global::System.Threading.Tasks.Task BarAsync () => global::Bootsharp.Proxies.Get>>("File.Scoped.Foo.BarAsync")(); + private static partial global::System.Threading.Tasks.Task BarAsync () => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_File_Scoped_Foo_BarAsync(); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ], @@ -77,7 +92,12 @@ partial class Foo """ partial class Foo { - partial void Bar (global::Record a) => global::Bootsharp.Proxies.Get>("Foo.Bar")(a); + partial void Bar (global::Record a) => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Foo_Bar(a); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ], @@ -104,8 +124,18 @@ namespace Classic { partial class Foo { - public partial global::System.DateTime GetTime (global::System.DateTime time) => global::Bootsharp.Proxies.Get>("Classic.Foo.GetTime")(time); - public partial global::System.Threading.Tasks.Task GetTimeAsync (global::System.DateTime time) => global::Bootsharp.Proxies.Get>>("Classic.Foo.GetTimeAsync")(time); + public partial global::System.DateTime GetTime (global::System.DateTime time) => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Classic_Foo_GetTime(time); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif + public partial global::System.Threading.Tasks.Task GetTimeAsync (global::System.DateTime time) => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Classic_Foo_GetTimeAsync(time); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } } """ @@ -125,7 +155,12 @@ partial class Foo partial class Foo { - partial void Bar () => global::Bootsharp.Proxies.Get("Foo.Bar")(); + partial void Bar () => + #if BOOTSHARP_EMITTED + global::Bootsharp.Generated.Interop.Proxy_Foo_Bar(); + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif } """ ] diff --git a/src/cs/Bootsharp.Generate/PartialMethod.cs b/src/cs/Bootsharp.Generate/PartialMethod.cs index 04f0bd82..24cdf25c 100644 --- a/src/cs/Bootsharp.Generate/PartialMethod.cs +++ b/src/cs/Bootsharp.Generate/PartialMethod.cs @@ -12,7 +12,14 @@ internal sealed class PartialMethod (MethodDeclarationSyntax syntax) public string EmitSource (Compilation compilation) { method = compilation.GetSemanticModel(syntax.SyntaxTree).GetDeclaredSymbol(syntax)!; - return $"{syntax.Modifiers} {EmitSignature()} => {EmitBody()};"; + return $""" + {syntax.Modifiers} {EmitSignature()} => + #if BOOTSHARP_EMITTED + {EmitBody()}; + #else + throw new System.NotImplementedException("https://github.com/elringus/bootsharp/issues/173"); + #endif + """; } private string EmitSignature () @@ -23,22 +30,15 @@ private string EmitSignature () private string EmitBody () { - return $"""global::Bootsharp.Proxies.Get<{BuildGetterType()}>("{BuildId()}")({BuildArgs()})"""; + return $"global::Bootsharp.Generated.Interop.{BuildName()}({BuildArgs()})"; } - private string BuildId () + private string BuildName () { - if (method.ContainingNamespace.IsGlobalNamespace) return $"{method.ContainingType.Name}.{method.Name}"; - return string.Join(".", [..method.ContainingNamespace.ConstituentNamespaces, method.ContainingType.Name, method.Name]); - } - - private string BuildGetterType () - { - if (method.ReturnsVoid && method.Parameters.Length == 0) return "global::System.Action"; - var basename = method.ReturnsVoid ? "global::System.Action" : "global::System.Func"; - var args = method.Parameters.Select(p => BuildSyntax(p.Type)); - if (!method.ReturnsVoid) args = args.Append(BuildSyntax(method.ReturnType)); - return $"{basename}<{string.Join(", ", args)}>"; + var name = method.ContainingNamespace.IsGlobalNamespace + ? $"{method.ContainingType.Name}_{method.Name}" + : string.Join("_", [..method.ContainingNamespace.ConstituentNamespaces, method.ContainingType.Name, method.Name]); + return $"Proxy_{name.Replace(".", "_")}"; } private string BuildArgs () diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs index df90858c..94be45ec 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs @@ -85,11 +85,11 @@ namespace Bootsharp.Generated.Imports { public class JSImported : global::IImported { - [JSFunction] public static void Inv (global::System.String? a) => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.Inv")(a); - [JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.InvAsync")(); - [JSFunction] public static global::Record? InvRecord () => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.InvRecord")(); - [JSFunction] public static global::System.Threading.Tasks.Task InvAsyncResult () => Proxies.Get>>("Bootsharp.Generated.Imports.JSImported.InvAsyncResult")(); - [JSFunction] public static global::System.String[] InvArray (global::System.Int32[] a) => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.InvArray")(a); + [JSFunction] public static void Inv (global::System.String? a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Inv(a); + [JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsync(); + [JSFunction] public static global::Record? InvRecord () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvRecord(); + [JSFunction] public static global::System.Threading.Tasks.Task InvAsyncResult () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvAsyncResult(); + [JSFunction] public static global::System.String[] InvArray (global::System.Int32[] a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_InvArray(a); void global::IImported.Inv (global::System.String? a) => Inv(a); global::System.Threading.Tasks.Task global::IImported.InvAsync () => InvAsync(); @@ -138,8 +138,8 @@ public class JSImported(global::System.Int32 _id) : global::IImported { ~JSImported() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id); - [JSFunction] public static void Fun (global::System.Int32 _id, global::System.String arg) => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.Fun")(_id, arg); - [JSEvent] public static void OnEvt (global::System.Int32 _id, global::System.String arg) => Proxies.Get>("Bootsharp.Generated.Imports.JSImported.OnEvt")(_id, arg); + [JSFunction] public static void Fun (global::System.Int32 _id, global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_Fun(_id, arg); + [JSEvent] public static void OnEvt (global::System.Int32 _id, global::System.String arg) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnEvt(_id, arg); void global::IImported.Fun (global::System.String arg) => Fun(_id, arg); void global::IImported.NotifyEvt (global::System.String arg) => OnEvt(_id, arg); @@ -185,7 +185,7 @@ namespace Bootsharp.Generated.Imports.Space { public class JSImported : global::Space.IImported { - [JSFunction] public static void Fun (global::Space.Record a) => Proxies.Get>("Bootsharp.Generated.Imports.Space.JSImported.Fun")(a); + [JSFunction] public static void Fun (global::Space.Record a) => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_Space_JSImported_Fun(a); void global::Space.IImported.Fun (global::Space.Record a) => Fun(a); } @@ -224,7 +224,7 @@ namespace Bootsharp.Generated.Imports { public class JSImported : global::IImported { - [JSEvent] public static void OnFoo () => Proxies.Get("Bootsharp.Generated.Imports.JSImported.OnFoo")(); + [JSEvent] public static void OnFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnFoo(); void global::IImported.NotifyFoo () => OnFoo(); } @@ -253,8 +253,8 @@ namespace Bootsharp.Generated.Imports { public class JSImported : global::IImported { - [JSFunction] public static void NotifyFoo () => Proxies.Get("Bootsharp.Generated.Imports.JSImported.NotifyFoo")(); - [JSEvent] public static void OnBar () => Proxies.Get("Bootsharp.Generated.Imports.JSImported.OnBar")(); + [JSFunction] public static void NotifyFoo () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_NotifyFoo(); + [JSEvent] public static void OnBar () => global::Bootsharp.Generated.Interop.Proxy_Bootsharp_Generated_Imports_JSImported_OnBar(); void global::IImported.NotifyFoo () => NotifyFoo(); void global::IImported.BroadcastBar () => OnBar(); diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index efc1f245..f2033e91 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -17,8 +17,6 @@ namespace Bootsharp.Generated; public static partial class Interop { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterProxies () """); } @@ -43,8 +41,6 @@ [JSInvokable] public static void Inv () {} } """)); Execute(); - Contains("""Proxies.Set("Class.Fun", () => Class_Fun());"""); - Contains("""Proxies.Set("Class.Evt", () => Class_Evt());"""); 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 ();"""); @@ -75,10 +71,6 @@ [JSInvokable] public static void Inv () {} } """)); Execute(); - Contains("""Proxies.Set("SpaceA.Class.Fun", () => SpaceA_Class_Fun());"""); - Contains("""Proxies.Set("SpaceA.Class.Evt", () => SpaceA_Class_Evt());"""); - Contains("""Proxies.Set("SpaceA.SpaceB.Class.Fun", () => SpaceA_SpaceB_Class_Fun());"""); - Contains("""Proxies.Set("SpaceA.SpaceB.Class.Evt", () => SpaceA_SpaceB_Class_Evt());"""); 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 ();"""); @@ -99,8 +91,6 @@ namespace Space { public interface IExported { void Inv (); } } public interface IImported { void Fun (); void NotifyEvt(); } """)); Execute(); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.JSImported.Fun", () => Bootsharp_Generated_Imports_JSImported_Fun());"""); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.JSImported.OnEvt", () => Bootsharp_Generated_Imports_JSImported_OnEvt());"""); 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 ();"""); @@ -127,9 +117,6 @@ public class Class } """)); Execute(); - Contains("""Proxies.Set("Class.GetImported", async (global::IExported arg) => (global::IImported)new global::Bootsharp.Generated.Imports.JSImported(await Class_GetImported(global::Bootsharp.Instances.Register(arg))));"""); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.JSImported.OnEvt", (global::System.Int32 _id) => Bootsharp_Generated_Imports_JSImported_OnEvt(_id));"""); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.Space.JSImported.Fun", (global::System.Int32 _id) => Bootsharp_Generated_Imports_Space_JSImported_Fun(_id));"""); Contains("JSExport] internal static async global::System.Threading.Tasks.Task Class_GetExported (global::System.Int32 arg) => global::Bootsharp.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)global::Bootsharp.Instances.Get(_id)).Inv();"); @@ -177,12 +164,12 @@ public class Class } """)); Execute(); - Contains("""Proxies.Set("Space.Class.Fun", (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, global::System.Byte[] a13, global::System.Int32[] a14, global::System.Double[] a15, global::System.String[] a16) => Space_Class_Fun(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16));"""); - Contains("""Proxies.Set("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, global::System.Byte[]? a13, global::System.Int32[]? a14, global::System.Double[]? a15, global::System.String[]? a16) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16));"""); 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::System.Byte[] a13, global::System.Int32[] a14, global::System.Double[] a15, global::System.String[] a16) => global::Space.Class.Inv(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16);"); 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::System.Byte[]? a13, global::System.Int32[]? a14, global::System.Double[]? a15, global::System.String[]? a16) => global::Space.Class.InvNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16);"); 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, global::System.Byte[] a13, global::System.Int32[] a14, global::System.Double[] a15, global::System.String[] a16);"""); + 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, global::System.Byte[]? a13, global::System.Int32[]? a14, global::System.Double[]? a15, global::System.String[]? a16) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16);"); 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, global::System.Byte[]? a13, global::System.Int32[]? a14, global::System.Double[]? a15, global::System.String[]? a16);"""); + 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, global::System.Byte[]? a13, global::System.Int32[]? a14, global::System.Double[]? a15, global::System.String[]? a16) => Space_Class_FunNull(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10, a11, a12, a13, a14, a15, a16);"); } [Fact] @@ -206,17 +193,17 @@ public class Class } """)); Execute(); - Contains("""Proxies.Set("Space.Class.FunA", (global::Space.Record a) => Deserialize(Space_Class_FunA(Serialize(a, SerializerContext.Default.X17062DAD)), SerializerContext.Default.X17062DAD));"""); - Contains("""Proxies.Set("Space.Class.FunB", async (global::Space.Record?[]? a) => Deserialize(await Space_Class_FunB(Serialize(a, SerializerContext.Default.X6E3181CF)), SerializerContext.Default.X6E3181CF));"""); - Contains("JSExport] internal static global::System.String Space_Class_InvA (global::System.String a) => Serialize(global::Space.Class.InvA(Deserialize(a, SerializerContext.Default.X17062DAD)), SerializerContext.Default.X17062DAD);"); - Contains("JSExport] internal static async global::System.Threading.Tasks.Task Space_Class_InvB (global::System.String? a) => Serialize(await global::Space.Class.InvB(Deserialize(a, SerializerContext.Default.X6E3181CF)), SerializerContext.Default.X6E3181CF);"); + Contains("JSExport] internal static global::System.String Space_Class_InvA (global::System.String a) => Serialize(global::Space.Class.InvA(Deserialize(a, global::Bootsharp.Generated.SerializerContext.Default.X17062DAD)), global::Bootsharp.Generated.SerializerContext.Default.X17062DAD);"); + Contains("JSExport] internal static async global::System.Threading.Tasks.Task Space_Class_InvB (global::System.String? a) => Serialize(await global::Space.Class.InvB(Deserialize(a, global::Bootsharp.Generated.SerializerContext.Default.X6E3181CF)), global::Bootsharp.Generated.SerializerContext.Default.X6E3181CF);"); Contains("""JSImport("Space.Class.funASerialized", "Bootsharp")] internal static partial global::System.String Space_Class_FunA (global::System.String a);"""); + Contains("public static global::Space.Record Proxy_Space_Class_FunA(global::Space.Record a) => Deserialize(Space_Class_FunA(Serialize(a, global::Bootsharp.Generated.SerializerContext.Default.X17062DAD)), global::Bootsharp.Generated.SerializerContext.Default.X17062DAD);"); Contains("""JSImport("Space.Class.funBSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_FunB (global::System.String? a);"""); + Contains("public static async global::System.Threading.Tasks.Task Proxy_Space_Class_FunB(global::Space.Record?[]? a) => Deserialize(await Space_Class_FunB(Serialize(a, global::Bootsharp.Generated.SerializerContext.Default.X6E3181CF)), global::Bootsharp.Generated.SerializerContext.Default.X6E3181CF);"); // TODO: Remove when resolved: https://github.com/elringus/bootsharp/issues/138 - Contains("""Proxies.Set("Space.Class.FunAsyncBytes", async () => await Space_Class_FunAsyncBytes());"""); Contains("JSExport] [return: JSMarshalAs>] internal static async global::System.Threading.Tasks.Task Space_Class_InvAsyncBytes () => await global::Space.Class.InvAsyncBytes();"); Contains("""JSImport("Space.Class.funAsyncBytesSerialized", "Bootsharp")] [return: JSMarshalAs>] internal static partial global::System.Threading.Tasks.Task Space_Class_FunAsyncBytes ();"""); + Contains("public static async global::System.Threading.Tasks.Task Proxy_Space_Class_FunAsyncBytes() => await Space_Class_FunAsyncBytes();"); } [Fact] @@ -241,10 +228,6 @@ [JSInvokable] public static void Inv () {} } """)); Execute(); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.Space.JSImported.Fun", () => Bootsharp_Generated_Imports_Space_JSImported_Fun());"""); - Contains("""Proxies.Set("Bootsharp.Generated.Imports.Space.JSImported.OnEvt", () => Bootsharp_Generated_Imports_Space_JSImported_OnEvt());"""); - Contains("""Proxies.Set("Space.Class.Fun", () => Space_Class_Fun());"""); - Contains("""Proxies.Set("Space.Class.Evt", () => Space_Class_Evt());"""); 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 ();"""); diff --git a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs index a5b94bc3..1fea1060 100644 --- a/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InterfaceGenerator.cs @@ -111,7 +111,8 @@ private string EmitImportMethod (InterfaceMeta i, MethodMeta method) var sig = $"public static {method.ReturnValue.TypeSyntax} {method.Name} ({sigArgs})"; var args = string.Join(", ", method.Arguments.Select(a => a.Name)); if (instanced.Contains(i)) args = PrependInstanceIdArgName(args); - return $"[{attr}] {sig} => {EmitProxyGetter(i, method)}({args});"; + var name = $"Proxy_{method.Space.Replace('.', '_')}_{method.Name}"; + return $"[{attr}] {sig} => global::Bootsharp.Generated.Interop.{name}({args});"; } private string EmitImportMethodImplementation (InterfaceMeta i, MethodMeta method) @@ -121,14 +122,4 @@ private string EmitImportMethodImplementation (InterfaceMeta i, MethodMeta metho if (instanced.Contains(i)) args = PrependInstanceIdArgName(args); return $"{method.ReturnValue.TypeSyntax} {i.TypeSyntax}.{method.InterfaceName} ({sigArgs}) => {method.Name}({args});"; } - - private string EmitProxyGetter (InterfaceMeta i, MethodMeta method) - { - var func = method.ReturnValue.Void ? "global::System.Action" : "global::System.Func"; - var syntax = method.Arguments.Select(a => a.Value.TypeSyntax).ToList(); - if (instanced.Contains(i)) syntax.Insert(0, BuildSyntax(typeof(int))); - if (!method.ReturnValue.Void) syntax.Add(method.ReturnValue.TypeSyntax); - if (syntax.Count > 0) func = $"{func}<{string.Join(", ", syntax)}>"; - return $"Proxies.Get<{func}>(\"{method.Space}.{method.Name}\")"; - } } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index a6e4b3c2..e939070b 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -7,7 +7,6 @@ namespace Bootsharp.Publish; /// internal sealed class InteropGenerator { - private readonly HashSet proxies = []; private readonly HashSet methods = []; private IReadOnlyCollection instanced = []; @@ -19,7 +18,7 @@ public string Generate (SolutionInspection inspection) .Concat(inspection.InstancedInterfaces.SelectMany(i => i.Methods)); foreach (var meta in @static) // @formatter:off if (meta.Kind == MethodKind.Invokable) AddExportMethod(meta); - else { AddProxy(meta); AddImportMethod(meta); } // @formatter:on + else { AddImportMethod(meta); AddImportProxy(meta); } // @formatter:on return $$""" #nullable enable @@ -32,12 +31,6 @@ namespace Bootsharp.Generated; public static partial class Interop { - [System.Runtime.CompilerServices.ModuleInitializer] - internal static void RegisterProxies () - { - {{JoinLines(proxies, 2)}} - } - [System.Runtime.InteropServices.JavaScript.JSExport] internal static void DisposeExportedInstance (int id) => global::Bootsharp.Instances.Dispose(id); [System.Runtime.InteropServices.JavaScript.JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (int id); @@ -88,15 +81,27 @@ string BuildBodyArg (ArgumentMeta arg) } } - private void AddProxy (MethodMeta method) + private void AddImportMethod (MethodMeta method) + { + var args = string.Join(", ", method.Arguments.Select(BuildSignatureArg)); + if (TryInstanced(method, out _)) args = PrependInstanceIdArgTypeAndName(args); + var @return = BuildReturnValue(method.ReturnValue); + var endpoint = $"{method.JSSpace}.{method.JSName}Serialized"; + var attr = $"""[System.Runtime.InteropServices.JavaScript.JSImport("{endpoint}", "Bootsharp")]"""; + var marsh = MarshalAmbiguous(method.ReturnValue, true); + methods.Add($"{attr} {marsh}internal static partial {@return} {BuildMethodName(method)} ({args});"); + } + + private void AddImportProxy (MethodMeta method) { var instanced = TryInstanced(method, out _); - var id = $"{method.Space}.{method.Name}"; + var name = $"Proxy_{BuildMethodName(method)}"; + var @return = method.ReturnValue.TypeSyntax; var args = string.Join(", ", method.Arguments.Select(arg => $"{arg.Value.TypeSyntax} {arg.Name}")); if (instanced) args = args = PrependInstanceIdArgTypeAndName(args); var wait = ShouldWait(method.ReturnValue); var async = wait ? "async " : ""; - proxies.Add($"""Proxies.Set("{id}", {async}({args}) => {BuildBody()});"""); + methods.Add($"public static {async}{@return} {name}({args}) => {BuildBody()};"); string BuildBody () { @@ -121,17 +126,6 @@ string BuildBodyArg (ArgumentMeta arg) } } - private void AddImportMethod (MethodMeta method) - { - var args = string.Join(", ", method.Arguments.Select(BuildSignatureArg)); - if (TryInstanced(method, out _)) args = PrependInstanceIdArgTypeAndName(args); - var @return = BuildReturnValue(method.ReturnValue); - var endpoint = $"{method.JSSpace}.{method.JSName}Serialized"; - var attr = $"""[System.Runtime.InteropServices.JavaScript.JSImport("{endpoint}", "Bootsharp")]"""; - var marsh = MarshalAmbiguous(method.ReturnValue, true); - methods.Add($"{attr} {marsh}internal static partial {@return} {BuildMethodName(method)} ({args});"); - } - private string BuildValueType (ValueMeta value) { if (value.Void) return "void"; @@ -172,6 +166,6 @@ private bool ShouldWait (ValueMeta value) private static string BuildTypeInfo (ValueMeta meta) { - return $"SerializerContext.Default.{meta.TypeInfo}"; + return $"global::Bootsharp.Generated.SerializerContext.Default.{meta.TypeInfo}"; } } diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index e7c90f9d..65d9336a 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -47,6 +47,7 @@ false false false + none @@ -54,10 +55,14 @@ - + + + $(DefineConstants);BOOTSHARP_EMITTED + + diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index b55a82ea..22431066 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.5.0-alpha.11 + 0.5.0-alpha.51 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs b/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs new file mode 100644 index 00000000..675efc80 --- /dev/null +++ b/src/js/test/cs/Test.Types/Vehicle/IRegistryProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace Test.Types; + +public interface IRegistryProvider +{ + Registry GetRegistry (); + IReadOnlyList GetRegistries (); + IReadOnlyDictionary GetRegistryMap (); +} diff --git a/src/js/test/cs/Test.Types/Vehicle/Registry.cs b/src/js/test/cs/Test.Types/Vehicle/Registry.cs index f90098d7..e345e586 100644 --- a/src/js/test/cs/Test.Types/Vehicle/Registry.cs +++ b/src/js/test/cs/Test.Types/Vehicle/Registry.cs @@ -5,8 +5,9 @@ namespace Test.Types; -public partial class Registry +public class Registry { + public static IRegistryProvider Provider { get; set; } public List Wheeled { get; set; } public List Tracked { get; set; } @@ -16,7 +17,7 @@ public partial class Registry [JSInvokable] public static float CountTotalSpeed () { - var registry = GetRegistry(); + var registry = Provider.GetRegistry(); return registry.Tracked.Sum(t => t.MaxSpeed) + registry.Wheeled.Sum(t => t.MaxSpeed); } @@ -25,25 +26,16 @@ public static float CountTotalSpeed () public static async Task> ConcatRegistriesAsync (IReadOnlyList registries) { await Task.Delay(1); - return registries.Concat(GetRegistries()).ToArray(); + return registries.Concat(Provider.GetRegistries()).ToArray(); } [JSInvokable] public static async Task> MapRegistriesAsync (IReadOnlyDictionary map) { await Task.Delay(1); - return map.Concat(GetRegistryMap()).ToDictionary(kv => kv.Key, kv => kv.Value); + return map.Concat(Provider.GetRegistryMap()).ToDictionary(kv => kv.Key, kv => kv.Value); } - [JSFunction] - public static partial Registry GetRegistry (); - - [JSFunction] - public static partial IReadOnlyList GetRegistries (); - - [JSFunction] - public static partial IReadOnlyDictionary GetRegistryMap (); - [JSInvokable] public static Vehicle GetWithEmptyId () => new() { Id = "" }; } diff --git a/src/js/test/cs/Test/Program.cs b/src/js/test/cs/Test/Program.cs index 8e734101..cc511ad2 100644 --- a/src/js/test/cs/Test/Program.cs +++ b/src/js/test/cs/Test/Program.cs @@ -5,8 +5,8 @@ using Microsoft.Extensions.DependencyInjection; using Test.Types; -[assembly: JSExport([typeof(IExportedStatic)])] -[assembly: JSImport([typeof(IImportedStatic)])] +[assembly: JSExport(typeof(IExportedStatic))] +[assembly: JSImport(typeof(IImportedStatic), typeof(IRegistryProvider))] namespace Test; @@ -21,6 +21,7 @@ public static void Main () .AddBootsharp() .BuildServiceProvider() .RunBootsharp(); + Registry.Provider = services.GetRequiredService(); OnMainInvoked(); } diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 7bbe5981..e7dd7791 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -76,7 +76,7 @@ describe("while bootsharp is booted", () => { expect(actual).toStrictEqual(expected); }); it("can transfer lists as arrays", async () => { - Test.Types.Registry.getRegistries = () => [{ + Test.Types.RegistryProvider.getRegistries = () => [{ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] }]; @@ -93,7 +93,7 @@ describe("while bootsharp is booted", () => { it("can transfer dictionaries as maps", async () => { // ES6 Map doesn't natively support JSON serialization, so using plain objects. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map - Test.Types.Registry.getRegistryMap = () => ({ + Test.Types.RegistryProvider.getRegistryMap = () => ({ foo: { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }] }, bar: { wheeled: [{ id: "bar", maxSpeed: 15, wheelCount: 5 }] } }); @@ -123,7 +123,7 @@ describe("while bootsharp is booted", () => { expect(Test.Functions.echoColExprByte([1, 2])).toStrictEqual([1, 2]); }); it("can invoke assigned JS functions in C#", () => { - Test.Types.Registry.getRegistry = () => ({ + Test.Types.RegistryProvider.getRegistry = () => ({ wheeled: [{ id: "", maxSpeed: 1, wheelCount: 0 }], tracked: [{ id: "", maxSpeed: 2, trackType: TrackType.Chain }] }); From 009af26bed64516691615414ea346571ff73145a Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 29 Mar 2025 12:41:08 +0300 Subject: [PATCH 09/12] formatting --- src/cs/Bootsharp/Build/Bootsharp.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 65d9336a..b9869418 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -62,7 +62,7 @@ $(DefineConstants);BOOTSHARP_EMITTED - + From 0ef55a539b2295c3f810b501feb302a48df3c918 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 29 Mar 2025 15:25:11 +0300 Subject: [PATCH 10/12] remove -c default --- src/cs/Bootsharp/Build/Bootsharp.props | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index cffa9856..55fb6dd1 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -3,7 +3,6 @@ Exe - Release true wasm browser From ff2dea6dc79907bfec45b11afb9423c1b14442bc Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:24:56 +0300 Subject: [PATCH 11/12] add bench sample --- samples/bench/bench.mjs | 29 ++++++++++++++++ samples/bench/bootsharp/Boot.csproj | 18 ++++++++++ samples/bench/bootsharp/Program.cs | 40 ++++++++++++++++++++++ samples/bench/bootsharp/init.mjs | 12 +++++++ samples/bench/bootsharp/readme.md | 2 ++ samples/bench/dotnet/DotNet.csproj | 17 +++++++++ samples/bench/dotnet/Program.cs | 39 +++++++++++++++++++++ samples/bench/dotnet/init.mjs | 22 ++++++++++++ samples/bench/dotnet/readme.md | 2 ++ samples/bench/fixtures.mjs | 18 ++++++++++ samples/bench/go/.gitignore | 2 ++ samples/bench/go/init.mjs | 20 +++++++++++ samples/bench/go/main.go | 46 +++++++++++++++++++++++++ samples/bench/go/readme.md | 3 ++ samples/bench/package.json | 5 +++ samples/bench/readme.md | 53 +++++++++++++++++++++++++++++ samples/bench/rust/.gitignore | 3 ++ samples/bench/rust/Cargo.toml | 19 +++++++++++ samples/bench/rust/init.mjs | 9 +++++ samples/bench/rust/readme.md | 3 ++ samples/bench/rust/src/lib.rs | 38 +++++++++++++++++++++ 21 files changed, 400 insertions(+) create mode 100644 samples/bench/bench.mjs create mode 100644 samples/bench/bootsharp/Boot.csproj create mode 100644 samples/bench/bootsharp/Program.cs create mode 100644 samples/bench/bootsharp/init.mjs create mode 100644 samples/bench/bootsharp/readme.md create mode 100644 samples/bench/dotnet/DotNet.csproj create mode 100644 samples/bench/dotnet/Program.cs create mode 100644 samples/bench/dotnet/init.mjs create mode 100644 samples/bench/dotnet/readme.md create mode 100644 samples/bench/fixtures.mjs create mode 100644 samples/bench/go/.gitignore create mode 100644 samples/bench/go/init.mjs create mode 100644 samples/bench/go/main.go create mode 100644 samples/bench/go/readme.md create mode 100644 samples/bench/package.json create mode 100644 samples/bench/readme.md create mode 100644 samples/bench/rust/.gitignore create mode 100644 samples/bench/rust/Cargo.toml create mode 100644 samples/bench/rust/init.mjs create mode 100644 samples/bench/rust/readme.md create mode 100644 samples/bench/rust/src/lib.rs diff --git a/samples/bench/bench.mjs b/samples/bench/bench.mjs new file mode 100644 index 00000000..6b3916ec --- /dev/null +++ b/samples/bench/bench.mjs @@ -0,0 +1,29 @@ +import { Bench, hrtimeNow } from "tinybench"; +import { init as initBootsharp } from "./bootsharp/init.mjs"; +import { init as initDotNet } from "./dotnet/init.mjs"; +import { init as initGo } from "./go/init.mjs"; +import { init as initRust } from "./rust/init.mjs"; + +/** + * @typedef {Object} Exports + * @property {() => number} echoNumber + * @property {() => Data} echoStruct + * @property {(n: number) => number} fi + */ + +await run("Rust", await initRust()); +await run("DotNet", await initDotNet()); +await run("Bootsharp", await initBootsharp()); +await run("Go", await initGo()); + +/** @param {string} lang */ +/** @param {Exports} exports */ +async function run(lang, exports) { + console.log(`Running ${lang}...`); + const bench = new Bench({ hrtimeNow }); + bench.add("Echo Number", exports.echoNumber); + bench.add("Echo Struct", exports.echoStruct); + bench.add("Fibonacci", () => exports.fi(25)); + await bench.run(); + console.table(bench.table()) +} diff --git a/samples/bench/bootsharp/Boot.csproj b/samples/bench/bootsharp/Boot.csproj new file mode 100644 index 00000000..1dbddb44 --- /dev/null +++ b/samples/bench/bootsharp/Boot.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + Release + browser-wasm + true + Speed + true + + + + + + + + + diff --git a/samples/bench/bootsharp/Program.cs b/samples/bench/bootsharp/Program.cs new file mode 100644 index 00000000..c6ab5f17 --- /dev/null +++ b/samples/bench/bootsharp/Program.cs @@ -0,0 +1,40 @@ +using Bootsharp; +using Bootsharp.Inject; +using Microsoft.Extensions.DependencyInjection; + +[assembly: JSImport(typeof(IImport))] +[assembly: JSExport(typeof(IExport))] + +new ServiceCollection() + .AddBootsharp() + .AddSingleton() + .BuildServiceProvider() + .RunBootsharp(); + +public struct Data +{ + public string Info; + public bool Ok; + public int Revision; + public string[] Messages; +} + +public interface IImport +{ + int GetNumber (); + Data GetStruct (); +} + +public interface IExport +{ + int EchoNumber (); + Data EchoStruct (); + int Fi (int n); +} + +public class Export (IImport import) : IExport +{ + public int EchoNumber () => import.GetNumber(); + public Data EchoStruct () => import.GetStruct(); + public int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2); +} diff --git a/samples/bench/bootsharp/init.mjs b/samples/bench/bootsharp/init.mjs new file mode 100644 index 00000000..9b455f81 --- /dev/null +++ b/samples/bench/bootsharp/init.mjs @@ -0,0 +1,12 @@ +import bootsharp, { Export, Import } from "./bin/bootsharp/index.mjs"; +import { getNumber, getStruct } from "../fixtures.mjs"; + +/** @returns {Promise} */ +export async function init() { + Import.getNumber = getNumber; + Import.getStruct = getStruct; + + await bootsharp.boot(); + + return { ...Export }; +} diff --git a/samples/bench/bootsharp/readme.md b/samples/bench/bootsharp/readme.md new file mode 100644 index 00000000..fdd43f39 --- /dev/null +++ b/samples/bench/bootsharp/readme.md @@ -0,0 +1,2 @@ +1. Install .NET https://dotnet.microsoft.com/en-us/download +2. Run `dotnet publish -c Release` diff --git a/samples/bench/dotnet/DotNet.csproj b/samples/bench/dotnet/DotNet.csproj new file mode 100644 index 00000000..94cf0404 --- /dev/null +++ b/samples/bench/dotnet/DotNet.csproj @@ -0,0 +1,17 @@ + + + net9.0 + Release + browser-wasm + true + true + Speed + true + + + + + <_Parameter1>browser + + + diff --git a/samples/bench/dotnet/Program.cs b/samples/bench/dotnet/Program.cs new file mode 100644 index 00000000..dfa6363c --- /dev/null +++ b/samples/bench/dotnet/Program.cs @@ -0,0 +1,39 @@ +using System.Runtime.InteropServices.JavaScript; +using System.Text.Json; +using System.Text.Json.Serialization; + +public struct Data +{ + public string Info; + public bool Ok; + public int Revision; + public string[] Messages; +} + +[JsonSerializable(typeof(Data))] +internal partial class SourceGenerationContext : JsonSerializerContext; + +public static partial class Program +{ + public static void Main () { } + + [JSExport] + public static int EchoNumber () => GetNumber(); + + [JSExport] + public static string EchoStruct () + { + var json = GetStruct(); + var data = JsonSerializer.Deserialize(json, SourceGenerationContext.Default.Data); + return JsonSerializer.Serialize(data, SourceGenerationContext.Default.Data); + } + + [JSExport] + public static int Fi (int n) => n <= 1 ? n : Fi(n - 1) + Fi(n - 2); + + [JSImport("getNumber", "x")] + private static partial int GetNumber (); + + [JSImport("getStruct", "x")] + private static partial string GetStruct (); +} diff --git a/samples/bench/dotnet/init.mjs b/samples/bench/dotnet/init.mjs new file mode 100644 index 00000000..f3c385b0 --- /dev/null +++ b/samples/bench/dotnet/init.mjs @@ -0,0 +1,22 @@ +import { dotnet } from "./bin/Release/net9.0/browser-wasm/AppBundle/_framework/dotnet.js"; +import { getNumber, getStruct } from "../fixtures.mjs"; + +/** @returns {Promise} */ +export async function init() { + const runtime = await dotnet.withDiagnosticTracing(false).create(); + const asm = runtime.getConfig().mainAssemblyName; + + runtime.setModuleImports("x", { + getNumber, + getStruct: () => JSON.stringify(getStruct()) + }); + + await runtime.runMain(asm, []); + + const exports = await runtime.getAssemblyExports(asm); + return { + echoNumber: exports.Program.EchoNumber, + echoStruct: exports.Program.EchoStruct, + fi: exports.Program.Fi + }; +} diff --git a/samples/bench/dotnet/readme.md b/samples/bench/dotnet/readme.md new file mode 100644 index 00000000..fdd43f39 --- /dev/null +++ b/samples/bench/dotnet/readme.md @@ -0,0 +1,2 @@ +1. Install .NET https://dotnet.microsoft.com/en-us/download +2. Run `dotnet publish -c Release` diff --git a/samples/bench/fixtures.mjs b/samples/bench/fixtures.mjs new file mode 100644 index 00000000..d2f4e6ff --- /dev/null +++ b/samples/bench/fixtures.mjs @@ -0,0 +1,18 @@ +/** + * @typedef {Object} Data + * @property {string} info + * @property {boolean} ok + * @property {number} revision + * @property {string[]} messages + */ + +/** @returns {number} */ +export const getNumber = () => 42; + +/** @returns {Data} */ +export const getStruct = () => ({ + info: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + ok: true, + revision: -112, + messages: ["foo", "bar", "baz", "nya", "far"] +}); diff --git a/samples/bench/go/.gitignore b/samples/bench/go/.gitignore new file mode 100644 index 00000000..cfd8b495 --- /dev/null +++ b/samples/bench/go/.gitignore @@ -0,0 +1,2 @@ +*.wasm +wasm_exec.js diff --git a/samples/bench/go/init.mjs b/samples/bench/go/init.mjs new file mode 100644 index 00000000..a585ca1a --- /dev/null +++ b/samples/bench/go/init.mjs @@ -0,0 +1,20 @@ +import fs from "fs"; +import "./wasm_exec.js"; +import { getNumber, getStruct } from "../fixtures.mjs"; + +/** @returns {Promise} */ +export async function init() { + global.getNumber = getNumber; + global.getStruct = getStruct; + + const bin = await WebAssembly.compile(fs.readFileSync("./go/main.wasm")); + const go = new Go(); + const wasm = await WebAssembly.instantiate(bin, go.importObject); + go.run(wasm); + + return { + echoNumber: global.echoNumber, + echoStruct: global.echoStruct, + fi: global.fi + }; +} diff --git a/samples/bench/go/main.go b/samples/bench/go/main.go new file mode 100644 index 00000000..2368cdb5 --- /dev/null +++ b/samples/bench/go/main.go @@ -0,0 +1,46 @@ +package main + +import ( + "encoding/json" + "syscall/js" +) + +type Data struct { + Info string `json:"Info"` + Ok bool `json:"Ok"` + Revision int `json:"Revision"` + Messages []string `json:"Messages"` +} + +func main() { + js.Global().Set("echoNumber", js.FuncOf(echoNumber)) + js.Global().Set("echoStruct", js.FuncOf(echoStruct)) + js.Global().Set("fi", js.FuncOf(fi)) + <-make(chan struct{}) +} + +func echoNumber(_ js.Value, _ []js.Value) any { + return js.Global().Call("getNumber").Int() +} + +func echoStruct(_ js.Value, _ []js.Value) any { + jsonStr := js.Global().Call("getStruct").String() + var data Data + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return "Error: " + err.Error() + } + resultJson, _ := json.Marshal(data) + return string(resultJson) +} + +func fi(_ js.Value, args []js.Value) any { + n := args[0].Int() + return fibonacci(n) +} + +func fibonacci(n int) int { + if n <= 1 { + return n + } + return fibonacci(n-1) + fibonacci(n-2) +} diff --git a/samples/bench/go/readme.md b/samples/bench/go/readme.md new file mode 100644 index 00000000..899c7edc --- /dev/null +++ b/samples/bench/go/readme.md @@ -0,0 +1,3 @@ +1. Install Go https://go.dev/dl +2. Copy `{GO_INSTALL_DIR}/lib/wasm/wasm_exec.js` to this folder +3. Run `& { $env:GOOS="js"; $env:GOARCH="wasm"; go build -o main.wasm main.go }` diff --git a/samples/bench/package.json b/samples/bench/package.json new file mode 100644 index 00000000..905ea820 --- /dev/null +++ b/samples/bench/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "tinybench": "^4.0.1" + } +} diff --git a/samples/bench/readme.md b/samples/bench/readme.md new file mode 100644 index 00000000..5b53966f --- /dev/null +++ b/samples/bench/readme.md @@ -0,0 +1,53 @@ +1. Build each sub-dir (readme inside) +2. Run `npm i` +3. Run `npm bench.mjs` + +## 2024 (.NET 9) + +### Rust + +``` +┌─────────┬───────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬──────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼───────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼──────────┤ +│ 0 │ 'Echo Number' │ '97.04 ± 0.04%' │ '100.00 ± 0.00' │ '9886346 ± 0.01%' │ '10000000 ± 0' │ 10304915 │ +│ 1 │ 'Echo Struct' │ '2399.2 ± 1.57%' │ '2300.0 ± 0.00' │ '425825 ± 0.02%' │ '434783 ± 0' │ 416802 │ +│ 2 │ 'Fibonacci' │ '298097 ± 0.12%' │ '296400 ± 200.00' │ '3358 ± 0.09%' │ '3374 ± 2' │ 3355 │ +└─────────┴───────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴──────────┘ +``` + +### DotNet + +``` +┌─────────┬───────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼───────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤ +│ 0 │ 'Echo Number' │ '591.95 ± 0.49%' │ '600.00 ± 0.00' │ '1741653 ± 0.02%' │ '1666667 ± 0' │ 1689343 │ +│ 1 │ 'Echo Struct' │ '10813 ± 0.24%' │ '10700 ± 300.00' │ '93369 ± 0.04%' │ '93458 ± 2696' │ 92483 │ +│ 2 │ 'Fibonacci' │ '485910 ± 0.11%' │ '485200 ± 1500.0' │ '2059 ± 0.08%' │ '2061 ± 6' │ 2058 │ +└─────────┴───────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘ +``` + +### Bootsharp + +``` +┌─────────┬───────────────┬──────────────────┬───────────────────┬────────────────────────┬────────────────────────┬─────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼───────────────┼──────────────────┼───────────────────┼────────────────────────┼────────────────────────┼─────────┤ +│ 0 │ 'Echo Number' │ '603.58 ± 0.30%' │ '600.00 ± 0.00' │ '1690103 ± 0.01%' │ '1666667 ± 0' │ 1656786 │ +│ 1 │ 'Echo Struct' │ '13243 ± 6.52%' │ '12400 ± 100.00' │ '79792 ± 0.04%' │ '80645 ± 656' │ 75513 │ +│ 2 │ 'Fibonacci' │ '521873 ± 0.04%' │ '521700 ± 1500.0' │ '1916 ± 0.03%' │ '1917 ± 6' │ 1917 │ +└─────────┴───────────────┴──────────────────┴───────────────────┴────────────────────────┴────────────────────────┴─────────┘ +``` + +### Go + +``` +┌─────────┬───────────────┬───────────────────┬────────────────────┬────────────────────────┬────────────────────────┬─────────┐ +│ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ +├─────────┼───────────────┼───────────────────┼────────────────────┼────────────────────────┼────────────────────────┼─────────┤ +│ 0 │ 'Echo Number' │ '16583 ± 1.03%' │ '16300 ± 3600.0' │ '66140 ± 0.23%' │ '61350 ± 12806' │ 60303 │ +│ 1 │ 'Echo Struct' │ '22017 ± 13.76%' │ '18300 ± 2200.0' │ '55011 ± 0.15%' │ '54645 ± 6705' │ 45420 │ +│ 2 │ 'Fibonacci' │ '1254378 ± 0.23%' │ '1250500 ± 2200.0' │ '798 ± 0.15%' │ '800 ± 1' │ 798 │ +└─────────┴───────────────┴───────────────────┴────────────────────┴────────────────────────┴────────────────────────┴─────────┘ +``` \ No newline at end of file diff --git a/samples/bench/rust/.gitignore b/samples/bench/rust/.gitignore new file mode 100644 index 00000000..d1126b76 --- /dev/null +++ b/samples/bench/rust/.gitignore @@ -0,0 +1,3 @@ +pkg +target +Cargo.lock diff --git a/samples/bench/rust/Cargo.toml b/samples/bench/rust/Cargo.toml new file mode 100644 index 00000000..0555a1df --- /dev/null +++ b/samples/bench/rust/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "rust-wasm" +version = "0.1.0" +edition = "2024" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +wasm-bindgen = "0.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[profile.release] +codegen-units = 1 +lto = true +opt-level = 3 +panic = "abort" +strip = true diff --git a/samples/bench/rust/init.mjs b/samples/bench/rust/init.mjs new file mode 100644 index 00000000..c3aa2f49 --- /dev/null +++ b/samples/bench/rust/init.mjs @@ -0,0 +1,9 @@ +import { echoNumber, echoStruct, fi } from './pkg/rust_wasm.js'; +import { getNumber, getStruct } from "../fixtures.mjs"; + +/** @returns {Promise} */ +export async function init() { + global.getNumber = getNumber; + global.getStruct = () => JSON.stringify(getStruct()); + return { echoNumber, echoStruct, fi }; +} diff --git a/samples/bench/rust/readme.md b/samples/bench/rust/readme.md new file mode 100644 index 00000000..b74a82ff --- /dev/null +++ b/samples/bench/rust/readme.md @@ -0,0 +1,3 @@ +1. Install Rust https://rustup.rs +2. Install wasm-pack: https://rustwasm.github.io/wasm-pack +3. Run `wasm-pack build --target nodejs` diff --git a/samples/bench/rust/src/lib.rs b/samples/bench/rust/src/lib.rs new file mode 100644 index 00000000..b146087f --- /dev/null +++ b/samples/bench/rust/src/lib.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +#[derive(Serialize, Deserialize)] +pub struct Data { + pub info: String, + pub ok: bool, + pub revision: i32, + pub messages: Vec, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = getNumber)] + fn get_number() -> i32; + #[wasm_bindgen(js_name = getStruct)] + fn get_struct() -> String; +} + +#[wasm_bindgen(js_name = echoNumber)] +pub fn echo_number() -> i32 { + get_number() +} + +#[wasm_bindgen(js_name = echoStruct)] +pub fn echo_struct() -> String { + let json = get_struct(); + let data: Data = serde_json::from_str(&json).unwrap(); + serde_json::to_string(&data).unwrap() +} + +#[wasm_bindgen] +pub fn fi(n: i32) -> i32 { + if n <= 1 { + return n; + } + fi(n - 1) + fi(n - 2) +} From 25e3bdd4993795589bab54f2e4c23f81772deaf4 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 29 Mar 2025 19:27:51 +0300 Subject: [PATCH 12/12] formatting --- samples/bench/go/main.go | 48 ++++++++++++++++++++-------------------- samples/bench/readme.md | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/samples/bench/go/main.go b/samples/bench/go/main.go index 2368cdb5..a721a84b 100644 --- a/samples/bench/go/main.go +++ b/samples/bench/go/main.go @@ -1,46 +1,46 @@ package main import ( - "encoding/json" - "syscall/js" + "encoding/json" + "syscall/js" ) type Data struct { - Info string `json:"Info"` - Ok bool `json:"Ok"` - Revision int `json:"Revision"` - Messages []string `json:"Messages"` + Info string `json:"Info"` + Ok bool `json:"Ok"` + Revision int `json:"Revision"` + Messages []string `json:"Messages"` } func main() { - js.Global().Set("echoNumber", js.FuncOf(echoNumber)) - js.Global().Set("echoStruct", js.FuncOf(echoStruct)) - js.Global().Set("fi", js.FuncOf(fi)) - <-make(chan struct{}) + js.Global().Set("echoNumber", js.FuncOf(echoNumber)) + js.Global().Set("echoStruct", js.FuncOf(echoStruct)) + js.Global().Set("fi", js.FuncOf(fi)) + <-make(chan struct{}) } func echoNumber(_ js.Value, _ []js.Value) any { - return js.Global().Call("getNumber").Int() + return js.Global().Call("getNumber").Int() } func echoStruct(_ js.Value, _ []js.Value) any { - jsonStr := js.Global().Call("getStruct").String() - var data Data - if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { - return "Error: " + err.Error() - } - resultJson, _ := json.Marshal(data) - return string(resultJson) + jsonStr := js.Global().Call("getStruct").String() + var data Data + if err := json.Unmarshal([]byte(jsonStr), &data); err != nil { + return "Error: " + err.Error() + } + resultJson, _ := json.Marshal(data) + return string(resultJson) } func fi(_ js.Value, args []js.Value) any { - n := args[0].Int() - return fibonacci(n) + n := args[0].Int() + return fibonacci(n) } func fibonacci(n int) int { - if n <= 1 { - return n - } - return fibonacci(n-1) + fibonacci(n-2) + if n <= 1 { + return n + } + return fibonacci(n-1) + fibonacci(n-2) } diff --git a/samples/bench/readme.md b/samples/bench/readme.md index 5b53966f..ac3eccfe 100644 --- a/samples/bench/readme.md +++ b/samples/bench/readme.md @@ -50,4 +50,4 @@ │ 1 │ 'Echo Struct' │ '22017 ± 13.76%' │ '18300 ± 2200.0' │ '55011 ± 0.15%' │ '54645 ± 6705' │ 45420 │ │ 2 │ 'Fibonacci' │ '1254378 ± 0.23%' │ '1250500 ± 2200.0' │ '798 ± 0.15%' │ '800 ± 1' │ 798 │ └─────────┴───────────────┴───────────────────┴────────────────────┴────────────────────────┴────────────────────────┴─────────┘ -``` \ No newline at end of file +```