microsoft/node-api-dotnet

Struct does not initialize correctly

camnewnham opened this issue · 6 comments

Considering this console app for reproduction:

using Microsoft.JavaScript.NodeApi;
using Microsoft.JavaScript.NodeApi.DotNetHost;
using Microsoft.JavaScript.NodeApi.Runtime;
using System.Reflection;

new NodejsPlatform("libnode.dll")
    .CreateEnvironment()
    .Run(() =>
{
    JSObject managedTypes = (JSObject)JSValue.CreateObject();
    JSValue.Global.SetProperty("dotnet", managedTypes);
    JSMarshaller marshaller = new JSMarshaller()
    {
        AutoCamelCase = false
    };

    TypeExporter typeExporter = new TypeExporter(marshaller, managedTypes);
    typeExporter.ExportAssemblyTypes(typeof(ValueType).Assembly);
    typeExporter.ExportAssemblyTypes(Assembly.GetExecutingAssembly());
    typeExporter.ExportType(typeof(double));
    typeExporter.ExportType(typeof(Test.Point));

    JSValue.RunScript(@"
var test = new dotnet.Test.Point(7,8,9);
console.info(typeof test, test.X, test.Y, test.Z);
console.info(JSON.stringify(test,null,2));
");
});

namespace Test
{
    public struct Point
    {
        public double m_x;
        public double X
        {
            get => m_x;
            set => m_x = value;
        }

        public double Z = 3;

        public double Y { get; set; }

        public Point(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public Point()
        {
            X = Y = Z = 1;
        }
    }
}

The output is:

> TypeExporter.ExportClass(Test.Point)
    X
    Y
    Equals()
    GetHashCode()
    ToString()
    GetType()
< TypeExporter.ExportClass()
object undefined undefined undefined
{}

Changing Point to a class instead of a struct outputs:

object 7 8 undefined
{
  "X": 7,
  "Y": 8
}
  • How do I get the struct to initialize correctly?
  • Is it possible to include fields in addition to properties?

Possibly related: I also noticed that when Point is a class System.Private.CoreLib was automatically called for ExportAssemblyTypes and System.Double for ExportClass, but for struct I only received the message Namespace 'System' not found for base type or interface 'ValueType'. - hence why I was adding it manually.

Is it possible to include fields in addition to properties?

Fields are not yet supported by the marshaller: #63

If you use properties instead, the initialization should work how you expect.

for struct I only received the message Namespace 'System' not found for base type or interface 'ValueType'. - hence why I was adding it manually.

I'm not sure why that is... but there has been less testing of marshalling types in the .NET hosting case, compared to the Node.js hosting. It seems there may be some missing initialization for exporting system types.

I think I must be missing something. I still have this issue with the simple struct in the above example:

public struct Point
{
    public double X { get; set; }

    public Point(double x)
    {
        X = x;
    }
}

With JS runscript:

var test = new dotnet.Test.Point(7);
console.info(typeof test, test.X);
console.info(JSON.stringify(test,null,2));

Produces in console:

object undefined
{}

Note this only occurs when the struct is created on the JS side. Passing the struct from dotnet works. As before this is not an issue when changed from struct to class.

// This works
export function test(point) { 
  console.info(typeof point, point.X);
    point.X += 1;
  return point;
}

// This does not work (point.X is undefined)
export function test2() {
  var point = new dotnet.Test.Point(123);
  console.info(typeof point, point.X);
  return point;
}
Expand for the full code sample
using Microsoft.JavaScript.NodeApi;
using Microsoft.JavaScript.NodeApi.DotNetHost;
using Microsoft.JavaScript.NodeApi.Runtime;
using System.Reflection;

string envFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
File.WriteAllText(Path.Combine(envFolder, "package.json"), @"{""type"":""module""}");

NodejsEnvironment env = new NodejsPlatform("libnode.dll")
    .CreateEnvironment(envFolder);
await env.RunAsync(async () =>
    {
        JSObject managedTypes = (JSObject)JSValue.CreateObject();
        JSValue.Global.SetProperty("dotnet", managedTypes);
        JSMarshaller marshaller = new JSMarshaller()
        {
            AutoCamelCase = false
        };

        TypeExporter typeExporter = new TypeExporter(marshaller, managedTypes);
        typeExporter.ExportAssemblyTypes(Assembly.GetExecutingAssembly());
        typeExporter.ExportType(typeof(Test.Point));

        string testFile = Path.Combine(envFolder, "test.js");
        File.WriteAllText(testFile, @"
export function test(point) { 
  console.info(typeof point, point.X);
    point.X += 1;
  return point;
}

export function test2() {
  var point = new dotnet.Test.Point(123);
  console.info(typeof point, point.X);
  return point;
}
");
        // Run test 1 : a point created on the dotnet side correctly increments a property
        JSValue testFn = await env.ImportAsync(testFile, "test", true);
        JSValue jsPt = marshaller.ToJS<Test.Point>(new Test.Point(7));
        JSValue result = testFn.Call(thisArg: default, jsPt);
        Test.Point resultPt = marshaller.FromJS<Test.Point>(result);
        Console.WriteLine("Result: " + resultPt.X);

        // Run test 2 : a point created on the js side correctly runs the constructor
        // This fails (point is an "object" but point.X is undefined)
        JSValue testFn2 = await env.ImportAsync(testFile, "test2", true);
        JSValue result2 = testFn2.Call(thisArg: default);
    });

namespace Test
{
    public struct Point
    {
        public double X { get; set; }

        public Point(double x)
        {
            X = x;
        }
    }
}

As best I can tell constructors are only created for class instances - comparing JSStructBuilder to JSClassBuilder -

JSValue classObject = JSValue.DefineClass(
StructName,
new JSCallbackDescriptor(StructName, (args) => args.ThisArg),
Properties.ToArray());

Ahh you're right, struct constructors are not implemented. The current design requires that you create a plain JavaScript object ({ X: 7, Y: 8 }) and then the object properties get marshalled to the .NET struct type when you pass the object to a .NET method that takes the struct type as a parameter.

I do think struct constructors should also be supported though. I'll keep this issue open to track that work.

Fixed in #354