Skip to content

Commit

Permalink
Marshal struct constructors & readonly props (#354)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored Aug 12, 2024
1 parent 467e030 commit 749b0f0
Show file tree
Hide file tree
Showing 12 changed files with 275 additions and 236 deletions.
14 changes: 9 additions & 5 deletions docs/reference/structs-tuples.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@ are _marshalled by value_. This has a few implications:
the .NET instance method, though the `struct` gets entirely copied/marshalled from JS to .NET
before every .NET method invocation.

### Read-only properties

Read-only properties of .NET `struct` types are supported by the marshaller if they are
_initializable_, that is if they are defined as `{ get; init; }` and not just `{ get; }`. When
marshalling a JS object to a .NET `struct`, the .NET properties are all initialized by marshalling
the property values from the JS object.

### Static members

Static properties and methods on a .NET `struct` work the same as on a `class`, since static members
do not deal with an instance that gets marshalled by value. (The `struct` _type_ object is still
marshalled by reference.)

### Read-only structs and properties

.NET `struct` types that are `readonly`, or read-only properties of non-`readonly structs` are
[not yet implemented](https://github.com/microsoft/node-api-dotnet/issues/132).

## Tuples

| C# Type | JS Type |
Expand Down
120 changes: 94 additions & 26 deletions src/NodeApi.DotNetHost/JSMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -202,16 +202,36 @@ internal static bool IsConvertedType(Type type)
/// Converts from a JS value to a specified type.
/// </summary>
/// <typeparam name="T">The type the value will be converted to.</typeparam>
/// <param name="value">The JavaScript value to be converted.</param>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
public T FromJS<T>(JSValue value) => GetFromJSValueDelegate<T>()(value);

/// <summary>
/// Converts from a JS value to a specified type.
/// </summary>
/// <param name="type">The type the value will be converted to.</param>
/// <param name="value">The JavaScript value to be converted.</param>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
public object FromJS(Type type, JSValue value) =>
GetFromJSValueDelegate(type).DynamicInvoke(value)!;

/// <summary>
/// Converts from a specified type to a JS value.
/// </summary>
/// <typeparam name="T">The type the value will be converted from.</typeparam>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
public JSValue ToJS<T>(T value) => GetToJSValueDelegate<T>()(value);

/// <summary>
/// Converts from a specified type to a JS value.
/// </summary>
/// <param name="type">The type the value will be converted from.</param>
/// <param name="value">The value to be converted. Must be an instance (or subtype of) the
/// <paramref name="type"/>.</param>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
public JSValue ToJS(Type type, object value) =>
(JSValue)GetToJSValueDelegate(type).DynamicInvoke(value)!;

/// <summary>
/// Gets a delegate that converts from a JS value to a specified type.
/// </summary>
Expand All @@ -223,7 +243,21 @@ internal static bool IsConvertedType(Type type)
/// </remarks>
public JSValue.To<T> GetFromJSValueDelegate<T>()
{
return (JSValue.To<T>)_fromJSDelegates.GetOrAdd(typeof(T), (toType) =>
return (JSValue.To<T>)GetFromJSValueDelegate(typeof(T));
}

/// <summary>
/// Gets a delegate that converts from a JS value to a specified type.
/// </summary>
/// <param name="type">The type the value will be converted to.</param>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
/// <remarks>
/// Type conversion delegates are built on created and then cached, so it is efficient
/// to call this method multiple times for the same type.
/// </remarks>
public Delegate GetFromJSValueDelegate(Type type)
{
return _fromJSDelegates.GetOrAdd(type, (toType) =>
{
LambdaExpression fromJSExpression = GetFromJSValueExpression(toType);
return fromJSExpression.Compile();
Expand All @@ -241,7 +275,21 @@ public JSValue.To<T> GetFromJSValueDelegate<T>()
/// </remarks>
public JSValue.From<T> GetToJSValueDelegate<T>()
{
return (JSValue.From<T>)_toJSDelegates.GetOrAdd(typeof(T), (fromType) =>
return (JSValue.From<T>)GetToJSValueDelegate(typeof(T));
}

/// <summary>
/// Gets a delegate that converts from a specified type to a JS value.
/// </summary>
/// <param name="type">The type the value will be converted from.</param>
/// <exception cref="NotSupportedException">The type cannot be converted.</exception>
/// <remarks>
/// Type conversion delegates are built on demand and then cached, so it is efficient
/// to call this method multiple times for the same type.
/// </remarks>
public Delegate GetToJSValueDelegate(Type type)
{
return _toJSDelegates.GetOrAdd(type, (fromType) =>
{
LambdaExpression toJSExpression = GetToJSValueExpression(fromType);
return toJSExpression.Compile();
Expand Down Expand Up @@ -342,14 +390,15 @@ public LambdaExpression GetToJSValueExpression(Type fromType)

/// <summary>
/// Builds a lambda expression for a JS callback adapter that constructs an instance of
/// a .NET class. When invoked, the expression will marshal the constructor arguments
/// from JS, invoke the constructor, then return the new instance of the class.
/// a .NET class or struct. When invoked, the expression will marshal the constructor arguments
/// from JS, invoke the constructor, then return the new instance either as a wrapped
/// .NET class or a JS object marshalled from the struct.
/// </summary>
/// <remarks>
/// The returned expression takes a <see cref="JSCallbackArgs"/> parameter and returns an
/// instance of the class as an external JS value. The lambda expression may be converted to
/// a delegate with <see cref="LambdaExpression.Compile()"/>, and used as the constructor
/// callback parameter for a <see cref="JSClassBuilder{T}"/>.
/// The returned expression takes a <see cref="JSCallbackArgs"/> parameter and returns a JS
/// value for the constructed instance. The lambda expression may be converted to a delegate
/// with <see cref="LambdaExpression.Compile()"/>, and used as the constructor callback
/// parameter for a <see cref="JSClassBuilder{T}"/>.
/// </remarks>
#pragma warning disable CA1822 // Mark members as static
public Expression<JSCallback> BuildFromJSConstructorExpression(ConstructorInfo constructor)
Expand All @@ -368,7 +417,7 @@ public Expression<JSCallback> BuildFromJSConstructorExpression(ConstructorInfo c

ParameterInfo[] parameters = constructor.GetParameters();
ParameterExpression[] argVariables = new ParameterExpression[parameters.Length];
IEnumerable<ParameterExpression> variables;
List<ParameterExpression> variables;
List<Expression> statements = new(parameters.Length + 2);

for (int i = 0; i < parameters.Length; i++)
Expand All @@ -380,14 +429,26 @@ public Expression<JSCallback> BuildFromJSConstructorExpression(ConstructorInfo c

ParameterExpression resultVariable = Expression.Variable(
constructor.DeclaringType!, "__result");
variables = argVariables.Append(resultVariable);
variables = new List<ParameterExpression>(argVariables.Append(resultVariable));
statements.Add(Expression.Assign(resultVariable,
Expression.New(constructor, argVariables)));

MethodInfo createExternalMethod = typeof(JSValue)
.GetStaticMethod(nameof(JSValue.CreateExternal));
statements.Add(Expression.Call(
createExternalMethod, resultVariable));
if (constructor.DeclaringType!.IsValueType)
{
// For structs, use the object from __args.ThisArg and marshal the result struct
// to a JS object (by value).
Expression thisExpression = Expression.Property(
s_argsParameter, nameof(JSCallbackArgs.ThisArg));
statements.AddRange(BuildToJSFromStructExpressions(
constructor.DeclaringType!, variables, resultVariable, thisExpression));
}
else
{
MethodInfo createExternalMethod = typeof(JSValue)
.GetStaticMethod(nameof(JSValue.CreateExternal));
statements.Add(Expression.Call(
createExternalMethod, resultVariable));
}

return (Expression<JSCallback>)Expression.Lambda(
delegateType: typeof(JSCallback),
Expand Down Expand Up @@ -2805,16 +2866,17 @@ private IEnumerable<Expression> BuildFromJSToStructExpressions(
ParameterExpression valueVariable)
{
/*
* StructName obj = default;
* obj.Property0 = (Property0Type)value["property0"];
* ...
* StructName obj = new()
* {
* Property0 = (Property0Type)value["property0"],
* ...
* };
* return obj;
*/
ParameterExpression objVariable = Expression.Variable(toType, "obj");
variables.Add(objVariable);

yield return Expression.Assign(objVariable, Expression.Default(toType));

List<MemberBinding> memberBindings = new();
foreach (PropertyInfo property in toType.GetProperties(
BindingFlags.Public | BindingFlags.Instance))
{
Expand All @@ -2825,21 +2887,23 @@ private IEnumerable<Expression> BuildFromJSToStructExpressions(
}

Expression propertyName = Expression.Constant(ToCamelCase(property.Name));
yield return Expression.Assign(
Expression.Property(objVariable, property),
InlineOrInvoke(
GetFromJSValueExpression(property.PropertyType),
Expression.Property(valueVariable, s_valueItem, propertyName),
nameof(BuildFromJSToStructExpressions)));
memberBindings.Add(Expression.Bind(property, InlineOrInvoke(
GetFromJSValueExpression(property.PropertyType),
Expression.Property(valueVariable, s_valueItem, propertyName),
nameof(BuildFromJSToStructExpressions))));
}

yield return Expression.Assign(
objVariable, Expression.MemberInit(Expression.New(toType), memberBindings));

yield return objVariable;
}

private IEnumerable<Expression> BuildToJSFromStructExpressions(
Type fromType,
List<ParameterExpression> variables,
Expression valueExpression)
Expression valueExpression,
Expression? thisExpression = null)
{
/*
* JSValue jsValue = JSRuntimeContext.Current.CreateStruct<StructName>();
Expand All @@ -2858,6 +2922,10 @@ private IEnumerable<Expression> BuildToJSFromStructExpressions(
yield return Expression.Assign(
jsValueVariable, Expression.Call(createObjectMethod));
}
else if (thisExpression != null)
{
yield return Expression.Assign(jsValueVariable, thisExpression);
}
else
{
MethodInfo createStructMethod = typeof(JSRuntimeContext)
Expand Down
5 changes: 2 additions & 3 deletions src/NodeApi.DotNetHost/TypeExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,12 +450,11 @@ private JSReference ExportClass(Type type, bool deferMembers)
try
{
bool isStatic = type.IsAbstract && type.IsSealed;
Type classBuilderType =
(type.IsValueType ? typeof(JSStructBuilder<>) : typeof(JSClassBuilder<>))
Type classBuilderType = typeof(JSClassBuilder<>)
.MakeGenericType(isStatic ? typeof(object) : type);

object classBuilder;
if (type.IsInterface || isStatic || type.IsValueType)
if (type.IsInterface || isStatic)
{
classBuilder = classBuilderType.CreateInstance(
new[] { typeof(string) }, new[] { type.Name });
Expand Down
10 changes: 9 additions & 1 deletion src/NodeApi.Generator/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,11 @@ member.Expression is ParameterExpression parameterExpression &&
LabelExpression label => label.DefaultValue != null ?
ToCS(label.DefaultValue, path, variables) : "???",

MemberInitExpression init => "new " + FormatType(init.Type) + "\n{\n" +
string.Concat(init.Bindings.Select((b) => b.Member.Name + " = " +
ToCS(((MemberAssignment)b).Expression, path, variables) + ",\n")) +
"}",

_ => throw new NotImplementedException(
"Expression type not implemented: " +
$"{expression.GetType().Name} ({expression.NodeType}) at {path}"),
Expand Down Expand Up @@ -276,6 +281,7 @@ private static string FormatStatement(
if (expression.NodeType == ExpressionType.Assign)
{
BinaryExpression assignment = (BinaryExpression)expression;

if (assignment.Left is ParameterExpression variable &&
!variables.Contains(variable.Name!))
{
Expand All @@ -286,7 +292,9 @@ private static string FormatStatement(

s += ToCS(expression, path, variables);

if (!s.EndsWith('}'))
if (!s.EndsWith('}') ||
(expression.NodeType == ExpressionType.Assign &&
((BinaryExpression)expression).Right.NodeType == ExpressionType.MemberInit))
{
s += ';';
}
Expand Down
Loading

0 comments on commit 749b0f0

Please sign in to comment.