-
Notifications
You must be signed in to change notification settings - Fork 77
Expand file tree
/
Copy pathNativeHost.cs
More file actions
386 lines (331 loc) · 15.1 KB
/
NativeHost.cs
File metadata and controls
386 lines (331 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#if !(NETFRAMEWORK || NETSTANDARD)
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.JavaScript.NodeApi.Runtime;
using static Microsoft.JavaScript.NodeApi.DotNetHost.HostFxr;
using static Microsoft.JavaScript.NodeApi.DotNetHost.MSCorEE;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
namespace Microsoft.JavaScript.NodeApi.DotNetHost;
/// <summary>
/// When AOT-compiled, exposes a native entry-point that supports loading the .NET runtime
/// and the Node API managed host.
/// </summary>
internal unsafe partial class NativeHost : IDisposable
{
private static readonly string s_managedHostTypeName =
typeof(NativeHost).Namespace + ".ManagedHost";
private static JSRuntime? s_jsRuntime;
private string? _targetFramework;
private string? _managedHostPath;
private ICLRRuntimeHost* _runtimeHost;
private hostfxr_handle _hostContextHandle;
private readonly JSValueScope _hostScope;
private JSReference? _exports;
public static bool IsTracingEnabled { get; } =
Environment.GetEnvironmentVariable("NODE_API_TRACE_HOST") == "1";
public static void Trace(string msg)
{
if (IsTracingEnabled)
{
Console.WriteLine(msg);
Console.Out.Flush();
}
}
[UnmanagedCallersOnly(
EntryPoint = nameof(napi_register_module_v1),
CallConvs = new[] { typeof(CallConvCdecl) })]
public static napi_value InitializeModule(napi_env env, napi_value exports)
{
Trace($"> NativeHost.InitializeModule({env.Handle:X8}, {exports.Handle:X8})");
s_jsRuntime ??= new NodejsRuntime();
// The native host JSValueScope is not disposed after a successful initialization. It
// becomes the parent of callback scopes, allowing the JS runtime instance to be inherited.
JSValueScope hostScope = new(JSValueScopeType.NoContext, env, s_jsRuntime);
try
{
NativeHost host = new(hostScope);
// Do not use JSModuleBuilder here because it relies on having a current context.
// But the context will be set by the managed host.
new JSValue(exports, hostScope).DefineProperties(
// The package index.js will invoke the initialize method with the path to
// the managed host assembly.
JSPropertyDescriptor.Function("initialize", host.InitializeManagedHost));
}
catch (Exception ex)
{
string message = $"Failed to load CLR native host module: {ex}";
Trace(message);
s_jsRuntime.Throw(env, (napi_value)JSValue.CreateError(null, (JSValue)message));
hostScope.Dispose();
}
Trace("< NativeHost.InitializeModule()");
return exports;
}
private NativeHost(JSValueScope hostScope)
{
_hostScope = hostScope;
}
/// <summary>
/// Receives host initialization parameters from JavaScript and loads the .NET
/// runtime and managed host.
/// </summary>
/// <returns>JS exports value from the managed host.</returns>
private JSValue InitializeManagedHost(JSCallbackArgs args)
{
string targetFramework = (string)args[0];
string managedHostPath = (string)args[1];
if (_hostContextHandle != default || _runtimeHost is not null)
{
// .NET is already loaded for this host.
if (targetFramework == _targetFramework && managedHostPath == _managedHostPath &&
_exports is not null)
{
// The same version of .NET and same managed host were requested again.
// Just return the same exports object that was initialized the first time.
// Normally this shouldn't happen because the host package initialization
// script would only be loaded once by require(). But certain situations like
// drive letter or path casing inconsistencies can cause it to be loaded twice.
return _exports.GetValue();
}
else
{
throw new NotSupportedException(
$".NET ({_targetFramework}) is already initialized in the current process. " +
"Initializing multiple .NET versions is not supported.");
}
}
JSValue require = args[2];
JSValue import = args[3];
Trace($"> NativeHost.InitializeManagedHost({targetFramework}, {managedHostPath})");
try
{
JSValue exports;
if (!targetFramework.Contains('.') &&
targetFramework.StartsWith("net", StringComparison.Ordinal) &&
targetFramework.Length >= 5)
{
// .NET Framework
Version frameworkVersion = new(
int.Parse(targetFramework.Substring(3, 1)),
int.Parse(targetFramework.Substring(4, 1)),
targetFramework.Length == 5 ? 0 :
int.Parse(targetFramework.Substring(5, 1)));
exports = InitializeFrameworkHost(
frameworkVersion, managedHostPath, require, import);
}
else
{
// .NET 5 or later
#if NETFRAMEWORK || NETSTANDARD
Version dotnetVersion = Version.Parse(targetFramework.Substring(3));
#else
Version dotnetVersion = Version.Parse(targetFramework.AsSpan(3));
#endif
exports = InitializeDotNetHost(
dotnetVersion, managedHostPath, require, import);
}
// Save init parameters and result in case of re-init.
_targetFramework = targetFramework;
_managedHostPath = managedHostPath;
_exports = new JSReference(exports);
return exports;
}
catch (Exception ex)
{
Trace("Failed to initialize managed host: " + ex);
throw;
}
finally
{
Trace("< NativeHost.InitializeManagedHost()");
}
}
/// <summary>
/// Initializes the .NET Framework 4.x runtime using MSCOREE.
/// </summary>
/// <param name="minVersion">Minimum requested .NET version.</param>
/// <param name="managedHostPath">Path to the managed host assembly file.</param>
/// <param name="require">Require function passed in by the init script.</param>
/// <param name="import">Import function passed in by the init script.</param>
/// <returns>JS exports value from the managed host.</returns>
private JSValue InitializeFrameworkHost(
Version minVersion,
string managedHostPath,
JSValue require,
JSValue import)
{
Trace(" Initializing .NET Framework " + minVersion);
ICLRMetaHostPolicy* hostPolicy = CLRCreateInstance<ICLRMetaHostPolicy>(
CLSID_CLRMetaHostPolicy, IID_ICLRMetaHostPolicy);
Trace(" Created CLR meta host policy.");
ICLRRuntimeInfo* runtimeInfo = null;
try
{
CLRMetaHostPolicyFlags policyFlags = CLRMetaHostPolicyFlags.ApplyUpgradePolicy;
runtimeInfo = hostPolicy->GetRequestedRuntime(
policyFlags, managedHostPath, out string runtimeVersion);
Trace(" Runtime version: " + runtimeVersion);
_runtimeHost = runtimeInfo->GetInterface<ICLRRuntimeHost>(
CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost);
Trace(" Created runtime host.");
_runtimeHost->Start();
Trace(" Started runtime.");
// Create an "exports" object for the managed host module initialization.
JSValue exportsValue = JSValue.CreateObject();
exportsValue.SetProperty("require", require);
exportsValue.SetProperty("import", import);
napi_env env = (napi_env)exportsValue.Scope;
napi_value exports = (napi_value)exportsValue;
// The method to be executed must take a single string argument and return a uint.
// So, encode the parameters and retval pointer in the argument string.
string argument = $"{(ulong)env.Handle:X8},{(ulong)exports.Handle:X8},{(ulong)&exports:X8}";
Trace($" Calling {s_managedHostTypeName}.{nameof(InitializeModule)}({argument})");
_runtimeHost->ExecuteInDefaultAppDomain(
managedHostPath,
s_managedHostTypeName,
nameof(InitializeModule),
argument);
exportsValue = exports;
return exportsValue;
}
catch (Exception)
{
if (_runtimeHost is not null)
{
_runtimeHost->Release();
_runtimeHost = null;
}
throw;
}
finally
{
if (runtimeInfo != null) runtimeInfo->Release();
}
}
/// <summary>
/// Initializes the .NET runtime using HostFxr.
/// </summary>
/// <param name="targetVersion">Requested .NET version.</param>
/// <param name="managedHostPath">Path to the managed host assembly file.</param>
/// <param name="require">Require function passed in by the init script.</param>
/// <param name="import">Import function passed in by the init script.</param>
/// <returns>JS exports value from the managed host.</returns>
private JSValue InitializeDotNetHost(
Version targetVersion,
string managedHostPath,
JSValue require,
JSValue import)
{
Trace(" Initializing .NET " + targetVersion);
string managedHostAssemblyName = Path.GetFileNameWithoutExtension(managedHostPath);
string nodeApiAssemblyName = managedHostAssemblyName.Substring(
0, managedHostAssemblyName.LastIndexOf('.'));
string runtimeConfigPath = Path.Join(
Path.GetDirectoryName(managedHostPath), nodeApiAssemblyName + ".runtimeconfig.json");
_hostContextHandle = InitializeManagedRuntime(targetVersion, runtimeConfigPath);
// Get a CLR function that can load an assembly.
Trace(" Getting runtime load-assembly delegate...");
hostfxr_status status = hostfxr_get_runtime_delegate(
_hostContextHandle,
hostfxr_delegate_type.load_assembly_and_get_function_pointer,
out load_assembly_and_get_function_pointer loadAssembly);
CheckStatus(status, "Failed to get CLR load-assembly function.");
// TODO Get the correct assembly version (and publickeytoken) somehow.
string managedHostTypeName = $"{s_managedHostTypeName}, {managedHostAssemblyName}" +
", Version=1.0.0.0, Culture=neutral, PublicKeyToken=null";
Trace(" Loading managed host type: " + managedHostTypeName);
int managedHostPathCapacity = HostFxr.Encoding.GetByteCount(managedHostPath) + 2;
int managedHostTypeNameCapacity = HostFxr.Encoding.GetByteCount(managedHostTypeName) + 2;
int methodNameCapacity = HostFxr.Encoding.GetByteCount(nameof(InitializeModule)) + 2;
nint initializeModulePointer;
fixed (byte*
managedHostPathBytes = new byte[managedHostPathCapacity],
methodNameBytes = new byte[methodNameCapacity],
managedHostTypeNameBytes = new byte[managedHostTypeNameCapacity])
{
Encode(managedHostPath, managedHostPathBytes, managedHostPathCapacity);
Encode(
managedHostTypeName,
managedHostTypeNameBytes,
managedHostTypeNameCapacity);
Encode(nameof(InitializeModule), methodNameBytes, methodNameCapacity);
// Load the managed host assembly and get a pointer to its module initialize method.
status = loadAssembly(
managedHostPathBytes,
managedHostTypeNameBytes,
methodNameBytes,
delegateType: -1 /* UNMANAGEDCALLERSONLY_METHOD */,
reserved: default,
&initializeModulePointer);
}
CheckStatus(status, "Failed to load managed host assembly.");
Trace(" Invoking managed host method: " + nameof(InitializeModule));
// Invoke the managed host initialize method.
// (It will define some properties on the exports object passed in.)
napi_register_module_v1 initializeModule =
Marshal.GetDelegateForFunctionPointer<napi_register_module_v1>(
initializeModulePointer);
// Create an "exports" object for the managed host module initialization.
var exports = JSValue.CreateObject();
exports.SetProperty("require", require);
exports.SetProperty("import", import);
// Define a dispose method implemented by the native host that closes the CLR context.
// The managed host proxy will pass through dispose calls to this callback.
exports.DefineProperties(new JSPropertyDescriptor(
"dispose", (_) => { Dispose(); return default; }));
exports = initializeModule((napi_env)exports.Scope, (napi_value)exports);
return exports;
}
private hostfxr_handle InitializeManagedRuntime(
Version targetVersion,
string runtimeConfigPath)
{
Trace($"> NativeHost.InitializeManagedRuntime({runtimeConfigPath})");
// Load the library that provides CLR hosting APIs.
HostFxr.Initialize(targetVersion, allowPrerelease: true);
int runtimeConfigPathCapacity = HostFxr.Encoding.GetByteCount(runtimeConfigPath) + 2;
hostfxr_status status;
hostfxr_handle hostContextHandle;
fixed (byte* runtimeConfigPathBytes = new byte[runtimeConfigPathCapacity])
{
Encode(runtimeConfigPath, runtimeConfigPathBytes, runtimeConfigPathCapacity);
// Initialize the CLR with configuration from runtimeconfig.json.
Trace(" Initializing runtime...");
status = hostfxr_initialize_for_runtime_config(
runtimeConfigPathBytes, initializeParameters: null, out hostContextHandle);
}
CheckStatus(status, "Failed to initialize CLR host.");
Trace("< NativeHost.InitializeManagedRuntime()");
return hostContextHandle;
}
public void Dispose()
{
// Close the CLR host context handle, if it's still open.
if (_hostContextHandle != default)
{
hostfxr_status status = hostfxr_close(_hostContextHandle);
_hostContextHandle = default;
CheckStatus(status, "Failed to dispose CLR host.");
}
// Release the .NET Framework runtime host object, if it is held.
if (_runtimeHost is not null)
{
_runtimeHost->Release();
_runtimeHost = null;
}
}
private static void CheckStatus(hostfxr_status status, string message)
{
if (status != hostfxr_status.Success &&
status != hostfxr_status.Success_HostAlreadyInitialized)
{
throw new Exception(Enum.IsDefined(status) ?
$"{message} Status: {status}" : $"{message} HRESULT: 0x{(uint)status:x8}");
}
}
}
#endif