Skip to content

Commit 424705b

Browse files
authored
Fix importing specific version of .NET (#193)
1 parent 1b708bf commit 424705b

34 files changed

Lines changed: 381 additions & 63 deletions

Docs/dynamic-invoke.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@ For examples of this scenario, see
5656
const dotnet = require('node-api-dotnet');
5757
```
5858
59+
To load a specific version of .NET, append the target framework moniker to the module name.
60+
A `.js` suffix is required when using ES modules, optional with CommonJS.
61+
```JavaScript
62+
import dotnet from 'node-api-dotnet/net6.0.js'
63+
```
64+
Currently the supported target frameworks are `net472`, `net6.0`, and `net8.0`.
65+
5966
4. Load one or more .NET packages using the generated `.js` files:
6067
```JavaScript
6168
require('./bin/Example.Package.js');

Docs/node-module.md

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,29 @@ For a minimal example of this scenario, see
5959
[build it from source](../README-DEV.md).<br>Then get the package from
6060
`out/pkg/node-api-dotnet-{version}.tgz`.
6161
62-
6. Import the `node-api-dotnet` package in your JavaScript code:
62+
6. Import the `node-api-dotnet` package in your JavaScript or TypeScript code. The import syntax
63+
depends on the [module system](https://nodejs.org/api/esm.html) the current project is using.
64+
65+
ES modules (TypeScript or JavaScript):
6366
```JavaScript
64-
const dotnet = require('node-api-dotnet');
67+
import dotnet from 'node-api-dotnet';
6568
```
66-
Or if using ES modules:
69+
CommonJS modules (TypeScript):
70+
```TypeScript
71+
import * as dotnet from 'node-api-dotnet';
72+
```
73+
CommonJS modules (JavaScript):
6774
```JavaScript
68-
import dotnet from 'node-api-dotnet';
75+
const dotnet = require('node-api-dotnet');
6976
```
7077
78+
To load a specific version of .NET, append the target framework moniker to the module name.
79+
A `.js` suffix is required when using ES modules, optional with CommonJS.
80+
```JavaScript
81+
import dotnet from 'node-api-dotnet/net6.0.js'
82+
```
83+
Currently the supported target frameworks are `net472`, `net6.0`, and `net8.0`.
84+
7185
7. Load your .NET module assembly from its path using the `dotnet.require()` function. Also provide
7286
a hint about type definitions (from the same path):
7387
```JavaScript

src/NodeApi.DotNetHost/ManagedHost.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ JSValue removeListener(JSCallbackArgs args)
137137
}
138138

139139
exports.DefineProperties(
140+
JSPropertyDescriptor.Accessor("runtimeVersion", ManagedHost.GetRuntimeVersion),
141+
JSPropertyDescriptor.Accessor("frameworkMoniker", ManagedHost.GetFrameworkMoniker),
142+
140143
// The require() method loads a .NET assembly that was built to be a Node API module.
141144
// It uses static binding to the APIs the module specifically exports to JS.
142145
JSPropertyDescriptor.Function("require", LoadModule),
@@ -164,12 +167,14 @@ JSValue removeListener(JSCallbackArgs args)
164167

165168
// Export the System.Runtime and System.Console assemblies by default.
166169
LoadAssemblyTypes(typeof(object).Assembly);
167-
_loadedAssembliesByName.Add("System.Runtime", typeof(object).Assembly);
170+
_loadedAssembliesByName.Add(
171+
typeof(object).Assembly.GetName().Name!, typeof(object).Assembly);
168172

169173
if (typeof(Console).Assembly != typeof(object).Assembly)
170174
{
171175
LoadAssemblyTypes(typeof(Console).Assembly);
172-
_loadedAssembliesByName.Add("System.Console", typeof(Console).Assembly);
176+
_loadedAssembliesByName.Add(
177+
typeof(Console).Assembly.GetName().Name!, typeof(Console).Assembly);
173178
}
174179
}
175180

@@ -324,6 +329,22 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
324329
return default;
325330
}
326331

332+
public static JSValue GetRuntimeVersion(JSCallbackArgs _)
333+
{
334+
return Environment.Version.ToString();
335+
}
336+
337+
public static JSValue GetFrameworkMoniker(JSCallbackArgs _)
338+
{
339+
Version runtimeVersion = Environment.Version;
340+
341+
// For .NET 4 the minor version may be higher, but net472 is the only TFM supported.
342+
string tfm = runtimeVersion.Major == 4 ? "net472" :
343+
$"net{runtimeVersion.Major}.{runtimeVersion.Minor}";
344+
345+
return tfm;
346+
}
347+
327348
/// <summary>
328349
/// Loads a .NET assembly that was built to be a Node API module, using static binding to
329350
/// the APIs the module specifically exports to JS.

src/NodeApi.Generator/TypeDefinitionsGenerator.cs

Lines changed: 40 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,11 @@ public SourceText GenerateTypeDefinitions(bool? autoCamelCase = null)
377377

378378
private bool IsTypeExported(Type type)
379379
{
380+
if (IsExcludedNamespace(type.Namespace))
381+
{
382+
return false;
383+
}
384+
380385
// Types not in the current assembly are not exported from this TS module.
381386
// (But support mscorlib and System.Runtime forwarding to System.Private.CoreLib.)
382387
if (type.Assembly != _assembly &&
@@ -626,7 +631,8 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
626631
foreach (PropertyInfo property in type.GetProperties(
627632
BindingFlags.Public | BindingFlags.Static))
628633
{
629-
if (!IsExcludedMember(property))
634+
// Indexed properties are not implemented.
635+
if (!IsExcludedMember(property) && property.GetIndexParameters().Length == 0)
630636
{
631637
if (isFirstMember) isFirstMember = false; else s++;
632638
ExportTypeMember(ref s, property);
@@ -665,7 +671,11 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
665671
string prefix = (implements.Length == 0 ? $" {implementsKind}" : ",") +
666672
(interfaceTypes.Length > 1 ? "\n\t" : " ");
667673

668-
if (isStreamSubclass &&
674+
if (!interfaceType.IsPublic || IsExcludedNamespace(interfaceType.Namespace))
675+
{
676+
continue;
677+
}
678+
else if (isStreamSubclass &&
669679
(interfaceType.Name == nameof(IDisposable) ||
670680
interfaceType.Name == nameof(IAsyncDisposable)))
671681
{
@@ -725,7 +735,8 @@ private void GenerateClassDefinition(ref SourceBuilder s, Type type)
725735
(isStaticClass ? BindingFlags.DeclaredOnly : default) |
726736
(type.IsInterface || isGenericTypeDefinition ? default : BindingFlags.Static)))
727737
{
728-
if (!IsExcludedMember(property))
738+
// Indexed properties are not implemented.
739+
if (!IsExcludedMember(property) && property.GetIndexParameters().Length == 0)
729740
{
730741
if (isFirstMember) isFirstMember = false; else s++;
731742
ExportTypeMember(ref s, property);
@@ -763,7 +774,9 @@ private static bool HasExplicitInterfaceImplementations(Type type, Type interfac
763774
type.GetInterfaces().Any((i) => i.Name == typeof(IComparable<>).Name)) ||
764775
(interfaceType.Name == "ISpanFormattable" &&
765776
(type.Name == "INumberBase`1" ||
766-
type.GetInterfaces().Any((i) => i.Name == "INumberBase`1"))))
777+
type.GetInterfaces().Any((i) => i.Name == "INumberBase`1"))) ||
778+
(interfaceType.Name == "ICollection" &&
779+
type.Name == "IProducerConsumerCollection`1"))
767780
{
768781
// TS interfaces cannot extend multiple interfaces that have non-identical methods
769782
// with the same name. This is most commonly an issue with IComparable and
@@ -773,10 +786,10 @@ private static bool HasExplicitInterfaceImplementations(Type type, Type interfac
773786

774787
return false;
775788
}
776-
else if (type.Name == "TypeDelegator" && interfaceType.Name == "IReflectableType")
789+
else if (interfaceType.Name == "IReflectableType")
777790
{
778-
// Special case: TypeDelegator has an explicit implementation of this interface,
779-
// but it isn't detected by reflection due to the runtime type delegation.
791+
// Special case: Reflectable types have explicit implementations of this interface,
792+
// but they aren't detected by reflection due to the runtime type delegation.
780793
return true;
781794
}
782795

@@ -823,6 +836,11 @@ private static bool HasExplicitInterfaceImplementations(Type type, Type interfac
823836
}
824837
}
825838

