Skip to content

Commit 4656f67

Browse files
authored
instance interop (#143)
1 parent a5cc885 commit 4656f67

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1621
-432
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using static Bootsharp.Instances;
2+
3+
namespace Bootsharp.Common.Test;
4+
5+
public class InstancesTest
6+
{
7+
[Fact]
8+
public void ThrowsWhenGettingUnregisteredInstance ()
9+
{
10+
Assert.Throws<Error>(() => Get(0));
11+
}
12+
13+
[Fact]
14+
public void ThrowsWhenDisposingUnregisteredInstance ()
15+
{
16+
Assert.Throws<Error>(() => Dispose(0));
17+
}
18+
19+
[Fact]
20+
public void CanRegisterGetAndDisposeInstance ()
21+
{
22+
var instance = new object();
23+
var id = Register(instance);
24+
Assert.Same(instance, Get(id));
25+
Dispose(id);
26+
Assert.Throws<Error>(() => Get(id));
27+
}
28+
29+
[Fact]
30+
public void GeneratesUniqueIdsOnEachRegister ()
31+
{
32+
Assert.NotEqual(Register(new object()), Register(new object()));
33+
}
34+
35+
[Fact]
36+
public void ReusesIdOfDisposedInstance ()
37+
{
38+
var id = Register(new object());
39+
Dispose(id);
40+
Assert.Equal(id, Register(new object()));
41+
}
42+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
namespace Bootsharp;
2+
3+
/// <summary>
4+
/// Manages exported (C# -> JavaScript) instanced interop interfaces.
5+
/// </summary>
6+
public static class Instances
7+
{
8+
private static readonly Dictionary<int, object> idToInstance = [];
9+
private static readonly Queue<int> idPool = [];
10+
private static int nextId = int.MinValue;
11+
12+
/// <summary>
13+
/// Registers specified interop instance and associates it with unique ID.
14+
/// </summary>
15+
/// <param name="instance">The instance to register.</param>
16+
/// <returns>Unique ID associated with the registered instance.</returns>
17+
public static int Register (object instance)
18+
{
19+
var id = idPool.Count > 0 ? idPool.Dequeue() : nextId++;
20+
idToInstance[id] = instance;
21+
return id;
22+
}
23+
24+
/// <summary>
25+
/// Resolves registered instance by the specified ID.
26+
/// </summary>
27+
/// <param name="id">Unique ID of the instance to resolve.</param>
28+
public static object Get (int id)
29+
{
30+
if (!idToInstance.TryGetValue(id, out var instance))
31+
throw new Error($"Failed to resolve exported interop instance with '{id}' ID: not registered.");
32+
return instance;
33+
}
34+
35+
/// <summary>
36+
/// Notifies that interop instance is no longer used on JavaScript side
37+
/// (eg, was garbage collected) and can be released on C# side as well.
38+
/// </summary>
39+
/// <param name="id">ID of the disposed interop instance.</param>
40+
public static void Dispose (int id)
41+
{
42+
if (!idToInstance.Remove(id))
43+
throw new Error($"Failed to dispose exported interop instance with '{id}' ID: not registered.");
44+
idPool.Enqueue(id);
45+
}
46+
}

src/cs/Bootsharp.Publish.Test/Emit/DependenciesTest.cs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ public class DependenciesTest : EmitTest
88
protected override string TestedContent => GeneratedDependencies;
99

1010
[Fact]
11-
public void WhenNothingInspectedIncludesCommonDependencies ()
11+
public void AddsCommonDependencies ()
1212
{
1313
Execute();
1414
Contains(
@@ -28,31 +28,55 @@ internal static void RegisterDynamicDependencies () { }
2828
}
2929

3030
[Fact]
31-
public void AddsGeneratedExportTypes ()
31+
public void AddsStaticInteropInterfaceImplementations ()
3232
{
3333
AddAssembly(
34-
With("[assembly:JSExport(typeof(IFoo), typeof(Space.IBar))]"),
35-
With("public interface IFoo {}"),
36-
With("Space", "public interface IBar {}"));
34+
With("[assembly:JSExport(typeof(IExported), typeof(Space.IExported))]"),
35+
With("[assembly:JSImport(typeof(IImported), typeof(Space.IImported))]"),
36+
With("public interface IExported {}"),
37+
With("public interface IImported {}"),
38+
With("Space", "public interface IExported {}"),
39+
With("Space", "public interface IImported {}"));
3740
Execute();
38-
Added(All, "Bootsharp.Generated.Exports.JSFoo");
39-
Added(All, "Bootsharp.Generated.Exports.Space.JSBar");
41+
Added(All, "Bootsharp.Generated.Exports.JSExported");
42+
Added(All, "Bootsharp.Generated.Exports.Space.JSExported");
43+
Added(All, "Bootsharp.Generated.Imports.JSImported");
44+
Added(All, "Bootsharp.Generated.Imports.Space.JSImported");
4045
}
4146

4247
[Fact]
43-
public void AddsGeneratedImportTypes ()
48+
public void AddsInstancedInteropInterfaceImplementations ()
4449
{
45-
AddAssembly(
46-
With("[assembly:JSImport(typeof(IFoo), typeof(Space.IBar))]"),
47-
With("public interface IFoo {}"),
48-
With("Space", "public interface IBar {}"));
50+
AddAssembly(With(
51+
"""
52+
[assembly:JSExport(typeof(IExportedStatic))]
53+
[assembly:JSImport(typeof(IImportedStatic))]
54+
55+
public interface IExportedStatic { IExportedInstancedA CreateExported (); }
56+
public interface IImportedStatic { IImportedInstancedA CreateImported (); }
57+
58+
public interface IExportedInstancedA { }
59+
public interface IExportedInstancedB { }
60+
public interface IImportedInstancedA { }
61+
public interface IImportedInstancedB { }
62+
63+
public class Class
64+
{
65+
[JSInvokable] public static IExportedInstancedB CreateExported () => default;
66+
[JSFunction] public static IImportedInstancedB CreateImported () => default;
67+
}
68+
"""));
4969
Execute();
50-
Added(All, "Bootsharp.Generated.Imports.JSFoo");
51-
Added(All, "Bootsharp.Generated.Imports.Space.JSBar");
70+
Added(All, "Bootsharp.Generated.Exports.JSExportedStatic");
71+
Added(All, "Bootsharp.Generated.Imports.JSImportedStatic");
72+
Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedA");
73+
Added(All, "Bootsharp.Generated.Imports.JSImportedInstancedB");
74+
// Export interop instances are not generated in C#; they're authored by user.
75+
Assert.DoesNotContain("Bootsharp.Generated.Exports.JSExportedInstanced", TestedContent);
5276
}
5377

5478
[Fact]
55-
public void AddsClassesWithInteropMethods ()
79+
public void AddsClassesWithStaticInteropMethods ()
5680
{
5781
AddAssembly("Assembly.With.Dots.dll",
5882
With("SpaceA", "public class ClassA { [JSInvokable] public static void Foo () {} }"),

src/cs/Bootsharp.Publish.Test/Emit/InterfacesTest.cs

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ public class InterfacesTest : EmitTest
55
protected override string TestedContent => GeneratedInterfaces;
66

77
[Fact]
8-
public void GeneratesInteropClassForExportedInterface ()
8+
public void GeneratesImplementationForExportedStaticInterface ()
99
{
1010
AddAssembly(With(
1111
"""
@@ -61,7 +61,7 @@ internal static void RegisterInterfaces ()
6161
}
6262

6363
[Fact]
64-
public void GeneratesImplementationForImportedInterface ()
64+
public void GeneratesImplementationForImportedStaticInterface ()
6565
{
6666
AddAssembly(With(
6767
"""
@@ -85,11 +85,11 @@ namespace Bootsharp.Generated.Imports
8585
{
8686
public class JSImported : global::IImported
8787
{
88-
[JSFunction] public static void Inv (global::System.String? a) => Proxies.Get<Action<global::System.String?>>("Bootsharp.Generated.Imports.JSImported.Inv")(a);
89-
[JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => Proxies.Get<Func<global::System.Threading.Tasks.Task>>("Bootsharp.Generated.Imports.JSImported.InvAsync")();
90-
[JSFunction] public static global::Record? InvRecord () => Proxies.Get<Func<global::Record?>>("Bootsharp.Generated.Imports.JSImported.InvRecord")();
91-
[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")();
92-
[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);
88+
[JSFunction] public static void Inv (global::System.String? a) => Proxies.Get<global::System.Action<global::System.String?>>("Bootsharp.Generated.Imports.JSImported.Inv")(a);
89+
[JSFunction] public static global::System.Threading.Tasks.Task InvAsync () => Proxies.Get<global::System.Func<global::System.Threading.Tasks.Task>>("Bootsharp.Generated.Imports.JSImported.InvAsync")();
90+
[JSFunction] public static global::Record? InvRecord () => Proxies.Get<global::System.Func<global::Record?>>("Bootsharp.Generated.Imports.JSImported.InvRecord")();
91+
[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")();
92+
[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);
9393
9494
void global::IImported.Inv (global::System.String? a) => Inv(a);
9595
global::System.Threading.Tasks.Task global::IImported.InvAsync () => InvAsync();
@@ -115,6 +115,40 @@ internal static void RegisterInterfaces ()
115115
""");
116116
}
117117

118+
[Fact]
119+
public void GeneratesImplementationForInstancedImportInterface ()
120+
{
121+
AddAssembly(With(
122+
"""
123+
public interface IExported { void Inv (string arg); }
124+
public interface IImported { void Fun (string arg); void NotifyEvt(string arg); }
125+
126+
public class Class
127+
{
128+
[JSInvokable] public static IExported GetExported () => default;
129+
[JSFunction] public static IImported GetImported () => Proxies.Get<Func<IImported>>("Class.GetImported")();
130+
}
131+
"""));
132+
Execute();
133+
Contains(
134+
"""
135+
namespace Bootsharp.Generated.Imports
136+
{
137+
public class JSImported(global::System.Int32 _id) : global::IImported
138+
{
139+
~JSImported() => global::Bootsharp.Generated.Interop.DisposeImportedInstance(_id);
140+
141+
[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);
142+
[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);
143+
144+
void global::IImported.Fun (global::System.String arg) => Fun(_id, arg);
145+
void global::IImported.NotifyEvt (global::System.String arg) => OnEvt(_id, arg);
146+
}
147+
}
148+
""");
149+
Assert.DoesNotContain("JSExported", TestedContent); // Exported instances are authored by user and registered on initial interop.
150+
}
151+
118152
[Fact]
119153
public void RespectsInterfaceNamespace ()
120154
{
@@ -151,7 +185,7 @@ namespace Bootsharp.Generated.Imports.Space
151185
{
152186
public class JSImported : global::Space.IImported
153187
{
154-
[JSFunction] public static void Fun (global::Space.Record a) => Proxies.Get<Action<global::Space.Record>>("Bootsharp.Generated.Imports.Space.JSImported.Fun")(a);
188+
[JSFunction] public static void Fun (global::Space.Record a) => Proxies.Get<global::System.Action<global::Space.Record>>("Bootsharp.Generated.Imports.Space.JSImported.Fun")(a);
155189
156190
void global::Space.IImported.Fun (global::Space.Record a) => Fun(a);
157191
}
@@ -190,7 +224,7 @@ namespace Bootsharp.Generated.Imports
190224
{
191225
public class JSImported : global::IImported
192226
{
193-
[JSEvent] public static void OnFoo () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.OnFoo")();
227+
[JSEvent] public static void OnFoo () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.OnFoo")();
194228
195229
void global::IImported.NotifyFoo () => OnFoo();
196230
}
@@ -219,8 +253,8 @@ namespace Bootsharp.Generated.Imports
219253
{
220254
public class JSImported : global::IImported
221255
{
222-
[JSFunction] public static void NotifyFoo () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.NotifyFoo")();
223-
[JSEvent] public static void OnBar () => Proxies.Get<Action>("Bootsharp.Generated.Imports.JSImported.OnBar")();
256+
[JSFunction] public static void NotifyFoo () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.NotifyFoo")();
257+
[JSEvent] public static void OnBar () => Proxies.Get<global::System.Action>("Bootsharp.Generated.Imports.JSImported.OnBar")();
224258
225259
void global::IImported.NotifyFoo () => NotifyFoo();
226260
void global::IImported.BroadcastBar () => OnBar();
@@ -242,4 +276,27 @@ internal static void RegisterInterfaces ()
242276
}
243277
""");
244278
}
279+
280+
[Fact]
281+
public void IgnoresImplementedInterfaceMethods ()
282+
{
283+
AddAssembly(With(
284+
"""
285+
[assembly:JSExport(typeof(IExportedStatic))]
286+
[assembly:JSImport(typeof(IImportedStatic))]
287+
288+
public interface IExportedStatic { int Foo () => 0; }
289+
public interface IImportedStatic { int Foo () => 0; }
290+
public interface IExportedInstanced { int Foo () => 0; }
291+
public interface IImportedInstanced { int Foo () => 0; }
292+
293+
public class Class
294+
{
295+
[JSInvokable] public static IExportedInstanced GetExported () => default;
296+
[JSFunction] public static IImportedInstanced GetImported () => default;
297+
}
298+
"""));
299+
Execute();
300+
Assert.DoesNotContain("Foo", TestedContent, StringComparison.OrdinalIgnoreCase);
301+
}
245302
}

0 commit comments

Comments
 (0)