From df39df364a92c8812e5e73f1d2013ebae7c6954f Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+elringus@users.noreply.github.com> Date: Tue, 28 May 2024 20:15:57 +0300 Subject: [PATCH] fix serialization context (#156) --- .../Bootsharp.Common.Test.csproj | 10 +-- .../Bootsharp.Common.Test/SerializerTest.cs | 16 ++-- src/cs/Bootsharp.Common.Test/TypesTest.cs | 9 --- src/cs/Bootsharp.Common/Interop/Serializer.cs | 6 +- .../Bootsharp.Generate.Test.csproj | 12 +-- .../Bootsharp.Inject.Test.csproj | 10 +-- .../Bootsharp.Inject/Bootsharp.Inject.csproj | 2 +- .../Bootsharp.Publish.Test.csproj | 16 ++-- .../Emit/DependenciesTest.cs | 2 +- .../Emit/InterfacesTest.cs | 4 +- .../Emit/InteropTest.cs | 20 ++--- .../Emit/SerializerTest.cs | 64 +-------------- .../Pack/BindingTest.cs | 8 +- .../Pack/DeclarationTest.cs | 14 ++-- src/cs/Bootsharp.Publish.Test/TaskTest.cs | 5 ++ .../Bootsharp.Publish.csproj | 4 +- .../Common/Global/GlobalInspection.cs | 36 +++++++++ .../Common/Global/GlobalSerialization.cs | 55 +++++++++++++ .../GlobalText.cs} | 4 +- .../GlobalType.cs} | 80 +------------------ .../SolutionInspector/SolutionInspection.cs | 2 +- .../Common/TypeConverter/TypeConverter.cs | 3 + .../Emit/InteropGenerator.cs | 44 +++++----- .../Emit/SerializerGenerator.cs | 32 -------- .../Pack/BindingGenerator/BindingGenerator.cs | 24 +++--- src/cs/Directory.Build.props | 2 +- src/js/package.json | 10 +-- src/js/test/cs/Test/Functions.cs | 41 ++++++++++ src/js/test/spec/interop.spec.ts | 48 +++++++++-- 29 files changed, 300 insertions(+), 283 deletions(-) create mode 100644 src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs create mode 100644 src/cs/Bootsharp.Publish/Common/Global/GlobalSerialization.cs rename src/cs/Bootsharp.Publish/Common/{TextUtilities.cs => Global/GlobalText.cs} (87%) rename src/cs/Bootsharp.Publish/Common/{TypeUtilities.cs => Global/GlobalType.cs} (65%) diff --git a/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj b/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj index 0a66cd84..6bb47b48 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.Common.Test/SerializerTest.cs b/src/cs/Bootsharp.Common.Test/SerializerTest.cs index 386215bf..fd26c9c7 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("")).Message); + Assert.Throws(() => Serialize("", null)).Message); } [Fact] @@ -31,20 +31,20 @@ public void WhenTypeInfoNotAvailableThrowsError () TypeInfoResolver = new MockResolver() }; Assert.Contains("Failed to resolve serializer info", - Assert.Throws(() => Serialize("")).Message); + Assert.Throws(() => Serialize("", null)).Message); } [Fact] public void CanSerialize () { Assert.Equal("""{"items":[{"id":"foo"},{"id":"bar"}]}""", - Serialize(new MockRecord(new MockItem[] { new("foo"), new("bar") }))); + Serialize(new MockRecord(new MockItem[] { new("foo"), new("bar") }), typeof(MockRecord))); } [Fact] public void SerializesNullAsNull () { - Assert.Equal("null", Serialize(null)); + Assert.Equal("null", Serialize(null, null)); } [Fact] @@ -77,8 +77,8 @@ public void WhenDeserializationFailsErrorIsThrown () [Fact] public void RespectsOptions () { - Assert.Equal("{\"enum\":0}", Serialize(new MockItemWithEnum(MockEnum.Foo))); - Assert.Equal("{\"enum\":null}", Serialize(new MockItemWithEnum(null))); + 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) { @@ -86,8 +86,8 @@ public void RespectsOptions () Converters = { new JsonStringEnumConverter() }, TypeInfoResolver = new DefaultJsonTypeInfoResolver() }; - Assert.Equal("{\"enum\":\"Foo\"}", Serialize(new MockItemWithEnum(MockEnum.Foo))); - Assert.Equal("{}", Serialize(new MockItemWithEnum(null))); + 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); } diff --git a/src/cs/Bootsharp.Common.Test/TypesTest.cs b/src/cs/Bootsharp.Common.Test/TypesTest.cs index f706a478..e60a911f 100644 --- a/src/cs/Bootsharp.Common.Test/TypesTest.cs +++ b/src/cs/Bootsharp.Common.Test/TypesTest.cs @@ -11,15 +11,6 @@ public class TypesTest private readonly CustomAttributeData export = GetMockExportAttribute(); private readonly CustomAttributeData import = GetMockImportAttribute(); - [Fact] - public void Records () - { - // TODO: Remove when coverlet bug is resolved: https://github.com/coverlet-coverage/coverlet/issues/1561 - _ = new MockItem("") with { Id = "foo" }; - _ = new MockItemWithEnum(default) with { Enum = MockEnum.Bar }; - _ = new MockRecord(default) with { Items = new[] { new MockItem("") } }; - } - [Fact] public void TypesAreAssigned () { diff --git a/src/cs/Bootsharp.Common/Interop/Serializer.cs b/src/cs/Bootsharp.Common/Interop/Serializer.cs index 74d628dd..a15b5a5a 100644 --- a/src/cs/Bootsharp.Common/Interop/Serializer.cs +++ b/src/cs/Bootsharp.Common/Interop/Serializer.cs @@ -17,12 +17,12 @@ public static class Serializer }; /// - /// Serializes specified object to JSON string. + /// Serializes specified object to JSON string using specified serialization context type. /// - public static string Serialize (object? @object) + public static string Serialize (object? @object, Type type) { if (@object is null) return "null"; - return JsonSerializer.Serialize(@object, GetInfo(@object.GetType())); + return JsonSerializer.Serialize(@object, GetInfo(type)); } /// diff --git a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj index 742b45f6..00022940 100644 --- a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj +++ b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj @@ -16,19 +16,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.Inject.Test/Bootsharp.Inject.Test.csproj b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj index e0e26eb3..f409c828 100644 --- a/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj +++ b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj @@ -13,17 +13,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.Inject/Bootsharp.Inject.csproj b/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj index 8af681c1..adac40d0 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 09eb9248..30a8a5b8 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 a542fb74..f1e7bb15 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs @@ -72,7 +72,7 @@ public class Class Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedA"); Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedB"); // Export interop instances are not generated in C#; they're authored by user. - Assert.DoesNotContain("Bootsharp.Generated.Exports.JSExportedInstanced", TestedContent); + DoesNotContain("Bootsharp.Generated.Exports.JSExportedInstanced"); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs index b1e78d6a..df90858c 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs @@ -146,7 +146,7 @@ public class JSImported(global::System.Int32 _id) : global::IImported } } """); - Assert.DoesNotContain("JSExported", TestedContent); // Exported instances are authored by user and registered on initial interop. + DoesNotContain("JSExported"); // Exported instances are authored by user and registered on initial interop. } [Fact] @@ -297,6 +297,6 @@ public class Class } """)); Execute(); - Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase); + DoesNotContain("Foo"); } } diff --git a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs index f49c8b07..f1c8ffe3 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/InteropTest.cs @@ -26,8 +26,8 @@ internal static void RegisterProxies () public void GeneratesDisposeInstanceBindings () { Execute(); - Contains("JSExport] internal static void DisposeExportedInstance (global::System.Int32 id) => global::Bootsharp.Instances.Dispose(id);"); - Contains("""JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (global::System.Int32 id);"""); + Contains("JSExport] internal static void DisposeExportedInstance (int id) => global::Bootsharp.Instances.Dispose(id);"); + Contains("""JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (int id);"""); } [Fact] @@ -158,7 +158,7 @@ public class Class } """)); Execute(); - Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase); + DoesNotContain("Foo"); } [Fact] @@ -206,17 +206,17 @@ public class Class } """)); Execute(); - Contains("""Proxies.Set("Space.Class.FunA", (global::Space.Record a) => Deserialize(Space_Class_FunA(Serialize(a))));"""); - Contains("""Proxies.Set("Space.Class.FunB", async (global::Space.Record?[]? a) => Deserialize(await Space_Class_FunB(Serialize(a))));"""); - Contains("JSExport] internal static global::System.String Space_Class_InvA (global::System.String a) => Serialize(global::Space.Class.InvA(Deserialize(a)));"); - 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)));"); + 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("""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);"""); // TODO: Remove when resolved: https://github.com/elringus/bootsharp/issues/138 - Contains("""Proxies.Set("Space.Class.FunAsyncBytes", async () => Deserialize(await Space_Class_FunAsyncBytes()));"""); - Contains("JSExport] internal static async global::System.Threading.Tasks.Task Space_Class_InvAsyncBytes () => Serialize(await global::Space.Class.InvAsyncBytes());"); - Contains("""JSImport("Space.Class.funAsyncBytesSerialized", "Bootsharp")] internal static partial global::System.Threading.Tasks.Task Space_Class_FunAsyncBytes ();"""); + 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 ();"""); } [Fact] diff --git a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs index 6c3c78f0..55c38188 100644 --- a/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Emit/SerializerTest.cs @@ -19,7 +19,7 @@ public void WhenNoSerializableTypesIsEmpty () WithClass("[JSInvokable] public static byte[] Bar (int[] a, double[] b, string[] c) => default;") ); Execute(); - Assert.DoesNotContain("JsonSerializable", TestedContent); + DoesNotContain("JsonSerializable"); } [Fact] @@ -43,7 +43,7 @@ public class Class } """)); Execute(); - Assert.DoesNotContain("JsonSerializable", TestedContent); + DoesNotContain("JsonSerializable"); } [Fact] // .NET's generator indexes types by short names (w/o namespace) and fails on duplicates. @@ -65,64 +65,4 @@ public void AddsOnlyTopLevelTypesAndCrawledDuplicates () Contains("[JsonSerializable(typeof(global::n.Baz)"); Contains("[JsonSerializable(typeof(global::y.Struct)"); } - - [Fact] - public void AddsProxiesForListInterface () - { - AddAssembly(WithClass("[JSInvokable] public static void Foo (IList a) {}")); - Execute(); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.IList)"); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.List)"); - Contains("[JsonSerializable(typeof(global::System.String[])"); - } - - [Fact] - public void AddsProxiesForReadOnlyListInterface () - { - AddAssembly(WithClass("[JSInvokable] public static void Foo (IReadOnlyList a) {}")); - Execute(); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.IReadOnlyList)"); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.List)"); - Contains("[JsonSerializable(typeof(global::System.String[])"); - } - - [Fact] - public void AddsProxiesForDictInterface () - { - AddAssembly(WithClass("[JSInvokable] public static void Foo (IDictionary a) {}")); - Execute(); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.IDictionary)"); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.Dictionary)"); - } - - [Fact] - public void AddsProxiesForReadOnlyDictInterface () - { - AddAssembly(WithClass("[JSInvokable] public static void Foo (IReadOnlyDictionary a) {}")); - Execute(); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.IReadOnlyDictionary)"); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.Dictionary)"); - } - - [Fact] - public void DoesntAddProxiesForTaskWithoutResult () - { - AddAssembly(WithClass("[JSInvokable] public static Task Foo (Task bar) => default;")); - Execute(); - Assert.DoesNotContain("JsonSerializable", TestedContent); - } - - [Fact] - public void AddsProxiesForTaskWithResult () - { - AddAssembly( - With("public record Info;"), - WithClass("[JSInvokable] public static Task Foo () => default;"), - WithClass("[JSInvokable] public static Task> Bar () => default;")); - Execute(); - Contains("[JsonSerializable(typeof((global::Info, byte))"); - Contains("[JsonSerializable(typeof((global::System.Collections.Generic.IReadOnlyList, byte))"); - Contains("[JsonSerializable(typeof(global::System.Collections.Generic.List)"); - Contains("[JsonSerializable(typeof(global::System.Boolean[])"); - } } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs index 4ae65202..7e95cd9d 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/BindingTest.cs @@ -22,7 +22,7 @@ public void InteropFunctionsImported () import { Event } from "./event"; import { registerInstance, getInstance, disposeOnFinalize } from "./instances"; - function getExports () { if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); return exports; } + function getExports() { if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); return exports; } function serialize(obj) { return JSON.stringify(obj); } function deserialize(json) { const result = JSON.parse(json); if (result === null) return undefined; return result; } """); @@ -431,8 +431,8 @@ public static class Exports { [JSInvokable] public static void Inv () {} } public static class Imports { [JSFunction] public static void Fun () {} } """)); Execute(); - Assert.DoesNotContain("inv: () =>", TestedContent); - Assert.DoesNotContain("get fun()", TestedContent); + DoesNotContain("inv: () =>"); + DoesNotContain("get fun()"); } [Fact] @@ -579,6 +579,6 @@ public class Class } """)); Execute(); - Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase); + DoesNotContain("Foo"); } } diff --git a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs index 123bde3f..4e20a0a6 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/DeclarationTest.cs @@ -288,7 +288,9 @@ public void IntArraysTranslatedToRelatedTypes () 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 BigInt64 (long[] foo) {}"), + WithClass("[JSInvokable] public static void Float32 (float[] foo) {}"), + WithClass("[JSInvokable] public static void Float64 (double[] foo) {}")); Execute(); Contains("uint8(foo: Uint8Array): void"); Contains("int8(foo: Int8Array): void"); @@ -297,6 +299,8 @@ public void IntArraysTranslatedToRelatedTypes () Contains("uint32(foo: Uint32Array): void"); Contains("int32(foo: Int32Array): void"); Contains("bigInt64(foo: BigInt64Array): void"); + Contains("float32(foo: Float32Array): void"); + Contains("float64(foo: Float64Array): void"); } [Fact] @@ -856,9 +860,9 @@ public static class Exports { [JSInvokable] public static void Inv (Record r) {} public static class Imports { [JSFunction] public static void Fun () {} } """)); Execute(); - Assert.DoesNotContain("Record", TestedContent); - Assert.DoesNotContain("export function inv", TestedContent); - Assert.DoesNotContain("export let fun", TestedContent); + DoesNotContain("Record"); + DoesNotContain("export function inv"); + DoesNotContain("export let fun"); } [Fact] @@ -1044,6 +1048,6 @@ public class Class } """)); Execute(); - Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase); + DoesNotContain("Foo"); } } diff --git a/src/cs/Bootsharp.Publish.Test/TaskTest.cs b/src/cs/Bootsharp.Publish.Test/TaskTest.cs index 02298a89..e648699c 100644 --- a/src/cs/Bootsharp.Publish.Test/TaskTest.cs +++ b/src/cs/Bootsharp.Publish.Test/TaskTest.cs @@ -54,6 +54,11 @@ protected void Contains (string content) Assert.Contains(content, TestedContent); } + protected void DoesNotContain (string content) + { + Assert.DoesNotContain(content, TestedContent, StringComparison.OrdinalIgnoreCase); + } + protected MatchCollection Matches (string pattern) { Assert.Matches(pattern, TestedContent); diff --git a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj index 147a821f..5e3aed95 100644 --- a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj +++ b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj @@ -9,8 +9,8 @@ - - + + diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs new file mode 100644 index 00000000..21990bb7 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -0,0 +1,36 @@ +global using static Bootsharp.Publish.GlobalInspection; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.RegularExpressions; + +namespace Bootsharp.Publish; + +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")) + if (assemblyPaths.All(p => Path.GetFileName(p) != Path.GetFileName(path))) + assemblyPaths.Add(path); + var resolver = new PathAssemblyResolver(assemblyPaths); + return new MetadataLoadContext(resolver); + } + + public static bool ShouldIgnoreAssembly (string filePath) + { + var assemblyName = Path.GetFileName(filePath); + return assemblyName.StartsWith("System.") || + assemblyName.StartsWith("Microsoft.") || + assemblyName.StartsWith("netstandard") || + assemblyName.StartsWith("mscorlib"); + } + + public static string WithPrefs (IReadOnlyCollection prefs, string input, string @default) + { + foreach (var pref in prefs) + if (Regex.IsMatch(input, pref.Pattern)) + return Regex.Replace(input, pref.Pattern, pref.Replacement); + return @default; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalSerialization.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalSerialization.cs new file mode 100644 index 00000000..05f7eba9 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalSerialization.cs @@ -0,0 +1,55 @@ +global using static Bootsharp.Publish.GlobalSerialization; +using System.Collections.Frozen; + +namespace Bootsharp.Publish; + +internal static class GlobalSerialization +{ + private static readonly FrozenSet native = new[] { + typeof(string).FullName!, typeof(bool).FullName!, typeof(byte).FullName!, + typeof(char).FullName!, typeof(short).FullName!, typeof(long).FullName!, + typeof(int).FullName!, typeof(float).FullName!, typeof(double).FullName!, + typeof(nint).FullName!, typeof(Task).FullName!, typeof(DateTime).FullName!, + typeof(DateTimeOffset).FullName!, typeof(Exception).FullName! + }.ToFrozenSet(); + + private static readonly FrozenSet arrayNative = new[] { + typeof(byte).FullName!, typeof(int).FullName!, + typeof(double).FullName!, typeof(string).FullName! + }.ToFrozenSet(); + + public static string MarshalAmbiguous (ValueMeta meta, bool @return) + { + var typeSyntax = meta.TypeSyntax; + var promise = meta.TypeSyntax.StartsWith("global::System.Threading.Tasks.Task<"); + if (promise) typeSyntax = meta.TypeSyntax[36..]; + var result = ""; + if (ShouldMarshalAsAny(meta.Type)) result = "JSType.Any"; + else if (typeSyntax.StartsWith("global::System.DateTime")) result = "JSType.Date"; + else if (typeSyntax.StartsWith("global::System.Int64")) result = "JSType.BigInt"; + if (result == "") return ""; + if (promise) result = $"JSType.Promise<{result}>"; + result = $"JSMarshalAs<{result}>"; + if (@return) result = $"return: {result}"; + return $"[{result}] "; + } + + // https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop + public static bool ShouldSerialize (Type type) + { + if (type.IsEnum) return true; + if (IsVoid(type)) return false; + if (IsInstancedInteropInterface(type, out _)) return false; + if (IsTaskWithResult(type, out var result)) return ShouldSerialize(result); + var array = type.IsArray; + if (array) type = type.GetElementType()!; + if (IsNullable(type)) type = GetNullableUnderlyingType(type); + if (array) return !arrayNative.Contains(type.FullName!); + return !native.Contains(type.FullName!); + } + + // TODO: Remove once solved https://github.com/elringus/bootsharp/issues/138. + public static bool ShouldMarshalAsAny (Type type) => + IsTaskWithResult(type, out var result) && + result.IsArray && !ShouldSerialize(result.GetElementType()!); +} diff --git a/src/cs/Bootsharp.Publish/Common/TextUtilities.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs similarity index 87% rename from src/cs/Bootsharp.Publish/Common/TextUtilities.cs rename to src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs index 08db8622..0422ead9 100644 --- a/src/cs/Bootsharp.Publish/Common/TextUtilities.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalText.cs @@ -1,8 +1,8 @@ -global using static Bootsharp.Publish.TextUtilities; +global using static Bootsharp.Publish.GlobalText; namespace Bootsharp.Publish; -internal static class TextUtilities +internal static class GlobalText { public static string JoinLines (IEnumerable values, int indent = 1, string separator = "\n") { diff --git a/src/cs/Bootsharp.Publish/Common/TypeUtilities.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs similarity index 65% rename from src/cs/Bootsharp.Publish/Common/TypeUtilities.cs rename to src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index cf129e23..a74712a3 100644 --- a/src/cs/Bootsharp.Publish/Common/TypeUtilities.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -1,27 +1,11 @@ -global using static Bootsharp.Publish.TypeUtilities; -using System.Collections.Frozen; +global using static Bootsharp.Publish.GlobalType; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; namespace Bootsharp.Publish; -internal static class TypeUtilities +internal static class GlobalType { - private static readonly FrozenSet native = new[] { - typeof(string).FullName!, typeof(bool).FullName!, typeof(byte).FullName!, - typeof(char).FullName!, typeof(short).FullName!, typeof(long).FullName!, - typeof(int).FullName!, typeof(float).FullName!, typeof(double).FullName!, - typeof(nint).FullName!, typeof(Task).FullName!, typeof(DateTime).FullName!, - typeof(DateTimeOffset).FullName!, typeof(Exception).FullName! - }.ToFrozenSet(); - - private static readonly FrozenSet arrayNative = new[] { - typeof(byte).FullName!, typeof(int).FullName!, - typeof(double).FullName!, typeof(string).FullName! - }.ToFrozenSet(); - public static bool IsTaskLike (Type type) { return type.GetMethod(nameof(Task.GetAwaiter)) != null; @@ -33,20 +17,6 @@ public static bool IsTaskWithResult (Type type, [NotNullWhen(true)] out Type? re ? type.GenericTypeArguments[0] : null) != null; } - public static string MarshalAmbiguous (string typeSyntax, bool @return) - { - var promise = typeSyntax.StartsWith("global::System.Threading.Tasks.Task<"); - if (promise) typeSyntax = typeSyntax[36..]; - var result = - typeSyntax.StartsWith("global::System.DateTime") ? "JSType.Date" : - typeSyntax.StartsWith("global::System.Int64") ? "JSType.BigInt" : ""; - if (result == "") return ""; - if (promise) result = $"JSType.Promise<{result}>"; - result = $"JSMarshalAs<{result}>"; - if (@return) result = $"return: {result}"; - return $"[{result}] "; - } - public static bool IsVoid (Type type) { return type.FullName == "System.Void"; @@ -64,9 +34,9 @@ bool IsGenericList (Type type) => public static bool IsDictionary (Type type) { - return IsDict(type) || type.GetInterfaces().Any(IsDict); + return IsGenericDictionary(type) || type.GetInterfaces().Any(IsGenericDictionary); - bool IsDict (Type type) => + bool IsGenericDictionary (Type type) => type.IsGenericType && (type.GetGenericTypeDefinition().FullName == typeof(IDictionary<,>).FullName || type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); @@ -128,25 +98,6 @@ public static bool IsAutoProperty (PropertyInfo property) return backingField != null; } - public static MetadataLoadContext CreateLoadContext (string directory) - { - var assemblyPaths = Directory.GetFiles(RuntimeEnvironment.GetRuntimeDirectory(), "*.dll").ToList(); - foreach (var path in Directory.GetFiles(directory, "*.dll")) - if (assemblyPaths.All(p => Path.GetFileName(p) != Path.GetFileName(path))) - assemblyPaths.Add(path); - var resolver = new PathAssemblyResolver(assemblyPaths); - return new MetadataLoadContext(resolver); - } - - public static bool ShouldIgnoreAssembly (string filePath) - { - var assemblyName = Path.GetFileName(filePath); - return assemblyName.StartsWith("System.") || - assemblyName.StartsWith("Microsoft.") || - assemblyName.StartsWith("netstandard") || - assemblyName.StartsWith("mscorlib"); - } - public static string GetGenericNameWithoutArgs (string typeName) { var delimiterIndex = typeName.IndexOf('`'); @@ -163,21 +114,6 @@ public static bool IsInstancedInteropInterface (Type type, [NotNullWhen(true)] o return !type.Namespace.StartsWith("System.", StringComparison.Ordinal); } - // https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/import-export-interop - public static bool ShouldSerialize (Type type) - { - if (IsVoid(type)) return false; - if (IsInstancedInteropInterface(type, out _)) return false; - if (IsTaskWithResult(type, out var result)) - // TODO: Remove 'IsList(result)' when resolved: https://github.com/elringus/bootsharp/issues/138 - return IsList(result) || ShouldSerialize(result); - var array = type.IsArray; - if (array) type = type.GetElementType()!; - if (IsNullable(type)) type = GetNullableUnderlyingType(type); - if (array) return !arrayNative.Contains(type.FullName!); - return !native.Contains(type.FullName!); - } - public static string BuildJSSpace (Type type, Preferences prefs) { var space = type.Namespace ?? ""; @@ -225,14 +161,6 @@ public static string BuildJSInteropInstanceClassName (InterfaceMeta inter) return inter.FullName.Replace("Bootsharp.Generated.Exports.", "").Replace(".", "_"); } - public static string WithPrefs (IReadOnlyCollection prefs, string input, string @default) - { - foreach (var pref in prefs) - if (Regex.IsMatch(input, pref.Pattern)) - return Regex.Replace(input, pref.Pattern, pref.Replacement); - return @default; - } - public static string BuildSyntax (Type type) => BuildSyntax(type, null, false); public static string BuildSyntax (Type type, ParameterInfo info) => BuildSyntax(type, GetNullability(info)); diff --git a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs index 005289a0..88ad8463 100644 --- a/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/SolutionInspector/SolutionInspection.cs @@ -11,7 +11,7 @@ namespace Bootsharp.Publish; /// Shouldn't be disposed to keep C# reflection APIs usable on the inspected types. /// Dispose to remove file lock on the inspected assemblies. /// -internal class SolutionInspection (MetadataLoadContext ctx) : IDisposable +internal sealed class SolutionInspection (MetadataLoadContext ctx) : IDisposable { /// /// Interop interfaces specified under or diff --git a/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeConverter.cs b/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeConverter.cs index 8a50023c..dc8cb93f 100644 --- a/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeConverter.cs +++ b/src/cs/Bootsharp.Publish/Common/TypeConverter/TypeConverter.cs @@ -37,6 +37,7 @@ private string ConvertList (Type type) { var elementType = GetListElementType(type); if (EnterNullability()) return $"Array<{Convert(elementType)} | null>"; + if (!type.IsArray) return $"Array<{Convert(elementType)}>"; return Type.GetTypeCode(elementType) switch { TypeCode.Byte => "Uint8Array", TypeCode.SByte => "Int8Array", @@ -45,6 +46,8 @@ private string ConvertList (Type type) TypeCode.UInt32 => "Uint32Array", TypeCode.Int32 => "Int32Array", TypeCode.Int64 => "BigInt64Array", + TypeCode.Single => "Float32Array", + TypeCode.Double => "Float64Array", _ => $"Array<{Convert(elementType)}>" }; } diff --git a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs index 533b2c0b..5768fb86 100644 --- a/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/InteropGenerator.cs @@ -38,8 +38,8 @@ internal static void RegisterProxies () {{JoinLines(proxies, 2)}} } - [System.Runtime.InteropServices.JavaScript.JSExport] internal static void DisposeExportedInstance (global::System.Int32 id) => global::Bootsharp.Instances.Dispose(id); - [System.Runtime.InteropServices.JavaScript.JSImport("disposeInstance", "Bootsharp")] internal static partial void DisposeImportedInstance (global::System.Int32 id); + [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); {{JoinLines(methods)}} } @@ -49,7 +49,7 @@ internal static void RegisterProxies () private void AddExportMethod (MethodMeta inv) { var instanced = TryInstanced(inv, out var instance); - var marshalAs = MarshalAmbiguous(inv.ReturnValue.TypeSyntax, true); + var marshalAs = MarshalAmbiguous(inv.ReturnValue, true); var wait = ShouldWait(inv.ReturnValue); var attr = $"[System.Runtime.InteropServices.JavaScript.JSExport] {marshalAs}"; methods.Add($"{attr}internal static {BuildSignature()} => {BuildBody()};"); @@ -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})"; + else if (inv.ReturnValue.Serialized) body = $"Serialize({body}, {BuildSerdeType(inv.ReturnValue)})"; return body; } @@ -110,16 +110,13 @@ string BuildBody () return $"({BuildSyntax(method.ReturnValue.InstanceType)})new global::{full}({body})"; } if (!method.ReturnValue.Serialized) return body; - var type = method.ReturnValue.Async - ? method.ReturnValue.TypeSyntax[36..^1] - : method.ReturnValue.TypeSyntax; - return $"Deserialize<{type}>({body})"; + return $"Deserialize<{StripTaskSyntax(method.ReturnValue)}>({body})"; } string BuildBodyArg (ArgumentMeta arg) { if (arg.Value.Instance) return $"global::Bootsharp.Instances.Register({arg.Name})"; - if (arg.Value.Serialized) return $"Serialize({arg.Name})"; + if (arg.Value.Serialized) return $"Serialize({arg.Name}, {BuildSerdeType(arg.Value)})"; return arg.Name; } } @@ -131,30 +128,29 @@ private void AddImportMethod (MethodMeta method) var @return = BuildReturnValue(method.ReturnValue); var endpoint = $"{method.JSSpace}.{method.JSName}Serialized"; var attr = $"""[System.Runtime.InteropServices.JavaScript.JSImport("{endpoint}", "Bootsharp")]"""; - var date = MarshalAmbiguous(method.ReturnValue.TypeSyntax, true); - methods.Add($"{attr} {date}internal static partial {@return} {BuildMethodName(method)} ({args});"); + 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"; var nil = value.Nullable ? "?" : ""; - if (value.Instance) return $"global::System.Int32{(nil)}"; - if (value.Serialized) return $"global::System.String{(nil)}"; + if (value.Instance) return $"global::System.Int32{nil}"; + if (value.Serialized) return $"global::System.String{nil}"; return value.TypeSyntax; } private string BuildSignatureArg (ArgumentMeta arg) { var type = BuildValueType(arg.Value); - return $"{MarshalAmbiguous(arg.Value.TypeSyntax, false)}{type} {arg.Name}"; + return $"{MarshalAmbiguous(arg.Value, false)}{type} {arg.Name}"; } private string BuildReturnValue (ValueMeta value) { - var syntax = BuildValueType(value); - if (ShouldWait(value)) - syntax = $"global::System.Threading.Tasks.Task<{syntax}>"; + var syntax = ShouldMarshalAsAny(value.Type) ? "object" : BuildValueType(value); + if (ShouldWait(value)) syntax = $"global::System.Threading.Tasks.Task<{syntax}>"; return syntax; } @@ -171,6 +167,18 @@ private bool TryInstanced (MethodMeta method, [NotNullWhen(true)] out InterfaceM private bool ShouldWait (ValueMeta value) { - return value.Async && (value.Serialized || value.Instance); + return value.Async && (value.Serialized || ShouldMarshalAsAny(value.Type) || value.Instance); + } + + private string BuildSerdeType (ValueMeta value) + { + return $"typeof({StripTaskSyntax(value)})".Replace("?", ""); + } + + private string StripTaskSyntax (ValueMeta value) + { + return value.Async + ? value.TypeSyntax[36..^1] + : value.TypeSyntax; } } diff --git a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs index 97fd60f1..9a1a2eae 100644 --- a/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs +++ b/src/cs/Bootsharp.Publish/Emit/SerializerGenerator.cs @@ -53,7 +53,6 @@ private void CollectAttributes (string syntax, Type type) // 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)"; - AddProxies(type); attributes.Add(BuildAttribute(syntax)); } @@ -71,35 +70,4 @@ private static string BuildAttribute (string syntax) var hint = $"X{syntax.GetHashCode():X}"; return $"[JsonSerializable(typeof({syntax}), TypeInfoPropertyName = \"{hint}\")]"; } - - private void AddProxies (Type type) - { - if (IsTaskWithResult(type, out var result)) type = result; - if (IsListInterface(type)) AddListProxies(type); - if (IsDictInterface(type)) AddDictProxies(type); - } - - private void AddListProxies (Type list) - { - var element = BuildSyntax(list.GenericTypeArguments[0]); - attributes.Add(BuildAttribute($"{element}[]")); - attributes.Add(BuildAttribute($"global::System.Collections.Generic.List<{element}>")); - } - - private void AddDictProxies (Type dict) - { - var key = BuildSyntax(dict.GenericTypeArguments[0]); - var value = BuildSyntax(dict.GenericTypeArguments[1]); - attributes.Add(BuildAttribute($"global::System.Collections.Generic.Dictionary<{key}, {value}>")); - } - - private static bool IsListInterface (Type type) => - type.IsInterface && type.IsGenericType && - (type.GetGenericTypeDefinition().FullName == typeof(IList<>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyList<>).FullName); - - private static bool IsDictInterface (Type type) => - type.IsInterface && type.IsGenericType && - (type.GetGenericTypeDefinition().FullName == typeof(IDictionary<,>).FullName || - type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } diff --git a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs index 0294c1ec..f98143ff 100644 --- a/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/BindingGenerator/BindingGenerator.cs @@ -29,6 +29,7 @@ public string Generate (SolutionInspection inspection) .OrderBy(m => m.Namespace).ToArray(); if (bindings.Length == 0) return ""; EmitImports(); + builder.Append("\n\n"); if (inspection.InstancedInterfaces.Count > 0) builder.Append(classGenerator.Generate(inspection.InstancedInterfaces)); for (index = 0; index < bindings.Length; index++) @@ -36,16 +37,19 @@ public string Generate (SolutionInspection inspection) return builder.ToString(); } - private void EmitImports () - { - builder.Append("import { exports } from \"./exports\";\n"); - builder.Append("import { Event } from \"./event\";\n"); - builder.Append("import { registerInstance, getInstance, disposeOnFinalize } from \"./instances\";\n\n"); - builder.Append("function getExports () { if (exports == null) throw Error(\"Boot the runtime before invoking C# APIs.\"); return exports; }\n"); - builder.Append("function serialize(obj) { return JSON.stringify(obj); }\n"); - builder.Append("function deserialize(json) { const result = JSON.parse(json); if (result === null) return undefined; return result; }\n\n"); - builder.Append("/* v8 ignore start */\n"); - } + private void EmitImports () => builder.Append( + """ + import { exports } from "./exports"; + import { Event } from "./event"; + import { registerInstance, getInstance, disposeOnFinalize } from "./instances"; + + function getExports() { if (exports == null) throw Error("Boot the runtime before invoking C# APIs."); return exports; } + function serialize(obj) { return JSON.stringify(obj); } + function deserialize(json) { const result = JSON.parse(json); if (result === null) return undefined; return result; } + + /* v8 ignore start */ + """ + ); private void EmitBinding () { diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index bb3a4e21..5de7a961 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.3.1 + 0.3.2 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/package.json b/src/js/package.json index d03a7ef3..44ec6e62 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -6,11 +6,11 @@ "build": "sh scripts/build.sh" }, "devDependencies": { - "typescript": "^5.4.2", - "@types/node": "^20.11.25", + "typescript": "^5.4.5", + "@types/node": "^20.12.12", "@types/ws": "^8.5.10", - "vitest": "^1.3.1", - "@vitest/coverage-v8": "^1.3.1", - "ws": "^8.16.0" + "vitest": "^1.6.0", + "@vitest/coverage-v8": "^1.6.0", + "ws": "^8.17.0" } } diff --git a/src/js/test/cs/Test/Functions.cs b/src/js/test/cs/Test/Functions.cs index 4c8892e4..9e54ae6a 100644 --- a/src/js/test/cs/Test/Functions.cs +++ b/src/js/test/cs/Test/Functions.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Threading.Tasks; using Bootsharp; @@ -26,4 +27,44 @@ public static async Task EchoStringAsync () [JSFunction] public static partial byte[] GetBytes (); + + [JSInvokable] + public static async Task EchoBytesAsync (byte[] arr) + { + await Task.Delay(1); + return arr; + } + + [JSInvokable] + public static IReadOnlyList EchoColExprString (IReadOnlyList list) + { + return [..list]; + } + + [JSInvokable] + public static IReadOnlyList EchoColExprDouble (IReadOnlyList list) + { + return [..list]; + } + + [JSInvokable] + public static IReadOnlyList EchoColExprInt (IReadOnlyList list) + { + return [..list]; + } + + [JSInvokable] + public static IReadOnlyList EchoColExprByte (IReadOnlyList 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; } diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index a9f0f358..7bbe5981 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -46,6 +46,21 @@ describe("while bootsharp is booted", () => { const echo = Test.Functions.echoBytes(); expect(Test.Invokable.bytesToString(echo)).toStrictEqual("Everything's shiny, Captain. Not to fret."); }); + it("can transfer byte array async", async () => { + expect(await Test.Functions.echoBytesAsync(new Uint8Array([ + 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, + 0x2c, 0x20, 0x43, 0x61, 0x70, 0x74, 0x61, 0x69, 0x6e, + 0x2e, 0x20, 0x4e, 0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, + 0x66, 0x72, 0x65, 0x74, 0x2e + ]))).toStrictEqual(new Uint8Array([ + 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69, 0x6e, + 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, + 0x2c, 0x20, 0x43, 0x61, 0x70, 0x74, 0x61, 0x69, 0x6e, + 0x2e, 0x20, 0x4e, 0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, + 0x66, 0x72, 0x65, 0x74, 0x2e + ])); + }); it("can transfer structs", () => { const expected = { wheeled: [ @@ -61,15 +76,18 @@ describe("while bootsharp is booted", () => { expect(actual).toStrictEqual(expected); }); it("can transfer lists as arrays", async () => { - Test.Types.Registry.getRegistries = () => [{ wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }] }]; - const result = await Test.Types.Registry.concatRegistriesAsync([ - { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }] }, - { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }] } + Test.Types.Registry.getRegistries = () => [{ + wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], + tracked: [] + }]; + const result = await Test.Types.Registry.concatRegistriesAsync([ + { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }], tracked: [] }, + { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] } ]); expect(result).toStrictEqual([ - { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }] }, - { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }] }, - { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }] } + { wheeled: [{ id: "bar", maxSpeed: 1, wheelCount: 9 }], tracked: [] }, + { tracked: [{ id: "baz", maxSpeed: 5, trackType: TrackType.Rubber }], wheeled: [] }, + { wheeled: [{ id: "foo", maxSpeed: 1, wheelCount: 0 }], tracked: [] } ]); }); it("can transfer dictionaries as maps", async () => { @@ -88,6 +106,22 @@ describe("while bootsharp is booted", () => { bar: { wheeled: [{ id: "bar", maxSpeed: 15, wheelCount: 5 }] } }); }); + it("can transfer raw arrays", () => { + expect(Test.Functions.echoStringArray(["foo", "bar"])) + .toStrictEqual(["foo", "bar"]); + expect(Test.Functions.echoDoubleArray(new Float64Array([0.5, -1.9]))) + .toStrictEqual(new Float64Array([0.5, -1.9])); + expect(Test.Functions.echoIntArray(new Int32Array([1, 2]))) + .toStrictEqual(new Int32Array([1, 2])); + expect(Test.Functions.echoByteArray(new Uint8Array([1, 2]))) + .toStrictEqual(new Uint8Array([1, 2])); + }); + it("can transfer collection expressions", () => { + expect(Test.Functions.echoColExprString(["foo", "bar"])).toStrictEqual(["foo", "bar"]); + expect(Test.Functions.echoColExprDouble([0.5, -1.9])).toStrictEqual([0.5, -1.9]); + expect(Test.Functions.echoColExprInt([1, 2])).toStrictEqual([1, 2]); + expect(Test.Functions.echoColExprByte([1, 2])).toStrictEqual([1, 2]); + }); it("can invoke assigned JS functions in C#", () => { Test.Types.Registry.getRegistry = () => ({ wheeled: [{ id: "", maxSpeed: 1, wheelCount: 0 }],