839+
if (type.BaseType != null && type.BaseType != typeof(object))
840+
{
841+
return HasExplicitInterfaceImplementations(type.BaseType!, interfaceType);
842+
}
843+
826844
return false;
827845
}
828846

@@ -968,6 +986,21 @@ private void EndNamespace(ref SourceBuilder s, Type type)
968986
}
969987
}
970988

989+
private static bool IsExcludedNamespace(string? ns)
990+
{
991+
// These namespaces contain APIs that are problematic for TS generation.
992+
// (Mostly old .NET Framework APIs.)
993+
return ns switch
994+
{
995+
"System.Runtime.InteropServices" or
996+
"System.Runtime.Remoting.Messaging" or
997+
"System.Runtime.Serialization" or
998+
"System.Security.AccessControl" or
999+
"System.Security.Policy" => true,
1000+
_ => false,
1001+
};
1002+
}
1003+
9711004
private static bool IsExcludedMember(PropertyInfo property)
9721005
{
9731006
if (property.PropertyType.IsPointer)

src/NodeApi/Runtime/PooledBuffer.cs

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,43 @@ namespace Microsoft.JavaScript.NodeApi.Runtime;
66

77
internal struct PooledBuffer : IDisposable
88
{
9-
private ArrayPool<byte>? _pool;
10-
public static readonly PooledBuffer Empty = new(null, [], 0);
9+
public static readonly PooledBuffer Empty = new();
10+
11+
public PooledBuffer()
12+
{
13+
Buffer = [];
14+
Length = 0;
15+
}
1116

12-
public PooledBuffer(ArrayPool<byte> pool, int length)
13-
: this(pool, pool.Rent(length), length) { }
17+
#if NETFRAMEWORK
1418

15-
public PooledBuffer(ArrayPool<byte> pool, int length, int bufferMinimumLength)
16-
: this(pool, pool.Rent(bufferMinimumLength), length) { }
19+
// Avoid a dependency on System.Buffers with .NET Framwork.
20+
// It is available as a nuget package, but might not be installed in the application.
21+
// In this case the buffer is not actually pooled.
1722

18-
private PooledBuffer(ArrayPool<byte>? pool, byte[] buffer, int length)
23+
public PooledBuffer(int length) : this(length, length) { }
24+
25+
public PooledBuffer(int length, int bufferMinimumLength)
1926
{
20-
_pool = pool;
21-
Buffer = buffer;
27+
Buffer = new byte[bufferMinimumLength];
2228
Length = length;
2329
}
2430

25-
public int Length { get; private set; }
31+
public readonly void Dispose() { }
2632

27-
public readonly byte[] Buffer { get; }
33+
#else
2834

29-
public readonly Span<byte> Span => Buffer;
35+
private ArrayPool<byte>? _pool;
3036

31-
public readonly ref byte Pin() => ref Span.GetPinnableReference();
37+
private PooledBuffer(int length, int bufferMinimumLength)
38+
: this(ArrayPool<byte>.Shared, length, bufferMinimumLength) { }
39+
40+
private PooledBuffer(ArrayPool<byte> pool, int length, int bufferMinimumLength)
41+
{
42+
_pool = pool;
43+
Buffer = pool.Rent(bufferMinimumLength);
44+
Length = length;
45+
}
3246

3347
public void Dispose()
3448
{
@@ -39,6 +53,16 @@ public void Dispose()
3953
}
4054
}
4155

56+
#endif
57+
58+
public int Length { get; private set; }
59+
60+
public readonly byte[] Buffer { get; }
61+
62+
public readonly Span<byte> Span => Buffer;
63+
64+
public readonly ref byte Pin() => ref Span.GetPinnableReference();
65+
4266
public static unsafe PooledBuffer FromStringUtf8(string? value)
4367
{
4468
if (string.IsNullOrEmpty(value))
@@ -47,15 +71,8 @@ public static unsafe PooledBuffer FromStringUtf8(string? value)
4771
}
4872

4973
int byteLength = Encoding.UTF8.GetByteCount(value);
50-
PooledBuffer buffer = new(ArrayPool<byte>.Shared, byteLength, byteLength + 1);
51-
52-
fixed (char* pChars = value)
53-
fixed (byte* pBuffer = buffer.Buffer)
54-
{
55-
// The Span<byte> overload of GetBytes() would be nicer, but is not available on .NET 4.
56-
Encoding.UTF8.GetBytes(pChars, value!.Length, pBuffer, byteLength);
57-
pBuffer[byteLength] = 0;
58-
}
74+
PooledBuffer buffer = new(byteLength, byteLength + 1);
75+
Encoding.UTF8.GetBytes(value, 0, value!.Length, buffer.Buffer, 0);
5976

6077
return buffer;
6178
}

src/node-api-dotnet/index.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@
66
// This explicit module declaration enables module members to be merged with imported namespaces.
77
declare module 'node-api-dotnet' {
88

9+
/**
10+
* Gets the current .NET runtime version, for example "8.0.1".
11+
*/
12+
export const runtimeVersion: string;
13+
14+
/**
15+
* Gets the framework monikier corresponding to the current .NET runtime version,
16+
* for example "net8.0" or "net472".
17+
*/
18+
export const frameworkMoniker: string;
19+
920
/**
1021
* Loads a .NET assembly that was built to be a Node API module, using static binding to
1122
* the APIs the module specifically exports to JS.

src/node-api-dotnet/init.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,36 @@ const ridPlatform =
1616
const ridArch = process.arch === 'ia32' ? 'x86' : process.arch;
1717
const rid = `${ridPlatform}-${ridArch}`;
1818

19+
const defaultTargetFramework = 'net8.0';
20+
21+
/**
22+
* The loaded instance of the .NET Runtime. Only one instance/version may be loaded in the process.
23+
*/
24+
let dotnet = undefined;
25+
1926
/**
2027
* Initializes the Node API .NET host.
21-
* @param {string} targetFramework Minimum requested .NET version. Must be one of the target
28+
* @param {string?} targetFramework Minimum requested .NET version. Must be one of the target
2229
* framework monikers supported by the Node API .NET package. The actual loaded version of .NET
2330
* may be higher, if the requested version is not installed.
2431
* @returns {import('./index')} The Node API .NET host.
2532
*/
2633
function initialize(targetFramework) {
34+
if (!targetFramework) {
35+
// Some version was already loaded and no specific version was requested.
36+
// Return the already-loaded version.
37+
if (dotnet) {
38+
return dotnet;
39+
}
40+
41+
targetFramework = defaultTargetFramework;
42+
}
43+
2744
const assemblyName = 'Microsoft.JavaScript.NodeApi';
2845
const nativeHostPath = __dirname + `/${rid}/${assemblyName}.node`;
2946
const managedHostPath = __dirname + `/${targetFramework}/${assemblyName}.DotNetHost.dll`
3047

3148
const nativeHost = require(nativeHostPath);
32-
return nativeHost.initialize(targetFramework, managedHostPath, require);
49+
dotnet = nativeHost.initialize(targetFramework, managedHostPath, require);
50+
return dotnet;
3351
}

src/node-api-dotnet/pack.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ const targetFrameworks = ['net8.0', 'net6.0'];
2222
if (process.platform === 'win32') targetFrameworks.push('net472');
2323

2424
const aotTargetFramework = 'net8.0';
25-
const defaultManagedHostTargetFramework = 'net6.0';
2625

2726
const fs = require('fs');
2827
const path = require('path');
@@ -189,8 +188,7 @@ function copyFile(sourceFilePath, destFilePath) {
189188

190189
function generateTargetFrameworkScriptFiles(packageStageDir) {
191190
// Generate `index.js` for the default target framework, plus one for each supported target.
192-
generateTargetFrameworkScriptFile(
193-
path.join(packageStageDir, 'index.js'), defaultManagedHostTargetFramework);
191+
generateTargetFrameworkScriptFile(path.join(packageStageDir, 'index.js'));
194192
for (let tfm of targetFrameworks) {
195193
generateTargetFrameworkScriptFile(path.join(packageStageDir, tfm + '.js'), tfm);
196194
}
@@ -199,7 +197,15 @@ function generateTargetFrameworkScriptFiles(packageStageDir) {
199197
function generateTargetFrameworkScriptFile(filePath, tfm) {
200198
// Each generated entrypoint script uses `init.js` to request a specific target framework version.
201199
const js = `const initialize = require('./init');
202-
module.exports = initialize('${tfm}');
200+
module.exports = initialize(${tfm ? `'${tfm}'` : ''});
203201
`;
204202
fs.writeFileSync(filePath, js);
203+
204+
// Also generate a `.d.ts` file for each tfm, which just re-exports the default index.
205+
if (tfm) {
206+
const dts = `import './index';
207+
export * from 'node-api-dotnet';
208+
`;
209+
fs.writeFileSync(filePath.replace(/\.js$/, '.d.ts'), dts);
210+
}
205211
}

0 commit comments

Comments
 (0)