Skip to content

Commit

Permalink
instance interop (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
elringus committed Feb 5, 2024
1 parent a5cc885 commit 4656f67
Show file tree
Hide file tree
Showing 60 changed files with 1,621 additions and 432 deletions.
42 changes: 42 additions & 0 deletions src/cs/Bootsharp.Common.Test/InstancesTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using static Bootsharp.Instances;

namespace Bootsharp.Common.Test;

public class InstancesTest
{
[Fact]
public void ThrowsWhenGettingUnregisteredInstance ()
{
Assert.Throws<Error>(() => Get(0));
}

[Fact]
public void ThrowsWhenDisposingUnregisteredInstance ()
{
Assert.Throws<Error>(() => Dispose(0));
}

[Fact]
public void CanRegisterGetAndDisposeInstance ()
{
var instance = new object();
var id = Register(instance);
Assert.Same(instance, Get(id));
Dispose(id);
Assert.Throws<Error>(() => Get(id));
}

[Fact]
public void GeneratesUniqueIdsOnEachRegister ()
{
Assert.NotEqual(Register(new object()), Register(new object()));
}

[Fact]
public void ReusesIdOfDisposedInstance ()
{
var id = Register(new object());
Dispose(id);
Assert.Equal(id, Register(new object()));
}
}
46 changes: 46 additions & 0 deletions src/cs/Bootsharp.Common/Interop/Instances.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
namespace Bootsharp;

/// <summary>
/// Manages exported (C# -> JavaScript) instanced interop interfaces.
/// </summary>
public static class Instances
{
private static readonly Dictionary<int, object> idToInstance = [];
private static readonly Queue<int> idPool = [];
private static int nextId = int.MinValue;

/// <summary>
/// Registers specified interop instance and associates it with unique ID.
/// </summary>
/// <param name="instance">The instance to register.</param>
/// <returns>Unique ID associated with the registered instance.</returns>
public static int Register (object instance)
{
var id = idPool.Count > 0 ? idPool.Dequeue() : nextId++;
idToInstance[id] = instance;
return id;
}

/// <summary>
/// Resolves registered instance by the specified ID.
/// </summary>
/// <param name="id">Unique ID of the instance to resolve.</param>
public static object Get (int id)
{
if (!idToInstance.TryGetValue(id, out var instance))
throw new Error($"Failed to resolve exported interop instance with '{id}' ID: not registered.");
return instance;
}

/// <summary>
/// Notifies that interop instance is no longer used on JavaScript side
/// (eg, was garbage collected) and can be released on C# side as well.
/// </summary>
/// <param name="id">ID of the disposed interop instance.</param>
public static void Dispose (int id)
{
if (!idToInstance.Remove(id))
throw new Error($"Failed to dispose exported interop instance with '{id}' ID: not registered.");
idPool.Enqueue(id);
}
}
54 changes: 39 additions & 15 deletions src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public class DependenciesTest : EmitTest
protected override string TestedContent => GeneratedDependencies;

[Fact]
public void WhenNothingInspectedIncludesCommonDependencies ()
public void AddsCommonDependencies ()
{
Execute();
Contains(
Expand All @@ -28,31 +28,55 @@ public static class Dependencies
}

[Fact]
public void AddsGeneratedExportTypes ()
public void AddsStaticInteropInterfaceImplementations ()
{
AddAssembly(
With("[assembly:JSExport(typeof(IFoo), typeof(Space.IBar))]"),
With("public interface IFoo {}"),
With("Space", "public interface IBar {}"));
With("[assembly:JSExport(typeof(IExported), typeof(Space.IExported))]"),
With("[assembly:JSImport(typeof(IImported), typeof(Space.IImported))]"),
With("public interface IExported {}"),
With("public interface IImported {}"),
With("Space", "public interface IExported {}"),
With("Space", "public interface IImported {}"));
Execute();
Added(All, "Bootsharp.Generated.Exports.JSFoo");
Added(All, "Bootsharp.Generated.Exports.Space.JSBar");
Added(All, "Bootsharp.Generated.Exports.JSExported");
Added(All, "Bootsharp.Generated.Exports.Space.JSExported");
Added(All, "Bootsharp.Generated.Imports.JSImported");
Added(All, "Bootsharp.Generated.Imports.Space.JSImported");
}

[Fact]
public void AddsGeneratedImportTypes ()
public void AddsInstancedInteropInterfaceImplementations ()
{
AddAssembly(
With("[assembly:JSImport(typeof(IFoo), typeof(Space.IBar))]"),
With("public interface IFoo {}"),
With("Space", "public interface IBar {}"));
AddAssembly(With(
"""
[assembly:JSExport(typeof(IExportedStatic))]
[assembly:JSImport(typeof(IImportedStatic))]

public interface IExportedStatic { IExportedInstancedA CreateExported (); }
public interface IImportedStatic { IImportedInstancedA CreateImported (); }

public interface IExportedInstancedA { }
public interface IExportedInstancedB { }
public interface IImportedInstancedA { }
public interface IImportedInstancedB { }

public class Class
{
[JSInvokable] public static IExportedInstancedB CreateExported () => default;
[JSFunction] public static IImportedInstancedB CreateImported () => default;
}
"""));
Execute();
Added(All, "Bootsharp.Generated.Imports.JSFoo");
Added(All, "Bootsharp.Generated.Imports.Space.JSBar");
Added(All, "Bootsharp.Generated.Exports.JSExportedStatic");
Added(All, "Bootsharp.Generated.Imports.JSImportedStatic");
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);
}

