Skip to content

Commit

Permalink
.NET Framework 4.7.2 support (microsoft#51)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasongin authored Apr 1, 2023
1 parent 6fa718c commit 98d1539
Show file tree
Hide file tree
Showing 56 changed files with 1,509 additions and 395 deletions.
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ jobs:
out/pkg/*.nupkg
out/pkg/*.tgz
- name: Test .NET 4.7.2
if: matrix.os == 'windows-latest'
env:
TRACE_NODE_API_HOST: 1
run: dotnet test -f net472 --configuration Release --logger trx --results-directory "test-netfx47-node${{ matrix.node-version }}"

- name: Test .NET 6
env:
TRACE_NODE_API_HOST: 1
Expand Down
11 changes: 10 additions & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<Project>
<PropertyGroup>
<TargetFrameworks Condition=" '$(TargetFrameworks)' == '' ">net7.0;net6.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(TargetFrameworks)' == '' and ! $([MSBuild]::IsOsPlatform('Windows')) ">net7.0;net6.0</TargetFrameworks>
<TargetFrameworks Condition=" '$(TargetFrameworks)' == '' and $([MSBuild]::IsOsPlatform('Windows')) ">net7.0;net6.0;net472</TargetFrameworks>
<LangVersion>10</LangVersion>
<Nullable>enable</Nullable>
<Configuration Condition="'$(Configuration)'==''">Debug</Configuration>
Expand All @@ -17,6 +18,14 @@
<VSTestLogger Condition="'$(VSTestLogger)' == ''">console%3Bverbosity=normal</VSTestLogger>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' == 'net472' ">
<NetFramework>true</NetFramework>
<DefineConstants>$(DefineConstants);NETFRAMEWORK</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' != 'net472' ">
<NetFramework>false</NetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="none" />
</ItemGroup>
Expand Down
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="7.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageVersion Include="Nerdbank.GitVersioning" Version="3.5.119" />
<PackageVersion Include="Nullability.Source" Version="2.1.0" />
<PackageVersion Include="System.Memory" Version="4.5.5" />
<PackageVersion Include="System.Reflection.MetadataLoadContext" Version="6.0.0" />
<PackageVersion Include="xunit" Version="2.4.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.4.5" />
Expand Down
10 changes: 10 additions & 0 deletions README-DEV.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# node-api-dotnet Development Notes

### Requirements for Development
- [.NET 7 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/7.0)
- _and_ [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0)
- _and_ [.NET 4.7.2 developer pack](https://dotnet.microsoft.com/en-us/download/dotnet-framework/net472)
(Windows only)
- [Node.js](https://nodejs.org/) version 16 or later

While `node-api-dotnet` supports .NET 6 or .NET Framework 4 at runtime, .NET 7 or later SDK is
required for building the AOT components.

## Build
```bash
dotnet build
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,11 +184,13 @@ Thanks to these design choices, JS to .NET calls are [more than twice as fast](
#### Requirements
- .NET 6 or later
- .NET 7 or later is required for AOT support.
- .NET Framework 4.x support is [coming soon](https://github.com/microsoft/node-api-dotnet/pull/51).
- .NET Framework 4.7.2 or later is supported at runtime,
but .NET 6 SDK is still required for building.
- Node.js v16 or later
- Other JS engines may be supported in the future.
- OS: Windows, Mac, or Linux
- It should work on any platform where .NET 6 is supported.

#### Instructions
Choose between one of the following scenarios:
- [Dynamically invoke .NET APIs from JavaScript](./Docs/dynamic-invoke.md)
Expand Down
7 changes: 7 additions & 0 deletions examples/dotnet-module/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,10 @@ doc-comments for the module's APIs via the auto-generated `.d.ts` file.
| `npm install` | Install Node API .NET npm package into example project.
| `dotnet build` | Install Node API .NET nuget packages into example project; build example project.
| `node example.js` | Run example JS code that calls the example module.

### .NET Framework
To use .NET Framework, apply the follwing change to `example.js`:
```diff
-const dotnet = require('node-api-dotnet');
+const dotnet = require('node-api-dotnet/net472');
```
3 changes: 2 additions & 1 deletion examples/dotnet-module/dotnet-module.csproj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net472</TargetFramework>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<OutDir>bin</OutDir>
<LangVersion>10</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
84 changes: 52 additions & 32 deletions src/NodeApi.DotNetHost/AssemblyExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.JavaScript.NodeApi.Interop;

using static Microsoft.JavaScript.NodeApi.DotNetHost.ManagedHost;
Expand Down Expand Up @@ -165,14 +167,19 @@ public JSValue TryExportType(string name)

private JSValue ExportClass(Type type)
{
Trace($"> AssemblyExporter.ExportClass({type.FullName})");

if (_typeObjects.TryGetValue(type, out JSReference? typeObjectReference))
{
Trace($"< AssemblyExporter.ExportClass() => already exported");
return typeObjectReference!.GetValue()!.Value;
}

if (type == typeof(object) || type == typeof(string) ||
type == typeof(void) || type.IsPrimitive)
{
return default;
}

Trace($"> AssemblyExporter.ExportClass({type.FullName})");

bool isStatic = type.IsAbstract && type.IsSealed;
Type classBuilderType =
(type.IsValueType ? typeof(JSStructBuilder<>) : typeof(JSClassBuilder<>))
Expand Down Expand Up @@ -234,14 +241,16 @@ private void ExportClassDependencies(Type type)
foreach (MemberInfo member in type.GetMembers
(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance))
{
if (member is PropertyInfo property && property.PropertyType.Assembly == type.Assembly)
if (member is PropertyInfo property &&
property.PropertyType.Assembly == type.Assembly &&
IsSupportedType(property.PropertyType))
{
ExportClass(property.PropertyType);
}
else if (member is MethodInfo method &&
IsSupportedMethod(method) &&
method.ReturnType.Assembly == type.Assembly &&
method.ReturnType != typeof(void))
IsSupportedType(method.ReturnType))
{
ExportClass(method.ReturnType);
}
Expand Down Expand Up @@ -272,6 +281,11 @@ private void ExportProperties(Type type, object classBuilder)
BindingFlags.Public | BindingFlags.Static |
(isStatic ? default : BindingFlags.Instance)))
{
if (!IsSupportedType(property.PropertyType))
{
continue;
}

JSPropertyAttributes propertyAttributes = attributes;
bool isStaticProperty = property.GetMethod?.IsStatic == true ||
property.SetMethod?.IsStatic == true;
Expand Down Expand Up @@ -437,6 +451,37 @@ private JSValue ExportEnum(Type type)
return enumObject;
}

private static bool IsSupportedType(Type type)
{
if (type.IsPointer ||
type == typeof(Type) ||
type.Namespace == "System.Reflection" ||
type.Namespace?.StartsWith("System.Collections.") == true ||
type.Namespace?.StartsWith("System.Threading.") == true)
{
return false;
}

#if NETFRAMEWORK
if (type.IsByRef)
#else
if (type.IsByRef || type.IsByRefLike)
#endif
{
// ref parameters aren't yet supported.
// ref structs like Span<T> aren't yet supported.
return false;
}

if (typeof(Stream).IsAssignableFrom(type))
{
// Streams should be projected as Duplex.
return false;
}

return true;
}

private static bool IsSupportedConstructor(ConstructorInfo constructor)
{
return constructor.GetParameters().All(IsSupportedParameter);
Expand All @@ -445,6 +490,7 @@ private static bool IsSupportedConstructor(ConstructorInfo constructor)
private static bool IsSupportedMethod(MethodInfo method)
{
return !method.IsGenericMethodDefinition &&
method.CallingConvention != CallingConventions.VarArgs &&
method.GetParameters().All(IsSupportedParameter) &&
IsSupportedParameter(method.ReturnParameter);
}
Expand All @@ -458,32 +504,6 @@ private static bool IsSupportedParameter(ParameterInfo parameter)
}

Type parameterType = parameter.ParameterType;

if (parameterType.IsByRef || parameterType.IsByRefLike)
{
// ref parameters aren't yet supported.
// ref structs like Span<T> aren't yet supported.
return false;
}

if (parameterType.IsPointer)
{
return false;
}

if (parameterType.IsGenericType &&
parameterType.GetGenericTypeDefinition() == typeof(IEnumerator<>))
{
// Enumerables should be projected as iterables.
return false;
}

if (parameter.ParameterType == typeof(Type))
{
// Methods using reflection aren't supported.
return false;
}

return true;
return IsSupportedType(parameterType);
}
}
30 changes: 18 additions & 12 deletions src/NodeApi.DotNetHost/JSInterfaceMarshaller.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,38 @@ namespace Microsoft.JavaScript.NodeApi.DotNetHost;
/// <summary>
/// Supports dynamic implementation of .NET interfaces by JavaScript.
/// </summary>
internal static class JSInterfaceMarshaller
internal class JSInterfaceMarshaller
{
private static readonly ConcurrentDictionary<Type, Type> s_interfaceTypes = new();
private static readonly AssemblyBuilder s_assemblyBuilder =
AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName(typeof(JSInterface).FullName!),
AssemblyBuilderAccess.Run);
private static readonly ModuleBuilder s_moduleBuilder =
s_assemblyBuilder.DefineDynamicModule(typeof(JSInterface).Name);
private readonly ConcurrentDictionary<Type, Type> _interfaceTypes = new();
private readonly AssemblyBuilder _assemblyBuilder;
private readonly ModuleBuilder _moduleBuilder;

public JSInterfaceMarshaller()
{
string assemblyName = typeof(JSInterface).FullName +
"_" + Environment.CurrentManagedThreadId;
_assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(
new AssemblyName(assemblyName), AssemblyBuilderAccess.Run);
_moduleBuilder =
_assemblyBuilder.DefineDynamicModule(typeof(JSInterface).Name);
}

/// <summary>
/// Defines a class type that extends <see cref="JSInterface" /> and implements the requested
/// interface type by forwarding all member access to the JS value.
/// </summary>
public static Type Implement(Type interfaceType, JSMarshaller marshaller)
public Type Implement(Type interfaceType, JSMarshaller marshaller)
{
return s_interfaceTypes.GetOrAdd(
return _interfaceTypes.GetOrAdd(
interfaceType,
(t) => BuildInterfaceImplementation(interfaceType, marshaller));
}

#pragma warning disable IDE0060 // Unused parameter 'marshaller'
private static Type BuildInterfaceImplementation(Type interfaceType, JSMarshaller marshaller)
private Type BuildInterfaceImplementation(Type interfaceType, JSMarshaller marshaller)
#pragma warning restore IDE0060 // Unused parameter
{
TypeBuilder typeBuilder = s_moduleBuilder.DefineType(
TypeBuilder typeBuilder = _moduleBuilder.DefineType(
"proxy_" +
JSMarshaller.FullTypeName(interfaceType),
TypeAttributes.Class | TypeAttributes.Sealed,
Expand Down
Loading

0 comments on commit 98d1539

Please sign in to comment.