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
-
node-api-dotnet/src/NodeApi/Interop/JSStructBuilderOfT.cs
Lines 104 to 107 in f7beb3c
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.