[Fact]
public void AddsClassesWithInteropMethods ()
public void AddsClassesWithStaticInteropMethods ()
{
AddAssembly("Assembly.With.Dots.dll",
With("SpaceA", "public class ClassA { [JSInvokable] public static void Foo () {} }"),
Expand Down
79 changes: 68 additions & 11 deletions src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public class InterfacesTest : EmitTest
protected override string TestedContent => GeneratedInterfaces;

[Fact]
public void GeneratesInteropClassForExportedInterface ()
public void GeneratesImplementationForExportedStaticInterface ()
{
AddAssembly(With(
"""
Expand Down Expand Up @@ -61,7 +61,7 @@ internal static void RegisterInterfaces ()
}

[Fact]
public void GeneratesImplementationForImportedInterface ()
public void GeneratesImplementationForImportedStaticInterface ()
{
AddAssembly(With(
"""
Expand All @@ -85,11 +85,11 @@ namespace Bootsharp.Generated.Imports
{
public class JSImported : global::IImported
{
[JSFunction] public static void Inv (global::System.String? a) => Proxies.Get<Action<global::System.String?>>("Bootsharp.Generated.Imports.JSImported.Inv")(a);
[JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => Proxies.Get<Func<global::System.Threading.Tasks.Task>>("Bootsharp.Generated.Imports.JSImported.InvAsync")();
[JSFunction] public static global::Record? InvRecord () => Proxies.Get<Func<global::Record?>>("Bootsharp.Generated.Imports.JSImported.InvRecord")();
[JSFunction] public static global::System.Threading.Tasks.Task<global::System.String> InvAsyncResult () => Proxies.Get<Func<global::System.Threading.Tasks.Task<global::System.String>>>("Bootsharp.Generated.Imports.JSImported.InvAsyncResult")();
[JSFunction] public static global::System.String[] InvArray (global::System.Int32[] a) => Proxies.Get<Func<global::System.Int32[], global::System.String[]>>("Bootsharp.Generated.Imports.JSImported.InvArray")(a);
[JSFunction] public static void Inv (global::System.String? a) => Proxies.Get<global::System.Action<global::System.String?>>("Bootsharp.Generated.Imports.JSImported.Inv")(a);
[JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => Proxies.Get<global::System.Func<global::System.Threading.Tasks.Task>>("Bootsharp.Generated.Imports.JSImported.InvAsync")();
[JSFunction] public static global::Record? InvRecord () => Proxies.Get<global::System.Func<global::Record?>>("Bootsharp.Generated.Imports.JSImported.InvRecord")();
[JSFunction] public static global::System.Threading.Tasks.Task<global::System.String> InvAsyncResult () => Proxies.Get<global::System.Func<global::System.Threading.Tasks.Task<global::System.String>>>("Bootsharp.Generated.Imports.JSImported.InvAsyncResult")();
[JSFunction] public static global::System.String[] InvArray (global::System.Int32[] a) => Proxies.Get<global::System.Func<global::System.Int32[], global::System.String[]>>("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();
Expand All @@ -115,6 +115,40 @@ internal static void RegisterInterfaces ()
""");
}

[Fact]
public void GeneratesImplementationForInstancedImportInterface ()
{
AddAssembly(With(
"""
public interface IExported { void Inv (string arg); }
public interface IImported { void Fun (string arg); void NotifyEvt(string arg); }

public class Class
{
[JSInvokable] public static IExported GetExported () => default;
[JSFunction] public static IImported GetImported () => Proxies.Get<Func<IImported>>("Class.GetImported")();
}
"""));
Execute();
Contains(
"""
namespace Bootsharp.Generated.Imports
{
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<global::System.Action<global::System.Int32, global::System.String>>("Bootsharp.Generated.Imports.JSImported.Fun")(_id, arg);
[JSEvent] public static void OnEvt (global::System.Int32 _id, global::System.String arg) => Proxies.Get<global::System.Action<global::System.Int32, global::System.String>>("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);
}
}
""");
Assert.DoesNotContain("JSExported", TestedContent); // Exported instances are authored by user and registered on initial interop.
}

[Fact]
public void RespectsInterfaceNamespace ()
{
Expand Down Expand Up @@ -151,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<Action<global::Space.Record>>("Bootsharp.Generated.Imports.Space.JSImported.Fun")(a);
[JSFunction] public static void Fun (global::Space.Record a) => Proxies.Get<global::System.Action<global::Space.Record>>("Bootsharp.Generated.Imports.Space.JSImported.Fun")(a);

void global::Space.IImported.Fun (global::Space.Record a) => Fun(a);
}
Expand Down Expand Up @@ -190,7 +224,7 @@ namespace Bootsharp.Generated.Imports
{
public class JSImported : global::IImported
{
[JSEvent] public static void OnFoo () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.OnFoo")();
[JSEvent] public static void OnFoo () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.OnFoo")();

void global::IImported.NotifyFoo () => OnFoo();
}
Expand Down Expand Up @@ -219,8 +253,8 @@ namespace Bootsharp.Generated.Imports
{
public class JSImported : global::IImported
{
[JSFunction] public static void NotifyFoo () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.NotifyFoo")();
[JSEvent] public static void OnBar () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.OnBar")();
[JSFunction] public static void NotifyFoo () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.NotifyFoo")();
[JSEvent] public static void OnBar () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.OnBar")();

void global::IImported.NotifyFoo () => NotifyFoo();
void global::IImported.BroadcastBar () => OnBar();
Expand All @@ -242,4 +276,27 @@ internal static void RegisterInterfaces ()
}
""");
}

[Fact]
public void IgnoresImplementedInterfaceMethods ()
{
AddAssembly(With(
"""
[assembly:JSExport(typeof(IExportedStatic))]
[assembly:JSImport(typeof(IImportedStatic))]

public interface IExportedStatic { int Foo () => 0; }
public interface IImportedStatic { int Foo () => 0; }
public interface IExportedInstanced { int Foo () => 0; }
public interface IImportedInstanced { int Foo () => 0; }

public class Class
{
[JSInvokable] public static IExportedInstanced GetExported () => default;
[JSFunction] public static IImportedInstanced GetImported () => default;
}
"""));
Execute();
Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase);
}
}
Loading

0 comments on commit 4656f67

Please sign in to comment.