Repository navigation
Closed
Description
The scenario is a "script editor" for a dotnet application where the user can interact with a dotnet program by writing js. The host program's architecture declares abstract classes and interfaces which are then included then loaded via Activator. I've attempted to isolate this in the example below:
Relates to #330
In this scenario, the goal is:
- The host program declares an abstract class "MyAbstractClass". A third party library inherits MyAbstractClass to create "MyClass".
- The host program creates an instance of "MyClass" via
Activator.CreateInstance - The host program passes this instance to JS for script interaction.
There are a couple of challenges here:
- We cant use
JSMarshaller.Current.ToJS<>because we don't have the generic type - (If calling via reflection) We get
InvalidOperationException: Class not registered for JS export: MyNamespace.MyClass
Interestingly, the global JS object dotnet will happily create instances of the class and they can be then accessed in the regular C#/dotnet context. So one way (JS -> C#) works fine, but the other (C# -> JS) does not.
Full code example
using Microsoft.JavaScript.NodeApi;
using Microsoft.JavaScript.NodeApi.DotNetHost;
using Microsoft.JavaScript.NodeApi.Runtime;
using MyNamespace;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Loader;
NodejsPlatform platform = new("libnode.dll");
string workingDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
File.WriteAllText(Path.Combine(workingDir, "package.json"), $"{{\"type\":\"module\"}}");
File.WriteAllText(Path.Combine(workingDir, "index.js"),
@"
export function getMyClass() {
return new dotnet.MyNamespace.MyClass();
}
export function doSomething(myClassInstance)
{
myClassInstance.SayHello();
}
");
string libThirdParty = Path.Combine(workingDir, "MyLibrary.dll");
/**
* // MyLibrary.dll
* namespace MyNamespace
* {
* public class MyClass : MyAbstractClass
* {
* public void SaySomething()
* {
* Console.WriteLine("Hello");
* }
* }
* }
*/
NodejsEnvironment env = platform.CreateEnvironment(workingDir);
await env.RunAsync(async () =>
{
JSObject exports = [];
JSValue.Global.SetProperty("dotnet", exports);
ManagedHost mh = new ManagedHost(exports);
typeof(ManagedHost)
.GetField("_loadContext", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(mh, AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly()));
// Load the third party lib into the dotnet context and then into the node library
Assembly myAssembly = Assembly.LoadFile(libThirdParty);
LoadDotNetLibrary(mh, libThirdParty);
// Test that we can create the class on the JS side and get it back as the correct type. Works.
JSValue getMyClassFunction = await env.ImportAsync(Path.Combine(workingDir, "index.js"), "getMyClass", true);
JSValue myClassInstance = getMyClassFunction.Call();
bool unwrapped = myClassInstance.RemoveWrap(out object objCreatedByJs);
Debug.Assert(unwrapped && objCreatedByJs is MyAbstractClass, "Failed to remove wrap!");
(objCreatedByJs as MyAbstractClass).SaySomething();
object unspecifiedType = Activator.CreateInstance(myAssembly.GetType("MyNamespace.MyClass"));
JSValue doSomethingFunction = await env.ImportAsync(Path.Combine(workingDir, "index.js"), "doSomething", true);
// We cant use JSMarshaller.ToJS<> because we don't have the generic type
MethodInfo? method = typeof(JSMarshaller).GetMethod(nameof(JSMarshaller.ToJS));
MethodInfo generic = method.MakeGenericMethod(unspecifiedType.GetType());
// Exception on next line: InvalidOperationException: Class not registered for JS export: MyNamespace.MyClass
JSValue result = (JSValue)generic.Invoke(JSMarshaller.Current, [unspecifiedType]);
doSomethingFunction.Call(thisArg: default, result);
});
static void LoadDotNetLibrary(ManagedHost managedHost, string libFullPath)
{
MethodInfo method = typeof(ManagedHost).GetMethod("LoadAssembly", BindingFlags.Instance | BindingFlags.NonPublic);
method.Invoke(managedHost, new object[] { libFullPath, false });
}
namespace MyNamespace
{
public abstract class MyAbstractClass
{
public abstract void SaySomething();
}
}Metadata
Metadata
Assignees
Labels
No labels
Activity
camnewnham commentedon Jul 29, 2024
On reflection, perhaps this question is simply put as "Can I marshal an arbitrary object from C# to JS at runtime?" From what I understand in the docs the
JSMarshallerdoes this for JS to DotNet, but I want to go the other away round, too.camnewnham commentedon Jul 29, 2024
This was caused by a difference in the
AssemblyLoadContext. When they have the same context, this works OK.