Skip to content

Exception when marshalling types from a precompiled external dll #340

Closed

Description

@camnewnham

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:

  1. The host program declares an abstract class "MyAbstractClass". A third party library inherits MyAbstractClass to create "MyClass".
  2. The host program creates an instance of "MyClass" via Activator.CreateInstance
  3. The host program passes this instance to JS for script interaction.

There are a couple of challenges here:

  1. We cant use JSMarshaller.Current.ToJS<> because we don't have the generic type
  2. (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();
    }
}

Activity

  1. camnewnham commented on Jul 29, 2024

    @camnewnham
    Author

    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 JSMarshaller does this for JS to DotNet, but I want to go the other away round, too.

  2. camnewnham commented on Jul 29, 2024

    @camnewnham
    Author

    This was caused by a difference in the AssemblyLoadContext. When they have the same context, this works OK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions