diff --git a/docs/guide/declarations.md b/docs/guide/declarations.md index 543bc1fd..25870879 100644 --- a/docs/guide/declarations.md +++ b/docs/guide/declarations.md @@ -83,6 +83,40 @@ export namespace Class { ::: +## Generic Methods + +JavaScript does not have generic functions, so Bootsharp projects an exported generic method as a concrete overload for each user type that satisfies the method's type parameter constraint, suffixing each with `Of...` derived from the bound type's name. Only a single type parameter, constrained to a user type, is expanded. + +::: code-group + +```csharp [Class.cs] +public interface IShape {} +public class Circle : IShape {} +public class Square : IShape {} + +public class Class +{ + [Export] + public static T CreateShape () where T : IShape + { + if (typeof(T) == typeof(Circle)) return new Circle(); + if (typeof(T) == typeof(Square)) return new Square(); + } +} +``` + +```ts [index.g.d.mts] +export interface Circle {} +export interface Square {} + +export namespace Class { + export function createShapeOfCircle(): Circle; + export function createShapeOfSquare(): Square; +} +``` + +::: + ## Default Arguments C# method parameters with default values are emitted as optional TypeScript parameters using the `?:` syntax, letting callers omit them at the call site: diff --git a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs index ef5ab048..8e0ed139 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateCS/CSInteropTest.cs @@ -388,6 +388,51 @@ public class Class DoesNotContain("Foo"); } + [Fact] + public void GeneratesSupportedGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + [Export] public static void Mix (T shape, int n) where T : IShape {} + [Export] public static T? EchoNull (T? shape) where T : class, IShape => shape; + } + """)); + Execute(); + Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make())"); + Contains("Class_TakeOfCircle (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_TakeOfSquare (int shape) => global::Class.Take(Instances.Resolve(shape))"); + Contains("Class_MixOfCircle (int shape, global::System.Int32 n) => global::Class.Mix(Instances.Resolve(shape), n)"); + Contains("Class_EchoNullOfCircle (int shape) => Instances.Export(global::Class.EchoNull(Instances.Resolve(shape)))"); + } + + [Fact] + public void GeneratesGenericMethodsForCompatibleTypesInOtherAssemblies () + { + AddAssembly("Contracts.dll", With("public interface IShape {}")); + AddAssembly("Shapes.dll", With( + """ + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + } + """)); + Execute(); + Contains("Class_MakeOfCircle () => Instances.Export(global::Class.Make())"); + Contains("Class_MakeOfSquare () => Instances.Export(global::Class.Make())"); + } + [Fact] public void DoesNotSerializeTypesThatShouldNotBeSerialized () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs index b03c3aa3..5a3e2333 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/DeclarationTest.cs @@ -542,6 +542,28 @@ export interface GenericClass2 { """); } + [Fact] + public void GeneratedForGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public class Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + } + """)); + Execute(); + Contains("export function makeOfCircle(): Circle;"); + Contains("export function makeOfSquare(): Square;"); + Contains("export function takeOfCircle(shape: Circle): void;"); + Contains("export function takeOfSquare(shape: Square): void;"); + } + [Fact] public void GeneratesForDelegates () { diff --git a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs index a7072f6c..2db7af20 100644 --- a/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs +++ b/src/cs/Bootsharp.Publish.Test/GenerateJS/JSModuleTest.cs @@ -705,31 +705,101 @@ [Export] public static void Start (string title, string info, double progress) { """ export const Class = { foo: (a) => exports.Class_Foo(a), - fooWithA: (a) => exports.Class_Foo(a), - fooWithB: (b, a) => exports.Class_Foo(b, a), + fooWithA: (a) => exports.Class_FooWithA(a), + fooWithB: (b, a) => exports.Class_FooWithB(b, a), bar: (x) => exports.Class_Bar(x), - barWithName: (x, name) => exports.Class_Bar(x, name), + barWithName: (x, name) => exports.Class_BarWithName(x, name), baz: (x, y) => exports.Class_Baz(x, y), - bazWithXAndY: (x, y) => exports.Class_Baz(x, y), + bazWithXAndY: (x, y) => exports.Class_BazWithXAndY(x, y), qux: (a) => exports.Class_Qux(a), - quxWithB: (a, b) => exports.Class_Qux(a, b), - quxWithBAndC: (a, b, c) => exports.Class_Qux(a, b, c), + quxWithB: (a, b) => exports.Class_QuxWithB(a, b), + quxWithBAndC: (a, b, c) => exports.Class_QuxWithBAndC(a, b, c), x: (x, y) => exports.Class_X(x, y), - xWithStringAndInt32: (x, y) => exports.Class_X(x, y), - xWithInt32AndString: (x, y) => exports.Class_X(x, y), + xWithStringAndInt32: (x, y) => exports.Class_XWithStringAndInt32(x, y), + xWithInt32AndString: (x, y) => exports.Class_XWithInt32AndString(x, y), bob: (x, y, z) => exports.Class_Bob(x, y, z), - bobWithQ: (x, y, q) => exports.Class_Bob(x, y, q), + bobWithQ: (x, y, q) => exports.Class_BobWithQ(x, y, q), change: (progress) => exports.Class_Change(progress), - changeWithInfo: (info) => exports.Class_Change(info), - changeWithProgressAndInfo: (progress, info) => exports.Class_Change(progress, info), + changeWithInfo: (info) => exports.Class_ChangeWithInfo(info), + changeWithProgressAndInfo: (progress, info) => exports.Class_ChangeWithProgressAndInfo(progress, info), start: (title) => exports.Class_Start(title), - startWithInfo: (title, info) => exports.Class_Start(title, info), - startWithProgress: (title, progress) => exports.Class_Start(title, progress), - startWithInfoAndProgress: (title, info, progress) => exports.Class_Start(title, info, progress) + startWithInfo: (title, info) => exports.Class_StartWithInfo(title, info), + startWithProgress: (title, progress) => exports.Class_StartWithProgress(title, progress), + startWithInfoAndProgress: (title, info, progress) => exports.Class_StartWithInfoAndProgress(title, info, progress) }; """); } + [Fact] + public void GeneratesSupportedGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public record Square : IShape { public double Side { get; set; } } + + public class Class + { + [Export] public static T Make () where T : IShape => default!; + [Export] public static void Take (T shape) where T : IShape {} + } + """)); + Execute(); + Contains("makeOfCircle: () =>"); + Contains("makeOfSquare: () =>"); + Contains("takeOfCircle: (shape) =>"); + Contains("takeOfSquare: (shape) =>"); + Contains("exports.Class_MakeOfCircle()"); + Contains("exports.Class_MakeOfSquare()"); + Contains("exports.Class_TakeOfCircle("); + Contains("exports.Class_TakeOfSquare("); + } + + [Fact] + public void DiscardsUnsupportedGenericMethods () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + public static class Box { [Export] public static void Stored (T item) {} } + public class Class + { + [Export] public static void Pair () where T1 : IShape where T2 : IShape {} + [Export] public static void Take (T shape) where T : IShape {} + [Export] public static void Many (List items) where T : IShape {} + [Export] public static void Free () {} + [Export] public static void Real () {} + } + """)); + Execute(); + Contains("real:"); + Contains("takeOfCircle:"); + DoesNotContain("stored"); // method declared on a generic type + DoesNotContain("pair"); // multiple type parameters + DoesNotContain("many"); // type parameter used nested + DoesNotContain("free"); // type parameter not constrained to a user type + } + + [Fact] + public void DiscardsGeneratedTypesFromGenericCandidates () + { + AddAssembly(With( + """ + public interface IShape {} + public class Circle : IShape { public double Radius { get; set; } } + namespace Bootsharp.Generated { public class JS_Import_Leaked : global::IShape {} } + public class Class + { + [Export] public static T Make () where T : IShape => default!; + } + """)); + Execute(); + Contains("makeOfCircle:"); + DoesNotContain("makeOfJS_Import_Leaked"); + } + [Fact] public void RespectsPrefsInStatics () { diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs index 1b66c79e..f90df4a7 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalInspection.cs @@ -36,6 +36,7 @@ public static bool IsUserAssembly (string assemblyName) => public static bool IsUserType (Type type) { + if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return false; if (Preferences.IsSpecialized(type)) return true; if (IsDelegate(type)) return true; if (type.IsArray) return false; @@ -79,10 +80,14 @@ public bool HasBase (Type clr, [NotNullWhen(true)] out TypeMeta? bs) return bs != null && IsUserType(bs.Clr); } - public TypeMeta Get (Type clr) + public TypeMeta First (Type clr) { - var def = clr.IsGenericType ? clr.GetGenericTypeDefinition() : clr; - return types.First(t => (t.Clr.IsGenericType ? t.Clr.GetGenericTypeDefinition() : t.Clr) == def); + return types.First(t => IsSameType(t.Clr, clr)); + } + + public bool Has (Type clr) + { + return types.Any(t => IsSameType(t.Clr, clr)); } } } diff --git a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs index 1bb70f4e..96342fa6 100644 --- a/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs +++ b/src/cs/Bootsharp.Publish/Common/Global/GlobalType.cs @@ -77,6 +77,10 @@ static bool IsDictionary (Type type) => type.GetGenericTypeDefinition().FullName == typeof(IReadOnlyDictionary<,>).FullName); } + public static bool IsSameType (Type a, Type b) => + (a.IsGenericType ? a.GetGenericTypeDefinition() : a) == + (b.IsGenericType ? b.GetGenericTypeDefinition() : b); + public static NullabilityInfo GetNullity (PropertyInfo prop) => FixNullity(new NullabilityInfoContext().Create(prop), prop.CustomAttributes, prop.DeclaringType); public static NullabilityInfo GetNullity (ParameterInfo param) => diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs new file mode 100644 index 00000000..a3832c51 --- /dev/null +++ b/src/cs/Bootsharp.Publish/Common/Inspection/GenericDisambiguator.cs @@ -0,0 +1,76 @@ +using System.Reflection; + +namespace Bootsharp.Publish; + +/// +/// Rewrites associated with the generic methods expanding them into a concrete +/// overload for each user type compatible with the method's type parameter constraint. +/// +internal static class GenericDisambiguator +{ + public static void Disambiguate (TypeMeta[] types) + { + foreach (var surface in types.OfType()) + foreach (var method in surface.Members.OfType().ToArray()) + if (method.IK == InteropKind.Export && method.Info.ContainsGenericParameters) + Disambiguate(surface, method, types); + } + + private static void Disambiguate (SurfaceMeta surf, MethodMeta meth, TypeMeta[] types) + { + surf.MemberList.Remove(meth); + foreach (var expanded in Expand(meth, types)) + surf.MemberList.Add(expanded); + } + + private static IEnumerable Expand (MethodMeta meth, TypeMeta[] types) + { + if (!meth.Info.IsGenericMethodDefinition) yield break; // ignore methods declared on a generic type + if (meth.Info.GetGenericArguments() is not [{ } param]) yield break; // only single supported + if (param.GetGenericParameterConstraints().FirstOrDefault(IsUserType) is not { } ct) yield break; + if (HasNestedGeneric(meth.Info)) yield break; // type parameter nested in another type can't be rewritten + foreach (var compatible in GetCompatible(ct, types)) + yield return CloseGeneric(meth, compatible); + } + + private static bool HasNestedGeneric (MethodInfo meth) => meth + .GetParameters().Select(p => p.ParameterType).Prepend(meth.ReturnType) + .Any(t => t.ContainsGenericParameters && !t.IsGenericMethodParameter); + + private static IEnumerable GetCompatible (Type ct, TypeMeta[] types) => types + .Where(t => t is InstanceMeta or SerializedObjectMeta && !t.Clr.IsAbstract && ct.IsAssignableFrom(t.Clr)) + .DistinctBy(t => t.Clr); + + private static MethodMeta CloseGeneric (MethodMeta meth, TypeMeta closeType) + { + var closed = meth.Info.MakeGenericMethod(closeType.Clr); + return new MethodMeta(closed) { + Surf = meth.Surf, + IK = meth.IK, + Name = $"{meth.Name}<{closeType.Syntax}>", + Endpoint = $"{meth.Name}Of{closeType.Id}", + JSName = $"{meth.JSName}Of{closeType.Clr.Name}", + Args = meth.Args.Zip(closed.GetParameters(), RewriteArg).ToArray(), + Return = meth.Info.ReturnType.IsGenericMethodParameter ? RewriteValue(meth.Return) : meth.Return, + Void = meth.Void, + Async = meth.Async + }; + + ArgumentMeta RewriteArg (ArgumentMeta arg, ParameterInfo param) + { + if (!arg.Info.ParameterType.IsGenericMethodParameter) return arg; + var value = RewriteValue(arg.Value); + return new ArgumentMeta(param) { Name = arg.Name, JSName = arg.JSName, Value = value }; + } + + ValueMeta RewriteValue (ValueMeta value) => value with { + Type = closeType, + TypeSyntax = value.Nullable ? $"{closeType.Syntax}?" : closeType.Syntax + }; + } + + extension (SurfaceMeta srf) + { + private IList MemberList => (IList)srf.Members; + } +} diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs index aaf5fbe3..3afa5677 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/Meta/MemberMeta.cs @@ -41,6 +41,10 @@ internal record MethodMeta (MethodInfo Info) : MemberMeta /// public override MethodInfo Info { get; } = Info; /// + /// Identifier of the generated interop endpoint. + /// + public required string Endpoint { get; init; } + /// /// Arguments of the method. /// public required IReadOnlyList Args { get; init; } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs index 48f1a9bd..a98c74a1 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/OverloadDisambiguator.cs @@ -1,10 +1,13 @@ namespace Bootsharp.Publish; +/// +/// Renames associated with the overloaded methods making the names unique. +/// internal static class OverloadDisambiguator { - public static void Disambiguate (IEnumerable surfaces) + public static void Disambiguate (TypeMeta[] types) { - foreach (var surface in surfaces) + foreach (var surface in types.OfType()) foreach (var overloaded in surface.Members.OfType() .GroupBy(m => m.JSName).Where(g => g.Count() > 1)) Disambiguate(surface, overloaded.ToArray()); @@ -27,7 +30,7 @@ private static Dictionary MapArgs (IReadOnlyList a.JSName).ToHashSet(); return overloaded.Where(m => m != baseline).ToDictionary(m => m, m => m.Args .Where(a => !baselineArgs.Contains(a.JSName)) - .Select(a => ToFirstUpper(a.JSName)) + .Select(a => ToFirstUpper(a.Info.Name!)) // if an overload has extra args — use their names as discriminator .ToArray() is { Length: > 0 } extra ? extra : GetArgNames(m)); } @@ -38,7 +41,7 @@ private static IEnumerable FindAmbiguous (Dictionary g.Select(kv => kv.Key)); private static string[] GetArgNames (MethodMeta method) => method.Args - .Select(a => ToFirstUpper(a.JSName)).ToArray(); + .Select(a => ToFirstUpper(a.Info.Name!)).ToArray(); private static string[] GetArgTypes (MethodMeta method) => method.Args .Select(a => a.Value.Type.Clr.Name).ToArray(); @@ -48,9 +51,12 @@ private static MethodMeta ResolveBaseline (IReadOnlyList overloaded) .ThenBy(m => string.Join("|", m.Args.Select(a => a.Value.Type.Clr.FullName))) .First(); - private static void Rename (SurfaceMeta srf, MethodMeta method, string discriminator) + private static void Rename (SurfaceMeta srf, MethodMeta meth, string discriminator) { var members = (IList)srf.Members; - members[members.IndexOf(method)] = method with { JSName = $"{method.JSName}With{discriminator}" }; + members[members.IndexOf(meth)] = meth with { + JSName = $"{meth.JSName}With{discriminator}", + Endpoint = $"{meth.Endpoint}With{discriminator}" + }; } } diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs index b2618e6f..3c747f3f 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/SerializedInspector.cs @@ -39,7 +39,7 @@ public IReadOnlyList Collect () private static bool IsSerialized (Type type) { - if (IsVoid(type)) return false; + if (IsVoid(type) || type.ContainsGenericParameters) return false; if (IsNullable(type, out var value)) return IsSerialized(value); if (IsTaskWithResult(type, out var result)) return IsSerialized(result); return !native.Contains(type.FullName!); diff --git a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs index df3ece7a..f6288c3f 100644 --- a/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs +++ b/src/cs/Bootsharp.Publish/Common/Inspection/TypeInspector.cs @@ -1,4 +1,5 @@ using System.Reflection; +using System.Runtime.Loader; namespace Bootsharp.Publish; @@ -31,15 +32,16 @@ public void Inspect (Assembly assembly) public IReadOnlyCollection Collect () { - OverloadDisambiguator.Disambiguate([..surfaces, ..its.Values]); - TypeMeta[] specialized = [..surfaces, ..its.Values, ..srd.Collect()]; - var clrs = specialized.Select(t => t.Clr).ToHashSet(); - return Preferences.Rename([..specialized, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]); + TypeMeta[] types = [..surfaces, ..its.Values, ..srd.Collect()]; + GenericDisambiguator.Disambiguate(types); + OverloadDisambiguator.Disambiguate(types); + var clrs = types.Select(t => t.Clr).ToHashSet(); + return Preferences.Rename([..types, ..crawled.Values.Where(c => !clrs.Contains(c.Clr))]); } private StaticMeta? InspectStatic (Type type) { - if (type.Namespace?.StartsWith("Bootsharp.Generated") == true) return null; + if (!IsUserType(type)) return null; var members = new List(); var st = new StaticMeta(type) { Members = members }; var flags = BindingFlags.Public | BindingFlags.Static; @@ -86,7 +88,7 @@ static bool IsInstanced (Type type) { // Instanced types are mutable user types that are passed by reference when crossing the // interop boundary (as opposed to serialized immutable types, which are copied by value). - if (!IsUserType(type)) return false; + if (!IsUserType(type) || type.ContainsGenericParameters) return false; if (type.IsInterface || Preferences.IsSpecialized(type)) return true; return type.IsClass && !IsStatic(type) && !IsRecord(type); // records are immutable by convention } @@ -172,6 +174,7 @@ bool ShouldInspectMethod (MethodInfo method) IK = ik, Surf = srf, Name = BuildCSName(method.Name), + Endpoint = BuildCSName(method.Name), JSName = BuildJSName(method.Name), Args = method.GetParameters().Select(p => InspectArg(p, GetNullity(p), ik.Invert)).ToArray(), Return = InspectValue(method.ReturnParameter.ParameterType, GetNullity(method.ReturnParameter), ik), @@ -193,8 +196,7 @@ bool ShouldInspectMethod (MethodInfo method) private TypeMeta InspectType (Type type, InteropKind ik, NullabilityInfo? nul = null) { - for (var clr = type; clr.IsNested && IsUserType(clr.DeclaringType!); clr = clr.DeclaringType!) - crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); + CrawlInspected(type, ik); return InspectInstance(type, ik, nul) ?? srd.Inspect(type, ik) ?? new TypeMeta(type); } @@ -213,6 +215,24 @@ private SurfaceProxy BuildProxy (Type type, string typeId, InteropKind ik, Speci }; } + private void CrawlInspected (Type type, InteropKind ik) + { + for (var clr = type; clr.IsNested && IsUserType(clr.DeclaringType!); clr = clr.DeclaringType!) + crawled.TryAdd(clr.DeclaringType!, new(clr.DeclaringType!)); + if (type.IsGenericMethodParameter) + foreach (var compatible in FindCompatible(type)) + InspectType(compatible, ik); + + static IEnumerable FindCompatible (Type param) + { + foreach (var ct in param.GetGenericParameterConstraints().Where(IsUserType)) + foreach (var ass in AssemblyLoadContext.GetLoadContext(ct.Assembly)!.Assemblies) + foreach (var clr in ass.GetExportedTypes()) + if (IsUserType(clr) && !clr.IsAbstract && !clr.ContainsGenericParameters && ct.IsAssignableFrom(clr)) + yield return clr; + } + } + private InteropKind? ResolveIK (MemberInfo info) { foreach (var attr in info.CustomAttributes) diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs index 7b69208d..b8bd45cb 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInstanceGenerator.cs @@ -160,10 +160,10 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = PrependIdArg(string.Join(", ", method.Args.Select(a => a.Name))); - var name = $"{it.Proxy.Id}_{method.Name}"; - var head = it.Proxy is SpecializedProxy + var name = $"{it.Proxy.Id}_{method.Endpoint}"; + var sig = it.Proxy is SpecializedProxy ? $"public override {method.Return.TypeSyntax} {method.Name}" : $"{method.Return.TypeSyntax} {it.Syntax}.{method.Name}"; - return $"{head} ({args}) => global::Bootsharp.Generated.Interop.{name}({callArgs});"; + return $"{sig} ({args}) => global::Bootsharp.Generated.Interop.{name}({callArgs});"; } } diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs index cba39481..c6c95b19 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSInteropGenerator.cs @@ -43,7 +43,7 @@ private static IEnumerable EmitInitializers (SurfaceMeta srf) yield return $"{stx}.{evt.Name} += Handle_{id}_{evt.Name};"; if (srf is not StaticMeta) yield break; foreach (var mem in srf.Members.OfType().Where(m => m.IK == InteropKind.Import)) - yield return $"{stx}.Bootsharp_{mem.Name} = &{id}_{mem.Name};"; + yield return $"{stx}.Bootsharp_{mem.Name} = &{id}_{mem.Endpoint};"; foreach (var p in srf.Members.OfType().Where(p => p.IK == InteropKind.Import)) { if (p.CanGet) yield return $"{stx}.Bootsharp_Get{p.Name} = &{id}_Get{p.Name};"; @@ -156,14 +156,14 @@ private IEnumerable EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); var attr = $"[JSExport] {MarshalAmbiguous(method.Return, true)}"; - var name = $"{id}_{method.Name}"; + var name = $"{id}_{method.Endpoint}"; var @return = BuildValueSyntax(method.Return); if (wait) @return = $"async global::System.Threading.Tasks.Task<{@return}>"; var sigArgs = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); if (isIt) sigArgs = $"int {PrependIdArg(sigArgs)}"; var invArgs = string.Join(", ", method.Args.Select(ImportCS)); - var invName = isIt - ? $"Instances.Exported<{key}>(_id).{method.Name}" + var invName = isIt ? $"Instances.Exported<{key}>(_id).{method.Name}" + : isMd ? $"{stx}.{method.Endpoint}" : $"{stx}.{method.Name}"; var body = ExportCS(method.Return, $"{(wait ? "await " : "")}{invName}({invArgs})"); yield return $"{attr}internal static {@return} {name} ({sigArgs}) => {body};"; @@ -173,7 +173,7 @@ private IEnumerable EmitMethodImport (MethodMeta method) { var attr = $"""[JSImport("{srf.JSNode}.{method.JSName}Serialized", "{srf.JSModule}")]"""; var marshalAs = MarshalAmbiguous(method.Return, true); - var name = $"{id}_{method.Name}"; + var name = $"{id}_{method.Endpoint}"; var @return = BuildValueSyntax(method.Return); if (ShouldWait(method)) @return = $"global::System.Threading.Tasks.Task<{@return}>"; var args = string.Join(", ", method.Args.Select(a => BuildParameter(a.Value, a.Name))); diff --git a/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs index 3ef65f31..08c64fe7 100644 --- a/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateCS/CSModuleGenerator.cs @@ -124,7 +124,7 @@ private string EmitPropertyImport (PropertyMeta prop) private string EmitMethodExport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); - var sig = $"public static {method.Return.TypeSyntax} {method.Name} ({args})"; + var sig = $"public static {method.Return.TypeSyntax} {method.Endpoint} ({args})"; var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); return $"[Export] {sig} => handler.{method.Name}({callArgs});"; } @@ -133,7 +133,7 @@ private string EmitMethodImport (MethodMeta method) { var args = string.Join(", ", method.Args.Select(a => $"{a.Value.TypeSyntax} {a.Name}")); var callArgs = string.Join(", ", method.Args.Select(a => a.Name)); - var name = $"{md.Proxy.Id}_{method.Name}"; + var name = $"{md.Proxy.Id}_{method.Endpoint}"; return $"{method.Return.TypeSyntax} {md.Syntax}.{method.Name} ({args}) => " + $"global::Bootsharp.Generated.Interop.{name}({callArgs});"; } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs index e687e874..126bf5d8 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/Declarations/DeclarationGenerator.cs @@ -109,7 +109,7 @@ private void DeclareInstance (InstanceMeta it) string BuildExtensions () { if (it.Proxy is SpecializedProxy) return ""; // specialized surfaces are self-contained - var ext = it.Clr.GetInterfaces().Where(IsUserType).ToList(); + var ext = it.Clr.GetInterfaces().Where(i => IsUserType(i) && spec.Types.Has(i)).ToList(); if (spec.Types.HasBase(it.Clr, out var bs)) ext.Insert(0, bs.Clr); return ext.Count == 0 ? "" : $" extends {string.Join(", ", ext.Select(ts.BuildFullName))}"; } diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs index 1d56a49f..77bebfc7 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModuleGenerator.cs @@ -152,7 +152,7 @@ private void EmitPropertyImport (PropertyMeta prop) private void EmitMethodExport (MethodMeta method) { var wait = ShouldWait(method); - var fnName = $"{id}_{method.Name}"; + var fnName = $"{id}_{method.Endpoint}"; var invName = debug ? $"""getExport("{fnName}")""" : $"exports.{fnName}"; var args = string.Join(", ", method.Args.Select(a => a.JSName)); if (isIt) args = PrependIdArg(args); diff --git a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs index 53f3e8c7..81717635 100644 --- a/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs +++ b/src/cs/Bootsharp.Publish/GenerateJS/JSModules/JSModules.cs @@ -38,7 +38,7 @@ public string Ref (TypeMeta type, JSModule? fromMd = null) /// public string Ref (Type clr, JSModule? fromMd = null) { - return Ref(types.Get(clr), fromMd); + return Ref(types.First(clr), fromMd); } /// diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e9523d02..98f2c319 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.0-alpha.440 + 0.8.0-alpha.450 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/test/cs/Test/Serialization.cs b/src/js/test/cs/Test/Serialization.cs index 40dc05d6..d0ffeadf 100644 --- a/src/js/test/cs/Test/Serialization.cs +++ b/src/js/test/cs/Test/Serialization.cs @@ -62,6 +62,7 @@ public static class Serialization { [Export] public static Primitives?[]? EchoPrimitives (Primitives?[]? value) => value; [Export] public static Union?[]? EchoUnions (Union?[]? value) => value; + [Export] public static Static.Square?[]? EchoSquares (Static.Square?[]? value) => value; [Export] public static byte[]? EchoBytes (byte[]? value) => value; [Export] public static int[]? EchoIntArray (int[]? value) => value; [Export] public static double[]? EchoDoubleArray (double[]? value) => value; @@ -73,11 +74,8 @@ public static class Serialization [Export] public static List?[]? EchoNestedIntList (List?[]? value) => value; [Export] public static Dictionary? EchoDictionary (Dictionary? value) => value; [Export] public static Dictionary?[]? EchoNestedDictionary (Dictionary?[]? value) => value; - [Export] public static IList EchoListInterface (IList value) => value; [Export] public static IReadOnlyList EchoReadOnlyList (IReadOnlyList value) => value; - [Export] public static ICollection EchoCollection (ICollection value) => value; [Export] public static IReadOnlyCollection EchoReadOnlyCollection (IReadOnlyCollection value) => value; - [Export] public static IDictionary EchoDictionaryInterface (IDictionary value) => value; [Export] public static IReadOnlyDictionary EchoReadOnlyDictionary (IReadOnlyDictionary value) => value; [Export] diff --git a/src/js/test/cs/Test/Static.cs b/src/js/test/cs/Test/Static.cs index 3d869cd7..f1617e96 100644 --- a/src/js/test/cs/Test/Static.cs +++ b/src/js/test/cs/Test/Static.cs @@ -12,6 +12,23 @@ public static partial class Static { public enum Enum { One = 1, Two = 2 } + public interface IShape + { + string Name { get; set; } + } + + public class Circle : IShape + { + public string Name { get; set; } = "circle"; + public double GetRadius () => 3.14; + } + + public record Square : IShape + { + public string Name { get; set; } = "square"; + public double Area { get; set; } + } + [Import] public static event Action? ImportedEvent; [Export] public static event Action? ExportedEvent; @@ -28,6 +45,11 @@ public enum Enum { One = 1, Two = 2 } [Export] public static void BroadcastExportedEvent (string? payload) => ExportedEvent?.Invoke(payload); [Export] public static DateTime AddDays (DateTime date, int days) => date.AddDays(days); [Export] public static Enum GetEnum (int idx) => (Enum)idx; + [Export] public static T MakeGeneric () where T : IShape, new() => new(); + [Export] public static T EchoGeneric (T shape) where T : IShape => shape; + [Export] public static string Combine (int a) => $"int:{a}"; + [Export] public static string Combine (string a) => $"str:{a}"; + [Export] public static string Combine (int a, int b) => $"sum:{a + b}"; [Export] public static async Task CanInteropWithImportedStaticsAsync () diff --git a/src/js/test/spec/interop.spec.ts b/src/js/test/spec/interop.spec.ts index 18c0181a..a84480cd 100644 --- a/src/js/test/spec/interop.spec.ts +++ b/src/js/test/spec/interop.spec.ts @@ -296,4 +296,23 @@ describe("while bootsharp is booted", () => { const actual = new Date(Static.addDays(date, 7)); expect(actual).toStrictEqual(expected); }); + it("can interop with generic methods", () => { + const circle = Static.makeGenericOfCircle(); + expect(circle.name).toBe("circle"); + circle.name = "foo"; + expect(circle.name).toBe("foo"); + expect(circle.getRadius()).toBe(3.14); + expect(Static.echoGenericOfCircle(circle)).toBe(circle); + const square = Static.makeGenericOfSquare(); + expect(square.name).toBe("square"); + expect(square.area).toBe(0); + const echoed = Static.echoGenericOfSquare(square); + expect(echoed).toEqual(square); + expect(echoed).not.toBe(square); + }); + it("can interop with overloaded methods", () => { + expect(Static.combine(42)).toBe("int:42"); + expect(Static.combineWithA("foo")).toBe("str:foo"); + expect(Static.combineWithB(1, 2)).toBe("sum:3"); + }); }); diff --git a/src/js/test/spec/serialization.spec.ts b/src/js/test/spec/serialization.spec.ts index 3a6448a1..235da536 100644 --- a/src/js/test/spec/serialization.spec.ts +++ b/src/js/test/spec/serialization.spec.ts @@ -52,6 +52,11 @@ describe("serialization", () => { }; expect(Serialization.echoPrimitives([input])).toStrictEqual([input]); }); + it("can echo squares", () => { + expect(Serialization.echoSquares([{ name: "square", area: 4 }, null])) + .toStrictEqual([{ name: "square", area: 4 }, null]); + expect(Serialization.echoSquares(undefined)).toBeNull(); + }); it("can echo unions", () => { const a: Union = { shared: "A", a: { string: "*", map: new Map([["a", null], ["b", 7]]) } }; const b: Union = { shared: "B", b: { ints: [], strings: ["foo", "bar"], times: [new Date()] } }; @@ -96,9 +101,7 @@ describe("serialization", () => { expect(Serialization.echoIntList([1, 2, 3])).toStrictEqual([1, 2, 3]); expect(Serialization.echoStringList(["a", null, "", "b"])).toStrictEqual(["a", null, "", "b"]); expect(Serialization.echoNestedIntList([[1, 2], null, []])).toStrictEqual([[1, 2], null, []]); - expect(Serialization.echoListInterface(["a", "b", "c"])).toStrictEqual(["a", "b", "c"]); expect(Serialization.echoReadOnlyList([1, 2, 3])).toStrictEqual([1, 2, 3]); - expect(Serialization.echoCollection(["a", "b", "c"])).toStrictEqual(["a", "b", "c"]); expect(Serialization.echoReadOnlyCollection([1, 2, 3])).toStrictEqual([1, 2, 3]); expect(Serialization.echoIntList(undefined)).toBeNull(); expect(Serialization.echoStringList(undefined)).toBeNull(); @@ -109,8 +112,6 @@ describe("serialization", () => { .toStrictEqual(new Map([["1", "a"], ["2", null], ["3", ""]])); expect(Serialization.echoNestedDictionary([new Map([["1", "a"]]), null, new Map([["2", null]])])) .toStrictEqual([new Map([["1", "a"]]), null, new Map([["2", null]])]); - expect(Serialization.echoDictionaryInterface(new Map([["a", "b"], ["c", "d"]]))) - .toStrictEqual(new Map([["a", "b"], ["c", "d"]])); expect(Serialization.echoReadOnlyDictionary(new Map([["a", "b"], ["c", "d"]]))) .toStrictEqual(new Map([["a", "b"], ["c", "d"]])); expect(Serialization.echoDictionary(undefined)).toBeNull();