diff --git a/Directory.Build.targets b/Directory.Build.targets index 63e2ded0..d48eac85 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -15,6 +15,9 @@ + + %(PackageVersion.Version) + + @@ -16,7 +17,6 @@ - diff --git a/bench/Benchmarks.cs b/bench/Benchmarks.cs index d4b81f7d..228f836d 100644 --- a/bench/Benchmarks.cs +++ b/bench/Benchmarks.cs @@ -50,7 +50,8 @@ public static void Main(string[] args) GetCurrentPlatformRuntimeIdentifier(), "libnode" + GetSharedLibraryExtension()); - private napi_env _env; + private NodeEmbeddingRuntime? _runtime; + private NodeEmbeddingNodeApiScope? _nodeApiScope; private JSValue _jsString; private JSFunction _jsFunction; private JSFunction _jsFunctionWithArgs; @@ -64,6 +65,9 @@ public static void Main(string[] args) private JSFunction _jsFunctionCallMethod; private JSFunction _jsFunctionCallMethodWithArgs; private JSReference _reference = null!; + private static readonly string[] s_settings = new[] { "node", "--expose-gc" }; + private static string s_mainScript { get; } = + "globalThis.require = require('module').createRequire(process.execPath);\n"; /// /// Simple class that is exported to JS and used in some benchmarks. @@ -84,16 +88,19 @@ public static void Method() { } /// protected void Setup() { - NodejsPlatform platform = new(LibnodePath/*, args: new[] { "node", "--expose-gc" }*/); - - // This setup avoids using NodejsEnvironment so benchmarks can run on the same thread. - // NodejsEnvironment creates a separate thread that would slow down the micro-benchmarks. - platform.Runtime.CreateEnvironment( - platform, Console.WriteLine, null, NodejsEnvironment.NodeApiVersion, out _env) - .ThrowIfFailed(); - - // The new scope instance saves itself as the thread-local JSValueScope.Current. - JSValueScope scope = new(JSValueScopeType.Root, _env, platform.Runtime); + NodeEmbeddingPlatform platform = new( + LibnodePath, + new NodeEmbeddingPlatformSettings { Args = s_settings }); + + // This setup avoids using NodejsEmbeddingThreadRuntime so benchmarks can run on + // the same thread. NodejsEmbeddingThreadRuntime creates a separate thread that would slow + // down the micro-benchmarks. + _runtime = NodeEmbeddingRuntime.Create(platform, + new NodeEmbeddingRuntimeSettings { MainScript = s_mainScript }); + + // The nodeApiScope creates JSValueScope instance that saves itself as + // the thread-local JSValueScope.Current. + _nodeApiScope = new(_runtime); // Create some JS values that will be used by the benchmarks. diff --git a/examples/jsdom/Program.cs b/examples/jsdom/Program.cs index 59b26d05..2a18aace 100644 --- a/examples/jsdom/Program.cs +++ b/examples/jsdom/Program.cs @@ -11,8 +11,13 @@ public static void Main() { string appDir = Path.GetDirectoryName(typeof(Program).Assembly.Location)!; string libnodePath = Path.Combine(appDir, "libnode.dll"); - using NodejsPlatform nodejsPlatform = new(libnodePath); - using NodejsEnvironment nodejs = nodejsPlatform.CreateEnvironment(appDir); + using NodeEmbeddingPlatform nodejsPlatform = new(libnodePath, null); + using NodeEmbeddingThreadRuntime nodejs = nodejsPlatform.CreateThreadRuntime(appDir, + new NodeEmbeddingRuntimeSettings + { + MainScript = + "globalThis.require = require('module').createRequire(process.execPath);\n" + }); if (Debugger.IsAttached) { int pid = Process.GetCurrentProcess().Id; @@ -25,7 +30,7 @@ public static void Main() Console.WriteLine(content); } - private static string GetContent(NodejsEnvironment nodejs, string html) + private static string GetContent(NodeEmbeddingThreadRuntime nodejs, string html) { JSValue jsdomClass = nodejs.Import(module: "jsdom", property: "JSDOM"); JSValue dom = jsdomClass.CallAsConstructor(html); diff --git a/examples/jsdom/jsdom.csproj b/examples/jsdom/jsdom.csproj index 37f04217..492e9909 100644 --- a/examples/jsdom/jsdom.csproj +++ b/examples/jsdom/jsdom.csproj @@ -11,13 +11,10 @@ - - false - PreserveNewest - + diff --git a/examples/winui-fluid/App.xaml.cs b/examples/winui-fluid/App.xaml.cs index 1c5840c6..5033c6de 100644 --- a/examples/winui-fluid/App.xaml.cs +++ b/examples/winui-fluid/App.xaml.cs @@ -26,9 +26,12 @@ public App() string appDir = Path.GetDirectoryName(typeof(App).Assembly.Location)!; string libnodePath = Path.Combine(appDir, "libnode.dll"); - NodejsPlatform nodejsPlatform = new(libnodePath); - - Nodejs = nodejsPlatform.CreateEnvironment(appDir); + NodeEmbeddingPlatform nodejsPlatform = new(libnodePath, null); + Nodejs = nodejsPlatform.CreateThreadRuntime(appDir, new NodeEmbeddingRuntimeSettings + { + MainScript = + "globalThis.require = require('module').createRequire(process.execPath);\n" + }); if (Debugger.IsAttached) { int pid = Process.GetCurrentProcess().Id; @@ -60,5 +63,5 @@ private void OnMainWindowClosed(object sender, WindowEventArgs args) public static new App Current => (App)Application.Current; - public NodejsEnvironment Nodejs { get; } + public NodeEmbeddingThreadRuntime Nodejs { get; } } diff --git a/examples/winui-fluid/CollabEditBox.xaml.cs b/examples/winui-fluid/CollabEditBox.xaml.cs index cf6a310f..920bd764 100644 --- a/examples/winui-fluid/CollabEditBox.xaml.cs +++ b/examples/winui-fluid/CollabEditBox.xaml.cs @@ -28,7 +28,7 @@ public sealed partial class CollabEditBox : UserControl private const string FluidServiceUri = "http://localhost:7070/"; private readonly SynchronizationContext uiSyncContext; - private readonly NodejsEnvironment nodejs; + private readonly NodeEmbeddingThreadRuntime nodejs; private readonly JSMarshaller marshaller; private ITinyliciousClient fluidClient = null!; diff --git a/examples/winui-fluid/winui-fluid.csproj b/examples/winui-fluid/winui-fluid.csproj index d8cb573c..064a8fb8 100644 --- a/examples/winui-fluid/winui-fluid.csproj +++ b/examples/winui-fluid/winui-fluid.csproj @@ -42,13 +42,10 @@ - - false - PreserveNewest - + diff --git a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs index 16bf7067..41c327fa 100644 --- a/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs +++ b/src/NodeApi.DotNetHost/JSRuntimeContextExtensions.cs @@ -54,7 +54,7 @@ public static T Import( /// Both and /// are null. public static T Import( - this NodejsEnvironment nodejs, + this NodeEmbeddingThreadRuntime nodejs, string? module, string? property, bool esModule, diff --git a/src/NodeApi/Interop/EmptyAttributes.cs b/src/NodeApi/Interop/EmptyAttributes.cs index d9617639..6cc66618 100644 --- a/src/NodeApi/Interop/EmptyAttributes.cs +++ b/src/NodeApi/Interop/EmptyAttributes.cs @@ -55,6 +55,23 @@ public CallerArgumentExpressionAttribute(string parameterName) public string ParameterName { get; } } + + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property + | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)] + public sealed class RequiredMemberAttribute : Attribute + { + } + + [AttributeUsage(AttributeTargets.All, AllowMultiple = true, Inherited = false)] + public sealed class CompilerFeatureRequiredAttribute : Attribute + { + public CompilerFeatureRequiredAttribute (string featureName) + { + FeatureName = featureName; + } + + public string FeatureName { get; } + } } namespace System.Diagnostics diff --git a/src/NodeApi/NodeApiStatusExtensions.cs b/src/NodeApi/NodeApiStatusExtensions.cs index 62596899..d1c6a6f9 100644 --- a/src/NodeApi/NodeApiStatusExtensions.cs +++ b/src/NodeApi/NodeApiStatusExtensions.cs @@ -4,7 +4,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; +using Microsoft.JavaScript.NodeApi.Runtime; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; +using static Microsoft.JavaScript.NodeApi.Runtime.NodejsRuntime; namespace Microsoft.JavaScript.NodeApi; @@ -59,5 +61,19 @@ public static T ThrowIfFailed(this napi_status status, status.ThrowIfFailed(memberName, sourceFilePath, sourceLineNumber); return value; } + + [StackTraceHidden] + public static void ThrowIfFailed([DoesNotReturnIf(true)] this NodeEmbeddingStatus status, + [CallerMemberName] string memberName = "", + [CallerFilePath] string sourceFilePath = "", + [CallerLineNumber] int sourceLineNumber = 0) + { + if (status == NodeEmbeddingStatus.OK) + return; + throw new JSException($""" + Error in {memberName} at {sourceFilePath}:{sourceLineNumber} + {NodeEmbedding.JSRuntime.EmbeddingGetLastErrorMessage()} + """); + } } diff --git a/src/NodeApi/Runtime/JSRuntime.Types.cs b/src/NodeApi/Runtime/JSRuntime.Types.cs index 54d3b20e..437b1065 100644 --- a/src/NodeApi/Runtime/JSRuntime.Types.cs +++ b/src/NodeApi/Runtime/JSRuntime.Types.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Runtime.InteropServices; -namespace Microsoft.JavaScript.NodeApi.Runtime; - // Type definitions from Node.JS js_native_api.h and js_native_api_types.h public unsafe partial class JSRuntime { @@ -31,7 +31,6 @@ public record struct napi_handle_scope(nint Handle); public record struct napi_escapable_handle_scope(nint Handle); public record struct napi_callback_info(nint Handle); public record struct napi_deferred(nint Handle); - public record struct napi_platform(nint Handle); //=========================================================================== // Enum types @@ -141,17 +140,6 @@ public napi_finalize(napi_finalize.Delegate callback) : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } - public struct napi_error_message_handler - { - public nint Handle; - - [UnmanagedFunctionPointer(CallingConvention.Cdecl)] - public delegate void Delegate(byte* message); - - public napi_error_message_handler(napi_error_message_handler.Delegate handler) - => Handle = Marshal.GetFunctionPointerForDelegate(handler); - } - public struct napi_property_descriptor { // One of utf8name or name should be NULL. diff --git a/src/NodeApi/Runtime/JSRuntime.cs b/src/NodeApi/Runtime/JSRuntime.cs index 8177e8cc..66fa3629 100644 --- a/src/NodeApi/Runtime/JSRuntime.cs +++ b/src/NodeApi/Runtime/JSRuntime.cs @@ -1,12 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace Microsoft.JavaScript.NodeApi.Runtime; - using static NodejsRuntime; /// @@ -519,20 +519,130 @@ public virtual napi_status GetBufferInfo( #region Embedding - public virtual napi_status CreatePlatform( - string[]? args, - Action? errorHandler, - out napi_platform result) => throw NS(); - public virtual napi_status DestroyPlatform(napi_platform platform) => throw NS(); - public virtual napi_status CreateEnvironment( - napi_platform platform, - Action? errorHandler, - string? mainScript, - int apiVersion, - out napi_env result) => throw NS(); - public virtual napi_status DestroyEnvironment(napi_env env, out int exitCode) => throw NS(); - public virtual napi_status RunEnvironment(napi_env env) => throw NS(); - public virtual napi_status AwaitPromise(napi_env env, napi_value promise, out napi_value result) => throw NS(); + public virtual string EmbeddingGetLastErrorMessage() => throw NS(); + + public virtual void EmbeddingSetLastErrorMessage(ReadOnlySpan message) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRunMain( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingCreatePlatform( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + out node_embedding_platform result) => throw NS(); + + public virtual NodeEmbeddingStatus + EmbeddingDeletePlatform(node_embedding_platform platform) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingPlatformConfigSetFlags( + node_embedding_platform_config platform_config, + NodeEmbeddingPlatformFlags flags) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingPlatformGetParsedArgs( + node_embedding_platform platform, + nint args_count, + nint args, + nint runtime_args_count, + nint runtime_args) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRunRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingCreateRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data, + out node_embedding_runtime result) => throw NS(); + + public virtual NodeEmbeddingStatus + EmbeddingDeleteRuntime(node_embedding_runtime runtime) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigSetNodeApiVersion( + node_embedding_runtime_config runtime_config, + int node_api_version) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigSetFlags( + node_embedding_runtime_config runtime_config, + NodeEmbeddingRuntimeFlags flags) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigSetArgs( + node_embedding_runtime_config runtime_config, + ReadOnlySpan args, + ReadOnlySpan runtime_args) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigOnPreload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback preload, + nint preload_data, + node_embedding_data_release_callback release_preload_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback run_load, + nint load_data, + node_embedding_data_release_callback release_load_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_loaded, + nint handle_loaded_data, + node_embedding_data_release_callback release_handle_loaded_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigAddModule( + node_embedding_runtime_config runtime_config, + ReadOnlySpan module_name, + node_embedding_module_initialize_callback init_module, + nint init_module_data, + node_embedding_data_release_callback release_init_module_data, + int module_node_api_version) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeSetUserData( + node_embedding_runtime runtime, + nint user_data, + node_embedding_data_release_callback release_user_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeGetUserData( + node_embedding_runtime runtime, + out nint user_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeConfigSetTaskRunner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + nint post_task_data, + node_embedding_data_release_callback release_post_task_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeRunEventLoop( + node_embedding_runtime runtime) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeTerminateEventLoop( + node_embedding_runtime runtime) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeRunOnceEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeRunNoWaitEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeRunNodeApi( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + nint run_node_api_data) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeOpenNodeApiScope( + node_embedding_runtime runtime, + out node_embedding_node_api_scope node_api_scope, + out napi_env env) => throw NS(); + + public virtual NodeEmbeddingStatus EmbeddingRuntimeCloseNodeApiScope( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope) => throw NS(); #endregion } diff --git a/src/NodeApi/Runtime/NodeEmbedding.cs b/src/NodeApi/Runtime/NodeEmbedding.cs new file mode 100644 index 00000000..39f63c22 --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbedding.cs @@ -0,0 +1,420 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +#if UNMANAGED_DELEGATES +using System.Runtime.CompilerServices; +#endif +using System.Runtime.InteropServices; +using static JSRuntime; +using static NodejsRuntime; + +/// +/// Shared code for the Node.js embedding classes. +/// +public sealed class NodeEmbedding +{ + public static readonly int EmbeddingApiVersion = 1; + public static readonly int NodeApiVersion = 8; + + private static JSRuntime? s_jsRuntime; + + public static JSRuntime JSRuntime + { + get + { + if (s_jsRuntime == null) + { + throw new InvalidOperationException("The JSRuntime is not initialized."); + } + return s_jsRuntime; + } + } + + public static void Initialize(string libNodePath) + { + if (string.IsNullOrEmpty(libNodePath)) throw new ArgumentNullException(nameof(libNodePath)); + if (s_jsRuntime != null) + { + throw new InvalidOperationException( + "The JSRuntime can be initialized only once per process."); + } + nint libnodeHandle = NativeLibrary.Load(libNodePath); + s_jsRuntime = new NodejsRuntime(libnodeHandle); + } + + public delegate void ConfigurePlatformCallback(node_embedding_platform_config platformConfig); + public delegate void ConfigureRuntimeCallback( + node_embedding_platform platform, node_embedding_runtime_config runtimeConfig); + public delegate void PreloadCallback( + NodeEmbeddingRuntime runtime, JSValue process, JSValue require); + public delegate JSValue LoadingCallback( + NodeEmbeddingRuntime runtime, JSValue process, JSValue require, JSValue runCommonJS); + public delegate void LoadedCallback( + NodeEmbeddingRuntime runtime, JSValue loadResul); + public delegate JSValue InitializeModuleCallback( + NodeEmbeddingRuntime runtime, string moduleName, JSValue exports); + public delegate void RunTaskCallback(); + public delegate bool PostTaskCallback( + node_embedding_task_run_callback runTask, + nint taskData, + node_embedding_data_release_callback releaseTaskData); + public delegate void RunNodeApiCallback(); + + public struct Functor + { + public nint Data; + public T Callback; + public readonly unsafe node_embedding_data_release_callback DataRelease => + new(s_releaseDataCallback); + } + + public struct FunctorRef : IDisposable + { + public nint Data; + public T Callback; + + public readonly void Dispose() + { + if (Data != default) + GCHandle.FromIntPtr(Data).Free(); + } + } + + public static unsafe FunctorRef + CreatePlatformConfigureFunctorRef(ConfigurePlatformCallback? callback) => new() + { + Data = callback != null ? (nint)GCHandle.Alloc(callback) : default, + Callback = callback != null + ? new node_embedding_platform_configure_callback(s_platformConfigureCallback) + : default + }; + + public static unsafe FunctorRef + CreateRuntimeConfigureFunctorRef(ConfigureRuntimeCallback? callback) => new() + { + Data = callback != null ? (nint)GCHandle.Alloc(callback) : default, + Callback = callback != null + ? new node_embedding_runtime_configure_callback(s_runtimeConfigureCallback) + : default + }; + + public static unsafe Functor + CreateRuntimePreloadFunctor(PreloadCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_runtime_preload_callback(s_runtimePreloadCallback) + }; + + public static unsafe Functor + CreateRuntimeLoadingFunctor(LoadingCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_runtime_loading_callback(s_runtimeLoadingCallback) + }; + + public static unsafe Functor + CreateRuntimeLoadedFunctor(LoadedCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_runtime_loaded_callback(s_runtimeLoadedCallback) + }; + + public static unsafe Functor + CreateModuleInitializeFunctor(InitializeModuleCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_module_initialize_callback(s_moduleInitializeCallback) + }; + + public static unsafe Functor + CreateTaskPostFunctor(PostTaskCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_task_post_callback(s_taskPostCallback) + }; + + public static unsafe FunctorRef + CreateNodeApiRunFunctorRef(RunNodeApiCallback callback) => new() + { + Data = (nint)GCHandle.Alloc(callback), + Callback = new node_embedding_node_api_run_callback(s_nodeApiRunCallback) + }; + +#if UNMANAGED_DELEGATES + internal static readonly unsafe delegate* unmanaged[Cdecl] + s_releaseDataCallback = &ReleaseDataCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, node_embedding_platform_config, NodeEmbeddingStatus> + s_platformConfigureCallback = &PlatformConfigureCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, + node_embedding_platform, + node_embedding_runtime_config, + NodeEmbeddingStatus> + s_runtimeConfigureCallback = &RuntimeConfigureCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, node_embedding_runtime, napi_env, napi_value, napi_value, void> + s_runtimePreloadCallback = &RuntimePreloadCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, node_embedding_runtime, napi_env, napi_value, napi_value, napi_value, napi_value> + s_runtimeLoadingCallback = &RuntimeLoadingCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, node_embedding_runtime, napi_env, napi_value, void> + s_runtimeLoadedCallback = &RuntimeLoadedCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, node_embedding_runtime, napi_env, nint, napi_value, napi_value> + s_moduleInitializeCallback = &ModuleInitializeCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl] + s_taskRunTaskCallback = &TaskRunCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, + node_embedding_task_run_callback, + nint, + node_embedding_data_release_callback, + nint, + NodeEmbeddingStatus> + s_taskPostCallback = &TaskPostCallbackAdapter; + internal static readonly unsafe delegate* unmanaged[Cdecl]< + nint, napi_env, void> + s_nodeApiRunCallback = &NodeApiRunCallbackAdapter; +#else + internal static readonly node_embedding_data_release_callback.Delegate + s_releaseDataCallback = ReleaseDataCallbackAdapter; + internal static readonly node_embedding_platform_configure_callback.Delegate + s_platformConfigureCallback = PlatformConfigureCallbackAdapter; + internal static readonly node_embedding_runtime_configure_callback.Delegate + s_runtimeConfigureCallback = RuntimeConfigureCallbackAdapter; + internal static readonly node_embedding_runtime_preload_callback.Delegate + s_runtimePreloadCallback = RuntimePreloadCallbackAdapter; + internal static readonly node_embedding_runtime_loading_callback.Delegate + s_runtimeLoadingCallback = RuntimeLoadingCallbackAdapter; + internal static readonly node_embedding_runtime_loaded_callback.Delegate + s_runtimeLoadedCallback = RuntimeLoadedCallbackAdapter; + internal static readonly node_embedding_module_initialize_callback.Delegate + s_moduleInitializeCallback = ModuleInitializeCallbackAdapter; + internal static readonly node_embedding_task_run_callback.Delegate + s_taskRunCallback = TaskRunCallbackAdapter; + internal static readonly node_embedding_task_post_callback.Delegate + s_taskPostCallback = TaskPostCallbackAdapter; + internal static readonly node_embedding_node_api_run_callback.Delegate + s_nodeApiRunCallback = NodeApiRunCallbackAdapter; +#endif + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe NodeEmbeddingStatus ReleaseDataCallbackAdapter(nint data) + { + if (data != default) + { + GCHandle.FromIntPtr(data).Free(); + } + return NodeEmbeddingStatus.OK; + } + + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe NodeEmbeddingStatus PlatformConfigureCallbackAdapter( + nint cb_data, + node_embedding_platform_config platform_config) + { + try + { + var callback = (ConfigurePlatformCallback)GCHandle.FromIntPtr(cb_data).Target!; + callback(platform_config); + return NodeEmbeddingStatus.OK; + } + catch (Exception ex) + { + JSRuntime.EmbeddingSetLastErrorMessage(ex.Message.AsSpan()); + return NodeEmbeddingStatus.GenericError; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe NodeEmbeddingStatus RuntimeConfigureCallbackAdapter( + nint cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config) + { + try + { + var callback = (ConfigureRuntimeCallback)GCHandle.FromIntPtr(cb_data).Target!; + callback(platform, runtime_config); + return NodeEmbeddingStatus.OK; + } + catch (Exception ex) + { + JSRuntime.EmbeddingSetLastErrorMessage(ex.Message.AsSpan()); + return NodeEmbeddingStatus.GenericError; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe void RuntimePreloadCallbackAdapter( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require) + { + using var jsValueScope = new JSValueScope(JSValueScopeType.Root, env, JSRuntime); + try + { + var callback = (PreloadCallback)GCHandle.FromIntPtr(cb_data).Target!; + NodeEmbeddingRuntime embeddingRuntime = NodeEmbeddingRuntime.FromHandle(runtime); + callback(embeddingRuntime, new JSValue(process), new JSValue(require)); + } + catch (Exception ex) + { + JSError.ThrowError(ex); + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe napi_value RuntimeLoadingCallbackAdapter( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs) + { + using var jsValueScope = new JSValueScope(JSValueScopeType.Root, env, JSRuntime); + try + { + var callback = (LoadingCallback)GCHandle.FromIntPtr(cb_data).Target!; + NodeEmbeddingRuntime embeddingRuntime = NodeEmbeddingRuntime.FromHandle(runtime); + return (napi_value)callback( + embeddingRuntime, new JSValue(process), new JSValue(require), new JSValue(run_cjs)); + } + catch (Exception ex) + { + JSError.ThrowError(ex); + return napi_value.Null; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe void RuntimeLoadedCallbackAdapter( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value loading_result) + { + using var jsValueScope = new JSValueScope(JSValueScopeType.Root, env, JSRuntime); + try + { + var callback = (LoadedCallback)GCHandle.FromIntPtr(cb_data).Target!; + NodeEmbeddingRuntime embeddingRuntime = NodeEmbeddingRuntime.FromHandle(runtime); + callback(embeddingRuntime, new JSValue(loading_result)); + } + catch (Exception ex) + { + JSError.ThrowError(ex); + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe napi_value ModuleInitializeCallbackAdapter( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + nint module_name, + napi_value exports) + { + using var jsValueScope = new JSValueScope(JSValueScopeType.Root, env, JSRuntime); + try + { + var callback = (InitializeModuleCallback)GCHandle.FromIntPtr(cb_data).Target!; + NodeEmbeddingRuntime embeddingRuntime = NodeEmbeddingRuntime.FromHandle(runtime); + return (napi_value)callback( + embeddingRuntime, + Utf8StringArray.PtrToStringUTF8((byte*)module_name), + new JSValue(exports)); + } + catch (Exception ex) + { + JSError.ThrowError(ex); + return napi_value.Null; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe NodeEmbeddingStatus TaskRunCallbackAdapter(nint cb_data) + { + try + { + var callback = (RunTaskCallback)GCHandle.FromIntPtr(cb_data).Target!; + callback(); + return NodeEmbeddingStatus.OK; + } + catch (Exception ex) + { + JSRuntime.EmbeddingSetLastErrorMessage(ex.Message.AsSpan()); + return NodeEmbeddingStatus.GenericError; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe NodeEmbeddingStatus TaskPostCallbackAdapter( + nint cb_data, + node_embedding_task_run_callback run_task, + nint task_data, + node_embedding_data_release_callback release_task_data, + nint is_posted) + { + try + { + var callback = (PostTaskCallback)GCHandle.FromIntPtr(cb_data).Target!; + bool isPosted = callback(run_task, task_data, release_task_data); + if (is_posted != default) + { + *(c_bool*)is_posted = isPosted; + } + + return NodeEmbeddingStatus.OK; + } + catch (Exception ex) + { + JSRuntime.EmbeddingSetLastErrorMessage(ex.Message.AsSpan()); + return NodeEmbeddingStatus.GenericError; + } + } + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + internal static unsafe void NodeApiRunCallbackAdapter(nint cb_data, napi_env env) + { + using var jsValueScope = new JSValueScope(JSValueScopeType.Root, env, JSRuntime); + try + { + var callback = (RunNodeApiCallback)GCHandle.FromIntPtr(cb_data).Target!; + callback(); + } + catch (Exception ex) + { + JSError.ThrowError(ex); + } + } +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingModuleInfo.cs b/src/NodeApi/Runtime/NodeEmbeddingModuleInfo.cs new file mode 100644 index 00000000..739c023c --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingModuleInfo.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using static NodeEmbedding; + +public class NodeEmbeddingModuleInfo +{ + public required string Name { get; set; } + public required InitializeModuleCallback OnInitialize { get; set; } + public int? NodeApiVersion { get; set; } +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingNodeApiScope.cs b/src/NodeApi/Runtime/NodeEmbeddingNodeApiScope.cs new file mode 100644 index 00000000..6513384b --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingNodeApiScope.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +using static JSRuntime; +using static NodejsRuntime; + +public sealed class NodeEmbeddingNodeApiScope : IDisposable +{ + readonly NodeEmbeddingRuntime _runtime; + private node_embedding_node_api_scope _nodeApiScope; + private readonly JSValueScope _valueScope; + + public NodeEmbeddingNodeApiScope(NodeEmbeddingRuntime runtime) + { + _runtime = runtime; + NodeEmbedding.JSRuntime.EmbeddingRuntimeOpenNodeApiScope( + runtime.Handle, out _nodeApiScope, out napi_env env) + .ThrowIfFailed(); + _valueScope = new JSValueScope( + JSValueScopeType.Root, env, NodeEmbedding.JSRuntime); + } + + /// + /// Gets a value indicating whether the Node.js embedding Node-API scope is disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Disposes the Node.js embedding Node-API scope. + /// + public void Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + + _valueScope.Dispose(); + NodeEmbedding.JSRuntime.EmbeddingRuntimeCloseNodeApiScope( + _runtime.Handle, _nodeApiScope) + .ThrowIfFailed(); + } +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs b/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs new file mode 100644 index 00000000..ccf69f02 --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingPlatform.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +using static NodeEmbedding; +using static NodejsRuntime; + +/// +/// Manages a Node.js platform instance, provided by `libnode`. +/// +/// +/// Only one Node.js platform instance can be created per process. Once the platform is disposed, +/// another platform instance cannot be re-initialized. One or more +/// instances may be created using the platform. +/// +public sealed class NodeEmbeddingPlatform : IDisposable +{ + private node_embedding_platform _platform; + + public static explicit operator node_embedding_platform(NodeEmbeddingPlatform platform) + => platform._platform; + + /// + /// Initializes the Node.js platform. + /// + /// Path to the `libnode` shared library, including extension. + /// Optional platform settings. + /// A Node.js platform instance has already been + /// loaded in the current process. + public NodeEmbeddingPlatform(string libNodePath, NodeEmbeddingPlatformSettings? settings) + { + if (Current != null) + { + throw new InvalidOperationException( + "Only one Node.js platform instance per process is allowed."); + } + Current = this; + Initialize(libNodePath); + + using FunctorRef functorRef = + CreatePlatformConfigureFunctorRef(settings?.CreateConfigurePlatformCallback()); + NodeEmbedding.JSRuntime.EmbeddingCreatePlatform( + settings?.Args ?? new string[] { "node" }, + functorRef.Callback, + functorRef.Data, + out _platform) + .ThrowIfFailed(); + } + + public node_embedding_platform Handle => _platform; + + /// + /// Gets a value indicating whether the current platform has been disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Gets the Node.js platform instance for the current process, or null if not initialized. + /// + public static NodeEmbeddingPlatform? Current { get; private set; } + + /// + /// Disposes the platform. After disposal, another platform instance may not be initialized + /// in the current process. + /// + public void Dispose() + { + if (IsDisposed) return; + IsDisposed = true; + NodeEmbedding.JSRuntime.EmbeddingDeletePlatform(_platform); + } + + /// + /// Creates a new Node.js embedding runtime with a dedicated main thread. + /// + /// Optional directory that is used as the base directory when resolving + /// imported modules, and also as the value of the global `__dirname` property. If unspecified, + /// importing modules is not enabled and `__dirname` is undefined. + /// Optional script to run in the environment. (Literal script content, + /// not a path to a script file.) + /// A new instance. + public NodeEmbeddingThreadRuntime CreateThreadRuntime( + string? baseDir = null, + NodeEmbeddingRuntimeSettings? settings = null) + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingPlatform)); + + return new NodeEmbeddingThreadRuntime(this, baseDir, settings); + } + + public unsafe string[] GetParsedArgs() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingPlatform)); + + int argc = 0; + nint argv = 0; + NodeEmbedding.JSRuntime.EmbeddingPlatformGetParsedArgs( + _platform, (nint)(&argc), (nint)(&argv), 0, 0).ThrowIfFailed(); + return Utf8StringArray.ToStringArray(argv, argc); + } + + public unsafe string[] GetRuntimeParsedArgs() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingPlatform)); + + int argc = 0; + nint argv = 0; + NodeEmbedding.JSRuntime.EmbeddingPlatformGetParsedArgs( + _platform, 0, 0, (nint)(&argc), (nint)(&argv)).ThrowIfFailed(); + return Utf8StringArray.ToStringArray(argv, argc); + } +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs b/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs new file mode 100644 index 00000000..9bf6d580 --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingPlatformSettings.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using static NodeEmbedding; +using static NodejsRuntime; + +public class NodeEmbeddingPlatformSettings +{ + public NodeEmbeddingPlatformFlags? PlatformFlags { get; set; } + public string[]? Args { get; set; } + public ConfigurePlatformCallback? ConfigurePlatform { get; set; } + + public unsafe ConfigurePlatformCallback CreateConfigurePlatformCallback() + => new((config) => + { + if (PlatformFlags != null) + { + NodeEmbedding.JSRuntime.EmbeddingPlatformConfigSetFlags(config, PlatformFlags.Value) + .ThrowIfFailed(); + } + ConfigurePlatform?.Invoke(config); + }); +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingRuntime.cs b/src/NodeApi/Runtime/NodeEmbeddingRuntime.cs new file mode 100644 index 00000000..f84aa1cd --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingRuntime.cs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +#if UNMANAGED_DELEGATES +using System.Runtime.CompilerServices; +#endif +using System.Runtime.InteropServices; +using static NodeEmbedding; +using static NodejsRuntime; + +/// +/// A Node.js runtime. +/// +/// +/// Multiple Node.js environments may be created (concurrently) in the same process. +/// +public sealed class NodeEmbeddingRuntime : IDisposable +{ + private bool _mustDeleteRuntime; // Only delete runtime if it was created by calling Create. + + public static unsafe NodeEmbeddingRuntime Create( + NodeEmbeddingPlatform platform, NodeEmbeddingRuntimeSettings? settings = null) + { + using FunctorRef functorRef = + CreateRuntimeConfigureFunctorRef(settings?.CreateConfigureRuntimeCallback()); + NodeEmbedding.JSRuntime.EmbeddingCreateRuntime( + platform.Handle, + functorRef.Callback, + functorRef.Data, + out node_embedding_runtime runtime) + .ThrowIfFailed(); + NodeEmbeddingRuntime result = FromHandle(runtime); + result._mustDeleteRuntime = true; + return result; + } + + private NodeEmbeddingRuntime(node_embedding_runtime runtime) + { + Handle = runtime; + } + + public node_embedding_runtime Handle { get; } + + public static unsafe NodeEmbeddingRuntime FromHandle(node_embedding_runtime runtime) + { + NodeEmbedding.JSRuntime.EmbeddingRuntimeGetUserData(runtime, out nint userData) + .ThrowIfFailed(); + if (userData != default) + { + return (NodeEmbeddingRuntime)GCHandle.FromIntPtr(userData).Target!; + } + + NodeEmbeddingRuntime result = new(runtime); + NodeEmbedding.JSRuntime.EmbeddingRuntimeSetUserData( + runtime, + (nint)GCHandle.Alloc(result), + new node_embedding_data_release_callback(s_releaseRuntimeCallback)) + .ThrowIfFailed(); + return result; + } + + public static unsafe void Run( + NodeEmbeddingPlatform platform, NodeEmbeddingRuntimeSettings? settings = null) + { + using FunctorRef functorRef = + CreateRuntimeConfigureFunctorRef(settings?.CreateConfigureRuntimeCallback()); + NodeEmbedding.JSRuntime.EmbeddingRunRuntime( + platform.Handle, functorRef.Callback, functorRef.Data) + .ThrowIfFailed(); + } + + /// + /// Gets a value indicating whether the Node.js environment is disposed. + /// + public bool IsDisposed { get; private set; } + + /// + /// Disposes the Node.js environment, causing its main thread to exit. + /// + public void Dispose() + { + if (IsDisposed || !_mustDeleteRuntime) return; + IsDisposed = true; + + NodeEmbedding.JSRuntime.EmbeddingDeleteRuntime(Handle).ThrowIfFailed(); + } + + public unsafe void RunEventLoop() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingRuntime)); + + NodeEmbedding.JSRuntime.EmbeddingRuntimeRunEventLoop(Handle).ThrowIfFailed(); + } + + public unsafe void TerminateEventLoop() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingRuntime)); + + NodeEmbedding.JSRuntime.EmbeddingRuntimeTerminateEventLoop(Handle).ThrowIfFailed(); + } + + public unsafe bool RunEventLoopOnce() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingRuntime)); + + NodeEmbedding.JSRuntime.EmbeddingRuntimeRunOnceEventLoop(Handle, out bool result) + .ThrowIfFailed(); + return result; + } + + public unsafe bool RunEventLoopNoWait() + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingRuntime)); + + NodeEmbedding.JSRuntime.EmbeddingRuntimeRunNoWaitEventLoop(Handle, out bool result) + .ThrowIfFailed(); + return result; + } + + public unsafe void RunNodeApi(RunNodeApiCallback runNodeApi) + { + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingRuntime)); + + using FunctorRef functorRef = + CreateNodeApiRunFunctorRef(runNodeApi); + NodeEmbedding.JSRuntime.EmbeddingRuntimeRunNodeApi( + Handle, functorRef.Callback, functorRef.Data) + .ThrowIfFailed(); + } + +#if UNMANAGED_DELEGATES + private static readonly unsafe delegate* unmanaged[Cdecl] + s_releaseRuntimeCallback = &ReleaseRuntimeCallbackAdapter; +#else + private static readonly node_embedding_data_release_callback.Delegate + s_releaseRuntimeCallback = ReleaseRuntimeCallbackAdapter; +#endif + +#if UNMANAGED_DELEGATES + [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] +#endif + private static unsafe NodeEmbeddingStatus ReleaseRuntimeCallbackAdapter(nint data) + { + if (data != default) + { + try + { + GCHandle gcHandle = GCHandle.FromIntPtr(data); + NodeEmbeddingRuntime runtime = (NodeEmbeddingRuntime)gcHandle.Target!; + gcHandle.Free(); + runtime._mustDeleteRuntime = false; + } + catch (Exception ex) + { + NodeEmbedding.JSRuntime.EmbeddingSetLastErrorMessage(ex.Message.AsSpan()); + return NodeEmbeddingStatus.GenericError; + } + } + return NodeEmbeddingStatus.OK; + } +} diff --git a/src/NodeApi/Runtime/NodeEmbeddingRuntimeSettings.cs b/src/NodeApi/Runtime/NodeEmbeddingRuntimeSettings.cs new file mode 100644 index 00000000..49261c49 --- /dev/null +++ b/src/NodeApi/Runtime/NodeEmbeddingRuntimeSettings.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using static NodeEmbedding; +using static NodejsRuntime; + +public class NodeEmbeddingRuntimeSettings +{ + public int? NodeApiVersion { get; set; } + public NodeEmbeddingRuntimeFlags? RuntimeFlags { get; set; } + public string[]? Args { get; set; } + public string[]? RuntimeArgs { get; set; } + public PreloadCallback? OnPreload { get; set; } + public string? MainScript { get; set; } + public LoadingCallback? OnLoading { get; set; } + public LoadedCallback? OnLoaded { get; set; } + public IEnumerable? Modules { get; set; } + public PostTaskCallback? OnPostTask { get; set; } + public ConfigureRuntimeCallback? ConfigureRuntime { get; set; } + + public unsafe ConfigureRuntimeCallback CreateConfigureRuntimeCallback() + => new((platform, config) => + { + if (NodeApiVersion != null) + { + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigSetNodeApiVersion( + config, NodeApiVersion.Value) + .ThrowIfFailed(); + } + if (RuntimeFlags != null) + { + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigSetFlags(config, RuntimeFlags.Value) + .ThrowIfFailed(); + } + if (Args != null || RuntimeArgs != null) + { + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigSetArgs(config, Args, RuntimeArgs) + .ThrowIfFailed(); + } + + if (OnPreload != null) + { + Functor functor = + CreateRuntimePreloadFunctor(OnPreload); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigOnPreload( + config, functor.Callback, functor.Data, functor.DataRelease) + .ThrowIfFailed(); + } + + if (MainScript != null) + { + JSValue onLoading(NodeEmbeddingRuntime runtime, + JSValue process, + JSValue require, + JSValue runCommonJS) + => runCommonJS.Call(JSValue.Null, (JSValue)MainScript); + + Functor functor = + CreateRuntimeLoadingFunctor(onLoading); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigOnLoading( + config, functor.Callback, functor.Data, functor.DataRelease) + .ThrowIfFailed(); + } + else if (OnLoading != null) + { + Functor functor = + CreateRuntimeLoadingFunctor(OnLoading); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigOnLoading( + config, functor.Callback, functor.Data, functor.DataRelease) + .ThrowIfFailed(); + } + + if (OnLoaded != null) + { + Functor functor = + CreateRuntimeLoadedFunctor(OnLoaded); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigOnLoaded( + config, functor.Callback, functor.Data, functor.DataRelease) + .ThrowIfFailed(); + } + + if (Modules != null) + { + foreach (NodeEmbeddingModuleInfo module in Modules) + { + Functor functor = + CreateModuleInitializeFunctor(module.OnInitialize); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigAddModule( + config, + module.Name.AsSpan(), + functor.Callback, + functor.Data, + functor.DataRelease, + module.NodeApiVersion ?? 0) + .ThrowIfFailed(); + } + } + + if (OnPostTask != null) + { + Functor functor = + CreateTaskPostFunctor(OnPostTask); + NodeEmbedding.JSRuntime.EmbeddingRuntimeConfigSetTaskRunner( + config, + new node_embedding_task_post_callback(s_taskPostCallback), + (nint)GCHandle.Alloc(OnPostTask), + new node_embedding_data_release_callback(s_releaseDataCallback)) + .ThrowIfFailed(); + } + ConfigureRuntime?.Invoke(platform, config); + }); +} diff --git a/src/NodeApi/Runtime/NodejsEnvironment.cs b/src/NodeApi/Runtime/NodeEmbeddingThreadRuntime.cs similarity index 89% rename from src/NodeApi/Runtime/NodejsEnvironment.cs rename to src/NodeApi/Runtime/NodeEmbeddingThreadRuntime.cs index 07c3b8f6..2635537c 100644 --- a/src/NodeApi/Runtime/NodejsEnvironment.cs +++ b/src/NodeApi/Runtime/NodeEmbeddingThreadRuntime.cs @@ -1,15 +1,14 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; using Microsoft.JavaScript.NodeApi.Interop; - -namespace Microsoft.JavaScript.NodeApi.Runtime; - using static JSRuntime; /// @@ -20,41 +19,41 @@ namespace Microsoft.JavaScript.NodeApi.Runtime; /// environment instance has its own dedicated execution thread. Except where otherwise documented, /// all interaction with the environment and JavaScript values associated with the environment MUST /// be executed on the environment's thread. Use the -/// to switch to the thread. +/// to switch to the thread. /// -public sealed class NodejsEnvironment : IDisposable +public sealed class NodeEmbeddingThreadRuntime : IDisposable { - /// - /// Corresponds to NAPI_VERSION from js_native_api.h. - /// - public const int NodeApiVersion = 8; - private readonly JSValueScope _scope; private readonly Thread _thread; - private readonly TaskCompletionSource _completion = new(); + private readonly JSThreadSafeFunction? _completion; - public static explicit operator napi_env(NodejsEnvironment environment) => + public static explicit operator napi_env(NodeEmbeddingThreadRuntime environment) => (napi_env)environment._scope; - public static implicit operator JSValueScope(NodejsEnvironment environment) => + public static implicit operator JSValueScope(NodeEmbeddingThreadRuntime environment) => environment._scope; - internal NodejsEnvironment(NodejsPlatform platform, string? baseDir, string? mainScript) + internal NodeEmbeddingThreadRuntime( + NodeEmbeddingPlatform platform, + string? baseDir, + NodeEmbeddingRuntimeSettings? settings) { JSValueScope scope = null!; JSSynchronizationContext syncContext = null!; + JSThreadSafeFunction? completion = null; using ManualResetEvent loadedEvent = new(false); _thread = new(() => { - platform.Runtime.CreateEnvironment( - (napi_platform)platform, - (error) => Console.WriteLine(error), - mainScript, - NodeApiVersion, - out napi_env env).ThrowIfFailed(); - + using var runtime = NodeEmbeddingRuntime.Create(platform, settings); // The new scope instance saves itself as the thread-local JSValueScope.Current. - scope = new JSValueScope(JSValueScopeType.Root, env, platform.Runtime); + using var nodeApiScope = new NodeEmbeddingNodeApiScope(runtime); + + completion = new JSThreadSafeFunction( + maxQueueSize: 0, + initialThreadCount: 1, + asyncResourceName: (JSValue)nameof(NodeEmbeddingThreadRuntime)); + + scope = JSValueScope.Current; syncContext = scope.RuntimeContext.SynchronizationContext; if (!string.IsNullOrEmpty(baseDir)) @@ -65,18 +64,24 @@ internal NodejsEnvironment(NodejsPlatform platform, string? baseDir, string? mai loadedEvent.Set(); - // Run the JS event loop until disposal completes the completion source. - platform.Runtime.AwaitPromise( - env, (napi_value)(JSValue)_completion.Task.AsPromise(), out _).ThrowIfFailed(); + // Run the JS event loop until disposal unrefs the completion thread safe function. + try + { + runtime.RunEventLoop(); + ExitCode = 0; + } + catch (Exception) + { + ExitCode = 1; + } syncContext.Dispose(); - platform.Runtime.DestroyEnvironment(env, out int exitCode).ThrowIfFailed(); - ExitCode = exitCode; }); _thread.Start(); loadedEvent.WaitOne(); + _completion = completion; _scope = scope; SynchronizationContext = syncContext; } @@ -110,10 +115,10 @@ private static void InitializeModuleImportFunctions( // The import keyword is not a function and is only available through use of an // external helper module. #if NETFRAMEWORK || NETSTANDARD - string assemblyLocation = new Uri(typeof(NodejsEnvironment).Assembly.CodeBase).LocalPath; + string assemblyLocation = new Uri(typeof(NodeEmbeddingThreadRuntime).Assembly.CodeBase).LocalPath; #else #pragma warning disable IL3000 // Assembly.Location returns an empty string for assemblies embedded in a single-file app - string assemblyLocation = typeof(NodejsEnvironment).Assembly.Location; + string assemblyLocation = typeof(NodeEmbeddingThreadRuntime).Assembly.Location; #pragma warning restore IL3000 #endif if (!string.IsNullOrEmpty(assemblyLocation)) @@ -198,8 +203,10 @@ public void Dispose() if (IsDisposed) return; IsDisposed = true; - // Setting the completion causes `AwaitPromise()` to return so the thread exits. - _completion.TrySetResult(true); + // Unreffing the completion should complete the Node.js event loop + // if it has nothing else to do. + // The Unref must be called in the JS thread. + _completion?.BlockingCall(() => _completion.Unref()); _thread.Join(); Debug.WriteLine($"Node.js environment exited with code: {ExitCode}"); @@ -207,7 +214,7 @@ public void Dispose() public Uri StartInspector(int? port = null, string? host = null, bool? wait = null) { - if (IsDisposed) throw new ObjectDisposedException(nameof(NodejsEnvironment)); + if (IsDisposed) throw new ObjectDisposedException(nameof(NodeEmbeddingThreadRuntime)); return SynchronizationContext.Run(() => { diff --git a/src/NodeApi/Runtime/NodeJSRuntime.cs b/src/NodeApi/Runtime/NodeJSRuntime.cs index acdec041..6855874b 100644 --- a/src/NodeApi/Runtime/NodeJSRuntime.cs +++ b/src/NodeApi/Runtime/NodeJSRuntime.cs @@ -1,12 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security; using System.Text; -namespace Microsoft.JavaScript.NodeApi.Runtime; // This part of the class includes the constructor and private helper methods. // See the other parts of this class for the actual imported APIs. [SuppressUnmanagedCodeSecurity] @@ -100,6 +101,115 @@ private nint Import(string functionName) return function; } + private delegate* unmanaged[Cdecl] Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl])Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl])Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl])Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] + Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] + Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] + Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + + private delegate* unmanaged[Cdecl] + Import( + ref delegate* unmanaged[Cdecl] function, + [CallerArgumentExpression(nameof(function))] string functionName = "") + { + if (function == null) + { + function = (delegate* unmanaged[Cdecl]) + Import(functionName); + } + return function; + } + private static unsafe string? PtrToStringUTF8(byte* ptr) { if (ptr == null) return null; diff --git a/src/NodeApi/Runtime/NodejsPlatform.cs b/src/NodeApi/Runtime/NodejsPlatform.cs deleted file mode 100644 index ec8e5556..00000000 --- a/src/NodeApi/Runtime/NodejsPlatform.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.JavaScript.NodeApi.Runtime; - -using static JSRuntime; - -/// -/// Manages a Node.js platform instance, provided by `libnode`. -/// -/// -/// Only one Node.js platform instance can be created per process. Once the platform is disposed, -/// another platform instance cannot be re-initialized. One or more -/// instances may be created using the platform. -/// -public sealed class NodejsPlatform : IDisposable -{ - private readonly napi_platform _platform; - - public static implicit operator napi_platform(NodejsPlatform platform) => platform._platform; - - /// - /// Initializes the Node.js platform. - /// - /// Path to the `libnode` shared library, including extension. - /// Optional platform arguments. - /// A Node.js platform instance has already been - /// loaded in the current process. - public NodejsPlatform( - string libnodePath, - string[]? args = null) - { - if (string.IsNullOrEmpty(libnodePath)) throw new ArgumentNullException(nameof(libnodePath)); - - if (Current != null) - { - throw new InvalidOperationException( - "Only one Node.js platform instance per process is allowed."); - } - - nint libnodeHandle = NativeLibrary.Load(libnodePath); - Runtime = new NodejsRuntime(libnodeHandle); - - Runtime.CreatePlatform(args, (error) => Console.WriteLine(error), out _platform) - .ThrowIfFailed(); - Current = this; - } - - /// - /// Gets the Node.js platform instance for the current process, or null if not initialized. - /// - public static NodejsPlatform? Current { get; private set; } - - public JSRuntime Runtime { get; } - - /// - /// Gets a value indicating whether the current platform has been disposed. - /// - public bool IsDisposed { get; private set; } - - /// - /// Disposes the platform. After disposal, another platform instance may not be initialized - /// in the current process. - /// - public void Dispose() - { - if (IsDisposed) return; - IsDisposed = true; - - Runtime.DestroyPlatform(_platform); - } - - /// - /// Creates a new Node.js environment with a dedicated main thread. - /// - /// Optional directory that is used as the base directory when resolving - /// imported modules, and also as the value of the global `__dirname` property. If unspecified, - /// importing modules is not enabled and `__dirname` is undefined. - /// Optional script to run in the environment. (Literal script content, - /// not a path to a script file.) - /// A new instance. - public NodejsEnvironment CreateEnvironment( - string? baseDir = null, - string? mainScript = null) - { - if (IsDisposed) throw new ObjectDisposedException(nameof(NodejsPlatform)); - - return new NodejsEnvironment(this, baseDir, mainScript); - } -} diff --git a/src/NodeApi/Runtime/NodejsRuntime.Embedding.cs b/src/NodeApi/Runtime/NodejsRuntime.Embedding.cs index 0e9a667b..4c3c5da7 100644 --- a/src/NodeApi/Runtime/NodejsRuntime.Embedding.cs +++ b/src/NodeApi/Runtime/NodejsRuntime.Embedding.cs @@ -1,131 +1,778 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Runtime.InteropServices; -namespace Microsoft.JavaScript.NodeApi.Runtime; - // Imports embedding APIs from libnode. public unsafe partial class NodejsRuntime { #pragma warning disable IDE1006 // Naming: missing prefix '_' - private delegate* unmanaged[Cdecl] - napi_create_platform; + //============================================================================================== + // Data types + //============================================================================================== + + public record struct node_embedding_platform(nint Handle); + public record struct node_embedding_runtime(nint Handle); + public record struct node_embedding_platform_config(nint Handle); + public record struct node_embedding_runtime_config(nint Handle); + public record struct node_embedding_node_api_scope(nint Handle); - public override napi_status CreatePlatform( - string[]? args, - Action? errorHandler, - out napi_platform result) + public enum NodeEmbeddingStatus : int { - napi_error_message_handler native_error_handler = errorHandler == null ? default : - new((byte* error) => - { - string? message = PtrToStringUTF8(error); - if (message is not null) errorHandler(message); - }); + OK = 0, + GenericError = 1, + NullArg = 2, + BadArg = 3, + OutOfMemory = 4, + // This value is added to the exit code in cases when Node.js API returns + // an error exit code. + ErrorExitCode = 512, + } - nint args_ptr = StringsToHGlobalUtf8(args, out int args_count); + [Flags] + public enum NodeEmbeddingPlatformFlags : int + { + None = 0, + // Enable stdio inheritance, which is disabled by default. + // This flag is also implied by + // node_embedding_platform_flags_no_stdio_initialization. + EnableStdioInheritance = 1 << 0, + // Disable reading the NODE_OPTIONS environment variable. + DisableNodeOptionsEnv = 1 << 1, + // Do not parse CLI options. + DisableCliOptions = 1 << 2, + // Do not initialize ICU. + NoIcu = 1 << 3, + // Do not modify stdio file descriptor or TTY state. + NoStdioInitialization = 1 << 4, + // Do not register Node.js-specific signal handlers + // and reset other signal handlers to default state. + NoDefaultSignalHandling = 1 << 5, + // Do not initialize OpenSSL config. + NoInitOpenSsl = 1 << 8, + // Do not initialize Node.js debugging based on environment variables. + NoParseGlobalDebugVariables = 1 << 9, + // Do not adjust OS resource limits for this process. + NoAdjustResourceLimits = 1 << 10, + // Do not map code segments into large pages for this process. + NoUseLargePages = 1 << 11, + // Skip printing output for --help, --version, --v8-options. + NoPrintHelpOrVersionOutput = 1 << 12, + // Initialize the process for predictable snapshot generation. + GeneratePredictableSnapshot = 1 << 14, + } - try - { - result = default; - fixed (napi_platform* result_ptr = &result) - { - if (napi_create_platform == null) - { - napi_create_platform = (delegate* unmanaged[Cdecl]< - int, nint, napi_error_message_handler, nint, napi_status>) - Import(nameof(napi_create_platform)); - } - - return napi_create_platform( - args_count, - args_ptr, - native_error_handler, - (nint)result_ptr); - } - } - finally - { - FreeStringsHGlobal(args_ptr, args_count); - } + // The flags for the Node.js runtime initialization. + // They match the internal EnvironmentFlags::Flags enum. + [Flags] + public enum NodeEmbeddingRuntimeFlags : int + { + None = 0, + // Use the default behavior for Node.js instances. + DefaultFlags = 1 << 0, + // Controls whether this Environment is allowed to affect per-process state + // (e.g. cwd, process title, uid, etc.). + // This is set when using default. + OwnsProcessState = 1 << 1, + // Set if this Environment instance is associated with the global inspector + // handling code (i.e. listening on SIGUSR1). + // This is set when using default. + OwnsInspector = 1 << 2, + // Set if Node.js should not run its own esm loader. This is needed by some + // embedders, because it's possible for the Node.js esm loader to conflict + // with another one in an embedder environment, e.g. Blink's in Chromium. + NoRegisterEsmLoader = 1 << 3, + // Set this flag to make Node.js track "raw" file descriptors, i.e. managed + // by fs.open() and fs.close(), and close them during + // node_embedding_delete_runtime(). + TrackUmanagedFDs = 1 << 4, + // Set this flag to force hiding console windows when spawning child + // processes. This is usually used when embedding Node.js in GUI programs on + // Windows. + HideConsoleWindows = 1 << 5, + // Set this flag to disable loading native addons via `process.dlopen`. + // This environment flag is especially important for worker threads + // so that a worker thread can't load a native addon even if `execArgv` + // is overwritten and `--no-addons` is not specified but was specified + // for this Environment instance. + NoNativeAddons = 1 << 6, + // Set this flag to disable searching modules from global paths like + // $HOME/.node_modules and $NODE_PATH. This is used by standalone apps that + // do not expect to have their behaviors changed because of globally + // installed modules. + NoGlobalSearchPaths = 1 << 7, + // Do not export browser globals like setTimeout, console, etc. + NoBrowserGlobals = 1 << 8, + // Controls whether or not the Environment should call V8Inspector::create(). + // This control is needed by embedders who may not want to initialize the V8 + // inspector in situations where one has already been created, + // e.g. Blink's in Chromium. + NoCreateInspector = 1 << 9, + // Controls whether or not the InspectorAgent for this Environment should + // call StartDebugSignalHandler. This control is needed by embedders who may + // not want to allow other processes to start the V8 inspector. + NoStartDebugSignalHandler = 1 << 10, + // Controls whether the InspectorAgent created for this Environment waits for + // Inspector frontend events during the Environment creation. It's used to + // call node::Stop(env) on a Worker thread that is waiting for the events. + NoWaitForInspectorFrontend = 1 << 11 + } + + //============================================================================================== + // Callbacks + //============================================================================================== + + public record struct node_embedding_data_release_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_data_release_callback( + delegate* unmanaged[Cdecl] handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NodeEmbeddingStatus Delegate(nint data); + + public node_embedding_data_release_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_platform_configure_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_platform_configure_callback(delegate* unmanaged[Cdecl]< + nint, node_embedding_platform_config, NodeEmbeddingStatus> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NodeEmbeddingStatus Delegate( + nint cb_data, + node_embedding_platform_config platform_config); + + public node_embedding_platform_configure_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_runtime_configure_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_runtime_configure_callback(delegate* unmanaged[Cdecl]< + nint, + node_embedding_platform, + node_embedding_runtime_config, + NodeEmbeddingStatus> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NodeEmbeddingStatus Delegate( + nint cb_data, + node_embedding_platform platform, + node_embedding_runtime_config runtime_config); + + public node_embedding_runtime_configure_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_runtime_preload_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_runtime_preload_callback(delegate* unmanaged[Cdecl]< + nint, node_embedding_runtime, napi_env, napi_value, napi_value, void> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require); + + public node_embedding_runtime_preload_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } - private delegate* unmanaged[Cdecl] - napi_destroy_platform; + public record struct node_embedding_runtime_loading_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_runtime_loading_callback(delegate* unmanaged[Cdecl]< + nint, + node_embedding_runtime, + napi_env, + napi_value, + napi_value, + napi_value, + napi_value> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate napi_value Delegate( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value process, + napi_value require, + napi_value run_cjs); - public override napi_status DestroyPlatform(napi_platform platform) + public node_embedding_runtime_loading_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_runtime_loaded_callback(nint Handle) { - return Import(ref napi_destroy_platform)(platform); +#if UNMANAGED_DELEGATES + public node_embedding_runtime_loaded_callback(delegate* unmanaged[Cdecl]< + nint, + node_embedding_runtime, + napi_env, + napi_value, + void> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + napi_value load_result); + + public node_embedding_runtime_loaded_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } } + public record struct node_embedding_module_initialize_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_module_initialize_callback(delegate* unmanaged[Cdecl]< + nint, + node_embedding_runtime, + napi_env, + nint, + napi_value, + napi_value> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate napi_value Delegate( + nint cb_data, + node_embedding_runtime runtime, + napi_env env, + nint module_name, + napi_value exports); + + public node_embedding_module_initialize_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_task_run_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_task_run_callback( + delegate* unmanaged[Cdecl] handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NodeEmbeddingStatus Delegate(nint cb_data); + + public node_embedding_task_run_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_task_post_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_task_post_callback(delegate* unmanaged[Cdecl]< + nint, + node_embedding_task_run_callback, + nint, + node_embedding_data_release_callback, + nint, + NodeEmbeddingStatus> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate NodeEmbeddingStatus Delegate( + nint cb_data, + node_embedding_task_run_callback run_task, + nint task_data, + node_embedding_data_release_callback release_task_data, + nint is_posted); + + public node_embedding_task_post_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + public record struct node_embedding_node_api_run_callback(nint Handle) + { +#if UNMANAGED_DELEGATES + public node_embedding_node_api_run_callback(delegate* unmanaged[Cdecl]< + nint, + napi_env, + void> handle) + : this((nint)handle) { } +#endif + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void Delegate( + nint cb_data, + napi_env env); + + public node_embedding_node_api_run_callback(Delegate callback) + : this(Marshal.GetFunctionPointerForDelegate(callback)) { } + } + + //============================================================================================== + // Functions + //============================================================================================== + + //---------------------------------------------------------------------------------------------- + // Error handling functions. + //---------------------------------------------------------------------------------------------- + + private delegate* unmanaged[Cdecl] node_embedding_last_error_message_get; + + private delegate* unmanaged[Cdecl] node_embedding_last_error_message_set; + + //---------------------------------------------------------------------------------------------- + // Node.js global platform functions. + //---------------------------------------------------------------------------------------------- + + private delegate* unmanaged[Cdecl]< + int, + int, + nint, + node_embedding_platform_configure_callback, + nint, + node_embedding_runtime_configure_callback, + nint, + NodeEmbeddingStatus> node_embedding_main_run; + + private delegate* unmanaged[Cdecl]< + int, + int, + nint, + node_embedding_platform_configure_callback, + nint, + nint, + NodeEmbeddingStatus> node_embedding_platform_create; + + private delegate* unmanaged[Cdecl] + node_embedding_platform_delete; + + private delegate* unmanaged[Cdecl]< + node_embedding_platform_config, + NodeEmbeddingPlatformFlags, + NodeEmbeddingStatus> node_embedding_platform_config_set_flags; + + private delegate* unmanaged[Cdecl]< + node_embedding_platform, + nint, + nint, + nint, + nint, + NodeEmbeddingStatus> node_embedding_platform_get_parsed_args; + + //---------------------------------------------------------------------------------------------- + // Node.js runtime functions. + //---------------------------------------------------------------------------------------------- + + private delegate* unmanaged[Cdecl]< + node_embedding_platform, + node_embedding_runtime_configure_callback, + nint, + NodeEmbeddingStatus> node_embedding_runtime_run; + + private delegate* unmanaged[Cdecl]< + node_embedding_platform, + node_embedding_runtime_configure_callback, + nint, + nint, + NodeEmbeddingStatus> node_embedding_runtime_create; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_delete; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_config_set_node_api_version; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + NodeEmbeddingRuntimeFlags, + NodeEmbeddingStatus> node_embedding_runtime_config_set_flags; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + int, + nint, + int, + nint, + NodeEmbeddingStatus> node_embedding_runtime_config_set_args; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + node_embedding_runtime_preload_callback, + nint, + node_embedding_data_release_callback, + NodeEmbeddingStatus> node_embedding_runtime_config_on_preload; + private delegate* unmanaged[Cdecl]< - napi_platform, napi_error_message_handler, nint, int, nint, napi_status> - napi_create_environment; + node_embedding_runtime_config, + node_embedding_runtime_loading_callback, + nint, + node_embedding_data_release_callback, + NodeEmbeddingStatus> node_embedding_runtime_config_on_loading; - public override napi_status CreateEnvironment( - napi_platform platform, - Action? errorHandler, - string? mainScript, - int apiVersion, - out napi_env result) + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + node_embedding_runtime_loaded_callback, + nint, + node_embedding_data_release_callback, + NodeEmbeddingStatus> node_embedding_runtime_config_on_loaded; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + nint, + node_embedding_module_initialize_callback, + nint, + node_embedding_data_release_callback, + int, + NodeEmbeddingStatus> node_embedding_runtime_config_add_module; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime, + nint, + node_embedding_data_release_callback, + NodeEmbeddingStatus> node_embedding_runtime_user_data_set; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime, + nint, + NodeEmbeddingStatus> node_embedding_runtime_user_data_get; + + //---------------------------------------------------------------------------------------------- + // Node.js runtime functions for the event loop. + //---------------------------------------------------------------------------------------------- + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime_config, + node_embedding_task_post_callback, + nint, + node_embedding_data_release_callback, + NodeEmbeddingStatus> node_embedding_runtime_config_set_task_runner; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_event_loop_run; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_event_loop_terminate; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_event_loop_run_once; + + private delegate* unmanaged[Cdecl] + node_embedding_runtime_event_loop_run_no_wait; + + //---------------------------------------------------------------------------------------------- + // Node.js runtime functions for the Node-API interop. + //---------------------------------------------------------------------------------------------- + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime, + node_embedding_node_api_run_callback, + nint, + NodeEmbeddingStatus> node_embedding_runtime_node_api_run; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime, + nint, + nint, + NodeEmbeddingStatus> node_embedding_runtime_node_api_scope_open; + + private delegate* unmanaged[Cdecl]< + node_embedding_runtime, + node_embedding_node_api_scope, + NodeEmbeddingStatus> node_embedding_runtime_node_api_scope_close; + + //============================================================================================== + // C# function wrappers + //============================================================================================== + + public override string EmbeddingGetLastErrorMessage() { - napi_error_message_handler native_error_handler = errorHandler == null ? default : - new((byte* error) => - { - string? message = PtrToStringUTF8(error); - if (message is not null) errorHandler(message); - }); + nint messagePtr = Import(ref node_embedding_last_error_message_get)(); + return PtrToStringUTF8((byte*)messagePtr) ?? string.Empty; + } - nint main_script_ptr = StringToHGlobalUtf8(mainScript); + public override void EmbeddingSetLastErrorMessage(ReadOnlySpan message) + { + using PooledBuffer messageBuffer = PooledBuffer.FromSpanUtf8(message); + fixed (byte* messagePtr = messageBuffer) + Import(ref node_embedding_last_error_message_set)((nint)messagePtr); + } - try + public override NodeEmbeddingStatus EmbeddingRunMain( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) + { + using Utf8StringArray utf8Args = new(args); + fixed (nint* argsPtr = utf8Args) + return Import(ref node_embedding_main_run)( + NodeEmbedding.EmbeddingApiVersion, + args.Length, + (nint)argsPtr, + configure_platform, + configure_platform_data, + configure_runtime, + configure_runtime_data); + } + + public override NodeEmbeddingStatus EmbeddingCreatePlatform( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + out node_embedding_platform result) + { + using Utf8StringArray utf8Args = new(args); + fixed (nint* argsPtr = utf8Args) + fixed (node_embedding_platform* result_ptr = &result) { - fixed (napi_env* result_ptr = &result) - { - return Import(ref napi_create_environment)( - platform, native_error_handler, main_script_ptr, apiVersion, (nint)result_ptr); - } + return Import(ref node_embedding_platform_create)( + NodeEmbedding.EmbeddingApiVersion, + args.Length, + (nint)argsPtr, + configure_platform, + configure_platform_data, + (nint)result_ptr); } - finally + } + + public override NodeEmbeddingStatus EmbeddingDeletePlatform(node_embedding_platform platform) + { + return Import(ref node_embedding_platform_delete)(platform); + } + + public override NodeEmbeddingStatus EmbeddingPlatformConfigSetFlags( + node_embedding_platform_config platform_config, NodeEmbeddingPlatformFlags flags) + { + return Import(ref node_embedding_platform_config_set_flags)(platform_config, flags); + } + + public override NodeEmbeddingStatus EmbeddingPlatformGetParsedArgs( + node_embedding_platform platform, + nint args_count, + nint args, + nint runtime_args_count, + nint runtime_args) + { + return Import(ref node_embedding_platform_get_parsed_args)( + platform, args_count, args, runtime_args_count, runtime_args); + } + + public override NodeEmbeddingStatus EmbeddingRunRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) + { + return Import(ref node_embedding_runtime_run)( + platform, configure_runtime, configure_runtime_data); + } + + public override NodeEmbeddingStatus EmbeddingCreateRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data, + out node_embedding_runtime result) + { + fixed (node_embedding_runtime* result_ptr = &result) { - if (main_script_ptr != default) Marshal.FreeHGlobal(main_script_ptr); + return Import(ref node_embedding_runtime_create)( + platform, configure_runtime, configure_runtime_data, (nint)result_ptr); } } - private delegate* unmanaged[Cdecl] - napi_destroy_environment; + public override NodeEmbeddingStatus + EmbeddingDeleteRuntime(node_embedding_runtime runtime) + { + return Import(ref node_embedding_runtime_delete)(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetNodeApiVersion( + node_embedding_runtime_config runtime_config, int node_api_version) + { + return Import(ref node_embedding_runtime_config_set_node_api_version)( + runtime_config, node_api_version); + } - public override napi_status DestroyEnvironment(napi_env env, out int exitCode) + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetFlags( + node_embedding_runtime_config runtime_config, NodeEmbeddingRuntimeFlags flags) { - fixed (int* exit_code_ptr = &exitCode) + return Import(ref node_embedding_runtime_config_set_flags)(runtime_config, flags); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetArgs( + node_embedding_runtime_config runtime_config, + ReadOnlySpan args, + ReadOnlySpan runtime_args) + { + using Utf8StringArray utf8Args = new(args); + using Utf8StringArray utf8RuntimeArgs = new(runtime_args); + fixed (nint* argsPtr = utf8Args) + fixed (nint* runtimeArgsPtr = utf8RuntimeArgs) { - return Import(ref napi_destroy_environment)(env, (nint)exit_code_ptr); + return Import(ref node_embedding_runtime_config_set_args)( + runtime_config, + args.Length, + (nint)argsPtr, + runtime_args.Length, + (nint)runtimeArgsPtr); } } - private delegate* unmanaged[Cdecl] - napi_run_environment; + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnPreload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback preload, + nint preload_data, + node_embedding_data_release_callback release_preload_data) + { + return Import(ref node_embedding_runtime_config_on_preload)( + runtime_config, preload, preload_data, release_preload_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback run_load, + nint load_data, + node_embedding_data_release_callback release_load_data) + { + return Import(ref node_embedding_runtime_config_on_loading)( + runtime_config, run_load, load_data, release_load_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_loaded, + nint handle_loaded_data, + node_embedding_data_release_callback release_handle_loaded_data) + { + return Import(ref node_embedding_runtime_config_on_loaded)( + runtime_config, handle_loaded, handle_loaded_data, release_handle_loaded_data); + } - public override napi_status RunEnvironment(napi_env env) + public override NodeEmbeddingStatus EmbeddingRuntimeConfigAddModule( + node_embedding_runtime_config runtime_config, + ReadOnlySpan module_name, + node_embedding_module_initialize_callback init_module, + nint init_module_data, + node_embedding_data_release_callback release_init_module_data, + int module_node_api_version) { - return Import(ref napi_run_environment)(env); + PooledBuffer moduleNameBuffer = PooledBuffer.FromSpanUtf8(module_name); + fixed (byte* moduleNamePtr = moduleNameBuffer) + return Import(ref node_embedding_runtime_config_add_module)( + runtime_config, + (nint)moduleNamePtr, + init_module, + init_module_data, + release_init_module_data, + module_node_api_version); } - private delegate* unmanaged[Cdecl] - napi_await_promise; + public override NodeEmbeddingStatus EmbeddingRuntimeSetUserData( + node_embedding_runtime runtime, + nint user_data, + node_embedding_data_release_callback release_user_data) + { + return Import(ref node_embedding_runtime_user_data_set)( + runtime, user_data, release_user_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeGetUserData( + node_embedding_runtime runtime, out nint user_data) + { + fixed (nint* userDataPtr = &user_data) + return Import(ref node_embedding_runtime_user_data_get)(runtime, (nint)userDataPtr); + } - public override napi_status AwaitPromise( - napi_env env, napi_value promise, out napi_value result) + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetTaskRunner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + nint post_task_data, + node_embedding_data_release_callback release_post_task_data) { - result = default; - fixed (napi_value* result_ptr = &result) + return Import(ref node_embedding_runtime_config_set_task_runner)( + runtime_config, post_task, post_task_data, release_post_task_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunEventLoop(node_embedding_runtime runtime) + { + return Import(ref node_embedding_runtime_event_loop_run)(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeTerminateEventLoop( + node_embedding_runtime runtime) + { + return Import(ref node_embedding_runtime_event_loop_terminate)(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunOnceEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) + { + fixed (bool* hasMoreWorkPtr = &hasMoreWork) + return Import(ref node_embedding_runtime_event_loop_run_once)( + runtime, (nint)hasMoreWorkPtr); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunNoWaitEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) + { + fixed (bool* hasMoreWorkPtr = &hasMoreWork) + return Import(ref node_embedding_runtime_event_loop_run_no_wait)( + runtime, (nint)hasMoreWorkPtr); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunNodeApi( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + nint run_node_api_data) + { + return Import(ref node_embedding_runtime_node_api_run)( + runtime, run_node_api, run_node_api_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeOpenNodeApiScope( + node_embedding_runtime runtime, + out node_embedding_node_api_scope node_api_scope, + out napi_env env) + { + fixed (node_embedding_node_api_scope* scopePtr = &node_api_scope) + fixed (napi_env* envPtr = &env) { - return Import(ref napi_await_promise)(env, promise, (nint)result_ptr); + return Import(ref node_embedding_runtime_node_api_scope_open)( + runtime, (nint)scopePtr, (nint)envPtr); } } + public override NodeEmbeddingStatus EmbeddingRuntimeCloseNodeApiScope( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope) + { + return Import(ref node_embedding_runtime_node_api_scope_close)(runtime, node_api_scope); + } + #pragma warning restore IDE1006 } diff --git a/src/NodeApi/Runtime/NodejsRuntime.JS.cs b/src/NodeApi/Runtime/NodejsRuntime.JS.cs index 062d8a96..21e3e2b4 100644 --- a/src/NodeApi/Runtime/NodejsRuntime.JS.cs +++ b/src/NodeApi/Runtime/NodejsRuntime.JS.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; - namespace Microsoft.JavaScript.NodeApi.Runtime; +using System; + // Imports Node.js native APIs defined in js_native_api.h public unsafe partial class NodejsRuntime { @@ -187,8 +187,8 @@ public override napi_status ThrowError(napi_env env, string? code, string msg) { using (PooledBuffer codeBuffer = PooledBuffer.FromStringUtf8(code)) using (PooledBuffer msgBuffer = PooledBuffer.FromStringUtf8(msg)) - fixed (byte* code_ptr = &codeBuffer.Pin()) - fixed (byte* msg_ptr = &codeBuffer.Pin()) + fixed (byte* code_ptr = codeBuffer) + fixed (byte* msg_ptr = msgBuffer) { return Import(ref napi_throw_error)( env, @@ -203,8 +203,8 @@ public override napi_status ThrowTypeError(napi_env env, string? code, string ms { using (PooledBuffer codeBuffer = PooledBuffer.FromStringUtf8(code)) using (PooledBuffer msgBuffer = PooledBuffer.FromStringUtf8(msg)) - fixed (byte* code_ptr = &codeBuffer.Pin()) - fixed (byte* msg_ptr = &codeBuffer.Pin()) + fixed (byte* code_ptr = codeBuffer) + fixed (byte* msg_ptr = msgBuffer) { return Import(ref napi_throw_type_error)( env, @@ -220,8 +220,8 @@ public override napi_status ThrowRangeError(napi_env env, string? code, string m { using (PooledBuffer codeBuffer = PooledBuffer.FromStringUtf8(code)) using (PooledBuffer msgBuffer = PooledBuffer.FromStringUtf8(msg)) - fixed (byte* code_ptr = &codeBuffer.Pin()) - fixed (byte* msg_ptr = &codeBuffer.Pin()) + fixed (byte* code_ptr = codeBuffer) + fixed (byte* msg_ptr = msgBuffer) { return Import(ref napi_throw_range_error)( @@ -238,8 +238,8 @@ public override napi_status ThrowSyntaxError(napi_env env, string? code, string { using (PooledBuffer codeBuffer = PooledBuffer.FromStringUtf8(code)) using (PooledBuffer msgBuffer = PooledBuffer.FromStringUtf8(msg)) - fixed (byte* code_ptr = &codeBuffer.Pin()) - fixed (byte* msg_ptr = &codeBuffer.Pin()) + fixed (byte* code_ptr = codeBuffer) + fixed (byte* msg_ptr = msgBuffer) { return Import(ref node_api_throw_syntax_error)( env, @@ -562,7 +562,7 @@ public override napi_status GetSymbolFor(napi_env env, string name, out napi_val { result = default; using (PooledBuffer nameBuffer = PooledBuffer.FromStringUtf8(name)) - fixed (byte* name_ptr = &nameBuffer.Pin()) + fixed (byte* name_ptr = nameBuffer) fixed (napi_value* result_ptr = &result) { return Import(ref node_api_symbol_for)( @@ -1047,7 +1047,7 @@ public override napi_status CreateFunction( out napi_value result) { using (PooledBuffer nameBuffer = PooledBuffer.FromStringUtf8(name)) - fixed (byte* name_ptr = &nameBuffer.Pin()) + fixed (byte* name_ptr = nameBuffer) fixed (napi_value* result_ptr = &result) { return Import(ref napi_create_function)( @@ -1591,7 +1591,7 @@ public override napi_status DefineClass( { result = default; using (PooledBuffer nameBuffer = PooledBuffer.FromStringUtf8(name)) - fixed (byte* name_ptr = &nameBuffer.Pin()) + fixed (byte* name_ptr = nameBuffer) fixed (napi_property_descriptor* properties_ptr = &properties.GetPinnableReference()) fixed (napi_value* result_ptr = &result) { diff --git a/src/NodeApi/Runtime/NodejsRuntime.Node.cs b/src/NodeApi/Runtime/NodejsRuntime.Node.cs index 07cb0c5a..406f57ac 100644 --- a/src/NodeApi/Runtime/NodejsRuntime.Node.cs +++ b/src/NodeApi/Runtime/NodejsRuntime.Node.cs @@ -1,11 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Diagnostics.CodeAnalysis; -namespace Microsoft.JavaScript.NodeApi.Runtime; - // Imports Node.js native APIs defined in node_api.h public unsafe partial class NodejsRuntime { @@ -467,8 +467,8 @@ public override void FatalError(string location, string message) { using (PooledBuffer locationBuffer = PooledBuffer.FromStringUtf8(location)) using (PooledBuffer messageBuffer = PooledBuffer.FromStringUtf8(message)) - fixed (byte* location_ptr = &locationBuffer.Pin()) - fixed (byte* message_ptr = &messageBuffer.Pin()) + fixed (byte* location_ptr = locationBuffer) + fixed (byte* message_ptr = messageBuffer) { if (napi_fatal_error == null) { diff --git a/src/NodeApi/Runtime/NodejsRuntime.Types.cs b/src/NodeApi/Runtime/NodejsRuntime.Types.cs index a0843e19..2a326165 100644 --- a/src/NodeApi/Runtime/NodejsRuntime.Types.cs +++ b/src/NodeApi/Runtime/NodejsRuntime.Types.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System.Runtime.InteropServices; - namespace Microsoft.JavaScript.NodeApi.Runtime; +using System.Runtime.InteropServices; + // Type definitions from Node.JS node_api.h and node_api_types.h public unsafe partial class NodejsRuntime { diff --git a/src/NodeApi/Runtime/PooledBuffer.cs b/src/NodeApi/Runtime/PooledBuffer.cs index a8c62e8a..d4a234db 100644 --- a/src/NodeApi/Runtime/PooledBuffer.cs +++ b/src/NodeApi/Runtime/PooledBuffer.cs @@ -1,9 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; +#if !(NETFRAMEWORK || NETSTANDARD) using System.Buffers; +#endif +using System.ComponentModel; using System.Text; -namespace Microsoft.JavaScript.NodeApi.Runtime; - internal struct PooledBuffer : IDisposable { public static readonly PooledBuffer Empty = new(); @@ -16,8 +22,8 @@ public PooledBuffer() #if NETFRAMEWORK || NETSTANDARD - // Avoid a dependency on System.Buffers with .NET Framwork. - // It is available as a nuget package, but might not be installed in the application. + // Avoid a dependency on System.Buffers with .NET Framework. + // It is available as a NuGet package, but might not be installed in the application. // In this case the buffer is not actually pooled. public PooledBuffer(int length) : this(length, length) { } @@ -61,7 +67,9 @@ public void Dispose() public readonly Span Span => Buffer; - public readonly ref byte Pin() => ref Span.GetPinnableReference(); + // To support PooledBuffer usage within a fixed statement. + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref byte GetPinnableReference() => ref Span.GetPinnableReference(); public static unsafe PooledBuffer FromStringUtf8(string? value) { @@ -76,4 +84,21 @@ public static unsafe PooledBuffer FromStringUtf8(string? value) return buffer; } + + public static unsafe PooledBuffer FromSpanUtf8(ReadOnlySpan value) + { + if (value.IsEmpty) + { + return Empty; + } + + fixed (char* valuePtr = value) + { + int byteLength = Encoding.UTF8.GetByteCount(valuePtr, value.Length); + PooledBuffer buffer = new(byteLength, byteLength + 1); + fixed (byte* bufferPtr = buffer.Span) + Encoding.UTF8.GetBytes(valuePtr, value.Length, bufferPtr, byteLength + 1); + return buffer; + } + } } diff --git a/src/NodeApi/Runtime/TracingJSRuntime.cs b/src/NodeApi/Runtime/TracingJSRuntime.cs index ad8ce702..27aa2534 100644 --- a/src/NodeApi/Runtime/TracingJSRuntime.cs +++ b/src/NodeApi/Runtime/TracingJSRuntime.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +namespace Microsoft.JavaScript.NodeApi.Runtime; + using System; using System.Collections.Generic; using System.Diagnostics; @@ -10,9 +12,6 @@ using System.Runtime.InteropServices; using System.Text; using Microsoft.JavaScript.NodeApi.Interop; - -namespace Microsoft.JavaScript.NodeApi.Runtime; - using static NodejsRuntime; /// @@ -61,7 +60,7 @@ public TracingJSRuntime(JSRuntime runtime, TraceSource trace) #region Formatting - private static string Format(napi_platform platform) => platform.Handle.ToString("X16"); + //private static string Format(napi_platform platform) => platform.Handle.ToString("X16"); private static string Format(napi_env env) => env.Handle.ToString("X16"); private static string Format(napi_handle_scope scope) => scope.Handle.ToString("X16"); private static string Format(napi_escapable_handle_scope scope) => scope.Handle.ToString("X16"); @@ -74,6 +73,9 @@ private static string Format(napi_threadsafe_function function) private static string Format(napi_async_cleanup_hook_handle hook) => hook.Handle.ToString("X16"); private static string Format(uv_loop_t loop) => loop.Handle.ToString("X16"); + private static string Format(node_embedding_node_api_scope node_api_scope) + => node_api_scope.Handle.ToString("X16"); + private string GetValueString(napi_env env, napi_value value) { @@ -2655,69 +2657,222 @@ public override napi_status GetNodeVersion(napi_env env, out napi_node_version r #region Embedding - public override napi_status CreatePlatform( - string[]? args, Action? errorHandler, out napi_platform result) + public override string EmbeddingGetLastErrorMessage() { - napi_platform resultValue = default; - napi_status status = TraceCall( - [ - $"[{string.Join(", ", args ?? [])}]", - ], - () => (_runtime.CreatePlatform(args, errorHandler, out resultValue), - Format(resultValue))); - result = resultValue; - return status; + return _runtime.EmbeddingGetLastErrorMessage(); } - public override napi_status DestroyPlatform(napi_platform platform) + public override void EmbeddingSetLastErrorMessage(ReadOnlySpan message) { - return TraceCall( - [Format(platform)], - () => _runtime.DestroyPlatform(platform)); + _runtime.EmbeddingSetLastErrorMessage(message); } - public override napi_status CreateEnvironment( - napi_platform platform, - Action? errorHandler, - string? mainScript, - int apiVersion, - out napi_env result) + public override NodeEmbeddingStatus EmbeddingRunMain( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) { - napi_env resultValue = default; - napi_status status = TraceCall( - [Format(platform), Format(mainScript)], - () => (_runtime.CreateEnvironment( - platform, errorHandler, mainScript, apiVersion, out resultValue), - Format(resultValue))); - result = resultValue; - return status; + return _runtime.EmbeddingRunMain( + args, + configure_platform, + configure_platform_data, + configure_runtime, + configure_runtime_data); } - public override napi_status DestroyEnvironment(napi_env env, out int exitCode) + public override NodeEmbeddingStatus EmbeddingCreatePlatform( + ReadOnlySpan args, + node_embedding_platform_configure_callback configure_platform, + nint configure_platform_data, + out node_embedding_platform result) { - int exitCodeValue = default; - napi_status status = TraceCall( - [Format(env)], - () => (_runtime.DestroyEnvironment(env, out exitCodeValue), - exitCodeValue.ToString())); - exitCode = exitCodeValue; - return status; + return _runtime.EmbeddingCreatePlatform( + args, configure_platform, configure_platform_data, out result); } - public override napi_status RunEnvironment(napi_env env) + public override NodeEmbeddingStatus EmbeddingDeletePlatform(node_embedding_platform platform) { - return TraceCall([Format(env)], () => _runtime.RunEnvironment(env)); + return _runtime.EmbeddingDeletePlatform(platform); } - public override napi_status AwaitPromise( - napi_env env, napi_value promise, out napi_value result) + public override NodeEmbeddingStatus EmbeddingPlatformConfigSetFlags( + node_embedding_platform_config platform_config, NodeEmbeddingPlatformFlags flags) { - napi_value resultValue = default; - napi_status status = TraceCall( - [Format(env, promise)], - () => (_runtime.AwaitPromise(env, promise, out resultValue), Format(env, resultValue))); - result = resultValue; - return status; + return _runtime.EmbeddingPlatformConfigSetFlags(platform_config, flags); + } + + public override NodeEmbeddingStatus EmbeddingPlatformGetParsedArgs( + node_embedding_platform platform, + nint args_count, + nint args, + nint runtime_args_count, + nint runtime_args) + { + return _runtime.EmbeddingPlatformGetParsedArgs( + platform, args_count, args, runtime_args_count, runtime_args); + } + + public override NodeEmbeddingStatus EmbeddingRunRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data) + { + return _runtime.EmbeddingRunRuntime(platform, configure_runtime, configure_runtime_data); + } + + public override NodeEmbeddingStatus EmbeddingCreateRuntime( + node_embedding_platform platform, + node_embedding_runtime_configure_callback configure_runtime, + nint configure_runtime_data, + out node_embedding_runtime result) + { + return _runtime.EmbeddingCreateRuntime( + platform, configure_runtime, configure_runtime_data, out result); + } + + public override NodeEmbeddingStatus + EmbeddingDeleteRuntime(node_embedding_runtime runtime) + { + return _runtime.EmbeddingDeleteRuntime(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetNodeApiVersion( + node_embedding_runtime_config runtime_config, int node_api_version) + { + return _runtime.EmbeddingRuntimeConfigSetNodeApiVersion(runtime_config, node_api_version); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetFlags( + node_embedding_runtime_config runtime_config, NodeEmbeddingRuntimeFlags flags) + { + return _runtime.EmbeddingRuntimeConfigSetFlags(runtime_config, flags); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetArgs( + node_embedding_runtime_config runtime_config, + ReadOnlySpan args, + ReadOnlySpan runtime_args) + { + return _runtime.EmbeddingRuntimeConfigSetArgs(runtime_config, args, runtime_args); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnPreload( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_preload_callback preload, + nint preload_data, + node_embedding_data_release_callback release_preload_data) + { + return _runtime.EmbeddingRuntimeConfigOnPreload( + runtime_config, preload, preload_data, release_preload_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoading( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loading_callback run_load, + nint load_data, + node_embedding_data_release_callback release_load_data) + { + return _runtime.EmbeddingRuntimeConfigOnLoading( + runtime_config, run_load, load_data, release_load_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigOnLoaded( + node_embedding_runtime_config runtime_config, + node_embedding_runtime_loaded_callback handle_loaded, + nint handle_loaded_data, + node_embedding_data_release_callback release_handle_loaded_data) + { + return _runtime.EmbeddingRuntimeConfigOnLoaded( + runtime_config, handle_loaded, handle_loaded_data, release_handle_loaded_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigAddModule( + node_embedding_runtime_config runtime_config, + ReadOnlySpan module_name, + node_embedding_module_initialize_callback init_module, + nint init_module_data, + node_embedding_data_release_callback release_init_module_data, + int module_node_api_version) + { + return _runtime.EmbeddingRuntimeConfigAddModule( + runtime_config, + module_name, + init_module, + init_module_data, + release_init_module_data, + module_node_api_version); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeSetUserData( + node_embedding_runtime runtime, + nint user_data, + node_embedding_data_release_callback release_user_data) + { + return _runtime.EmbeddingRuntimeSetUserData(runtime, user_data, release_user_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeGetUserData( + node_embedding_runtime runtime, out nint user_data) + { + return _runtime.EmbeddingRuntimeGetUserData(runtime, out user_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeConfigSetTaskRunner( + node_embedding_runtime_config runtime_config, + node_embedding_task_post_callback post_task, + nint post_task_data, + node_embedding_data_release_callback release_post_task_data) + { + return _runtime.EmbeddingRuntimeConfigSetTaskRunner( + runtime_config, post_task, post_task_data, release_post_task_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunEventLoop(node_embedding_runtime runtime) + { + return _runtime.EmbeddingRuntimeRunEventLoop(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeTerminateEventLoop( + node_embedding_runtime runtime) + { + return _runtime.EmbeddingRuntimeTerminateEventLoop(runtime); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunOnceEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) + { + return _runtime.EmbeddingRuntimeRunOnceEventLoop(runtime, out hasMoreWork); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunNoWaitEventLoop( + node_embedding_runtime runtime, out bool hasMoreWork) + { + return _runtime.EmbeddingRuntimeRunNoWaitEventLoop(runtime, out hasMoreWork); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeRunNodeApi( + node_embedding_runtime runtime, + node_embedding_node_api_run_callback run_node_api, + nint run_node_api_data) + { + return _runtime.EmbeddingRuntimeRunNodeApi(runtime, run_node_api, run_node_api_data); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeOpenNodeApiScope( + node_embedding_runtime runtime, + out node_embedding_node_api_scope node_api_scope, + out napi_env env) + { + return _runtime.EmbeddingRuntimeOpenNodeApiScope(runtime, out node_api_scope, out env); + } + + public override NodeEmbeddingStatus EmbeddingRuntimeCloseNodeApiScope( + node_embedding_runtime runtime, + node_embedding_node_api_scope node_api_scope) + { + return _runtime.EmbeddingRuntimeCloseNodeApiScope(runtime, node_api_scope); } #endregion diff --git a/src/NodeApi/Runtime/Utf8StringArray.cs b/src/NodeApi/Runtime/Utf8StringArray.cs new file mode 100644 index 00000000..f3b7b36c --- /dev/null +++ b/src/NodeApi/Runtime/Utf8StringArray.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.JavaScript.NodeApi.Runtime; + +using System; +#if !(NETFRAMEWORK || NETSTANDARD) +using System.Buffers; +#endif +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Text; + +internal struct Utf8StringArray : IDisposable +{ + // Use one contiguous buffer for all UTF-8 strings. + private readonly byte[] _stringBuffer; + private GCHandle _pinnedStringBuffer; + + public unsafe Utf8StringArray(ReadOnlySpan strings) + { + int byteLength = 0; + for (int i = 0; i < strings.Length; i++) + { + byteLength += Encoding.UTF8.GetByteCount(strings[i]) + 1; + } + +#if NETFRAMEWORK || NETSTANDARD + // Avoid a dependency on System.Buffers with .NET Framework. + // It is available as a Nuget package, but might not be installed in the application. + // In this case the buffer is not actually pooled. + + Utf8Strings = new nint[strings.Length]; + _stringBuffer = new byte[byteLength]; +#else + Utf8Strings = ArrayPool.Shared.Rent(strings.Length); + _stringBuffer = ArrayPool.Shared.Rent(byteLength); +#endif + + // Pin the string buffer + _pinnedStringBuffer = GCHandle.Alloc(_stringBuffer, GCHandleType.Pinned); + nint stringBufferPtr = _pinnedStringBuffer.AddrOfPinnedObject(); + int offset = 0; + for (int i = 0; i < strings.Length; i++) + { + fixed (char* src = strings[i]) + { + Utf8Strings[i] = stringBufferPtr + offset; + offset += Encoding.UTF8.GetBytes( + src, strings[i].Length, (byte*)(stringBufferPtr + offset), byteLength - offset) + + 1; // +1 for the string Null-terminator. + } + } + } + + public void Dispose() + { + if (!Disposed) + { + Disposed = true; + _pinnedStringBuffer.Free(); + +#if !(NETFRAMEWORK || NETSTANDARD) + ArrayPool.Shared.Return(Utf8Strings); + ArrayPool.Shared.Return(_stringBuffer); +#endif + } + } + + public readonly nint[] Utf8Strings { get; } + + public bool Disposed { get; private set; } + + // To support Utf8StringArray usage within a fixed statement. + [EditorBrowsable(EditorBrowsableState.Never)] + public readonly ref nint GetPinnableReference() + { + if (Disposed) throw new ObjectDisposedException(nameof(Utf8StringArray)); + Span span = Utf8Strings; + return ref span.GetPinnableReference(); + } + + public static unsafe string[] ToStringArray(nint utf8StringArray, int size) + { + var utf8Strings = new ReadOnlySpan((void*)utf8StringArray, size); + string[] strings = new string[size]; + for (int i = 0; i < utf8Strings.Length; i++) + { + strings[i] = PtrToStringUTF8((byte*)utf8Strings[i]); + } + return strings; + } + + public static unsafe string PtrToStringUTF8(byte* ptr) + { +#if NETFRAMEWORK || NETSTANDARD + if (ptr == null) throw new ArgumentNullException(nameof(ptr)); + int length = 0; + while (ptr[length] != 0) length++; + return Encoding.UTF8.GetString(ptr, length); +#else + return Marshal.PtrToStringUTF8((nint)ptr) ?? throw new ArgumentNullException(nameof(ptr)); +#endif + } +} diff --git a/test/GCTests.cs b/test/GCTests.cs index d27052c3..3d1ddedc 100644 --- a/test/GCTests.cs +++ b/test/GCTests.cs @@ -12,17 +12,17 @@ public class GCTests { private static string LibnodePath { get; } = GetLibnodePath(); - [SkippableFact] + [Fact] public void GCHandles() { - Skip.If( - NodejsEmbeddingTests.NodejsPlatform == null, - "Node shared library not found at " + LibnodePath); - using NodejsEnvironment nodejs = NodejsEmbeddingTests.NodejsPlatform.CreateEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = + NodejsEmbeddingTests.CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { - Assert.Equal(0, JSRuntimeContext.Current.GCHandleCount); + // 3 GC handles are created in the NodeEmbeddingThreadRuntime constructor + // to define the 'require', 'resolve', and ' import' functions. + Assert.Equal(3, JSRuntimeContext.Current.GCHandleCount); JSClassBuilder classBuilder = new(nameof(DotnetClass), () => new DotnetClass()); @@ -43,7 +43,7 @@ public void GCHandles() // - JSPropertyDescriptor: DotnetClass.property // - JSPropertyDescriptor: DotnetClass.method // - JSPropertyDescriptor: DotnetClass.toString - Assert.Equal(5, JSRuntimeContext.Current.GCHandleCount); + Assert.Equal(3 + 5, JSRuntimeContext.Current.GCHandleCount); using JSValueScope innerScope = new(JSValueScopeType.Callback); jsCreateInstanceFunction.CallAsStatic(dotnetClass); @@ -51,7 +51,7 @@ public void GCHandles() // Two more handles should have been allocated by the JS create-instance function call. // - One for the 'external' type value passed to the constructor. // - One for the JS object wrapper. - Assert.Equal(7, JSRuntimeContext.Current.GCHandleCount); + Assert.Equal(3 + 5 + 2, JSRuntimeContext.Current.GCHandleCount); }); nodejs.GC(); @@ -59,17 +59,15 @@ public void GCHandles() nodejs.Run(() => { // After GC, the handle count should have reverted back to the original set. - Assert.Equal(5, JSRuntimeContext.Current.GCHandleCount); + Assert.Equal(3 + 5, JSRuntimeContext.Current.GCHandleCount); }); } - [SkippableFact] + [Fact] public void GCObjects() { - Skip.If( - NodejsEmbeddingTests.NodejsPlatform == null, - "Node shared library not found at " + LibnodePath); - using NodejsEnvironment nodejs = NodejsEmbeddingTests.NodejsPlatform.CreateEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = + NodejsEmbeddingTests.CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { @@ -86,7 +84,7 @@ public void GCObjects() "function jsCreateInstanceFunction(Class) { new Class() }; " + "jsCreateInstanceFunction"); - Assert.Equal(5, JSRuntimeContext.Current.GCHandleCount); + Assert.Equal(8, JSRuntimeContext.Current.GCHandleCount); using (JSValueScope innerScope = new(JSValueScopeType.Callback)) { diff --git a/test/JSReferenceTests.cs b/test/JSReferenceTests.cs index 1283cbec..6e487afc 100644 --- a/test/JSReferenceTests.cs +++ b/test/JSReferenceTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Threading.Tasks; using Xunit; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; @@ -52,7 +51,7 @@ public void GetReferenceFromDifferentThread() JSReference reference = new(value); // Run in a new thread which will not have any current scope. - Task.Run(() => + TestUtils.RunInThread(() => { Assert.Throws(() => reference.GetValue()); }).Wait(); @@ -67,7 +66,7 @@ public void GetReferenceFromDifferentRootScope() JSReference reference = new(value); // Run in a new thread and establish another root scope there. - Task.Run(() => + TestUtils.RunInThread(() => { using JSValueScope rootScope2 = TestScope(JSValueScopeType.Root); Assert.Throws(() => reference.GetValue()); diff --git a/test/JSValueScopeTests.cs b/test/JSValueScopeTests.cs index 2057e5ee..201d2608 100644 --- a/test/JSValueScopeTests.cs +++ b/test/JSValueScopeTests.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Threading.Tasks; using Xunit; using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime; @@ -325,7 +324,7 @@ public void CreateValueFromDifferentThread() using JSValueScope rootScope = TestScope(JSValueScopeType.Root); // Run in a new thread which will not have any current scope. - Task.Run(() => + TestUtils.RunInThread(() => { Assert.Throws(() => JSValueScope.Current); JSInvalidThreadAccessException ex = Assert.Throws( @@ -342,7 +341,7 @@ public void AccessValueFromDifferentThread() JSValue objectValue = JSValue.CreateObject(); // Run in a new thread which will not have any current scope. - Task.Run(() => + TestUtils.RunInThread(() => { Assert.Throws(() => JSValueScope.Current); JSInvalidThreadAccessException ex = Assert.Throws( @@ -359,7 +358,7 @@ public void AccessValueFromDifferentRootScope() JSValue objectValue = JSValue.CreateObject(); // Run in a new thread and establish another root scope there. - Task.Run(() => + TestUtils.RunInThread(() => { using JSValueScope rootScope2 = TestScope(JSValueScopeType.Root); Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType); diff --git a/test/NodeApi.Test.csproj b/test/NodeApi.Test.csproj index 8aa0a447..698ad30e 100644 --- a/test/NodeApi.Test.csproj +++ b/test/NodeApi.Test.csproj @@ -20,10 +20,10 @@ + - diff --git a/test/NodejsEmbeddingTests.cs b/test/NodejsEmbeddingTests.cs index f1c452ce..660ac683 100644 --- a/test/NodejsEmbeddingTests.cs +++ b/test/NodejsEmbeddingTests.cs @@ -19,28 +19,49 @@ namespace Microsoft.JavaScript.NodeApi.Test; public class NodejsEmbeddingTests { + private static string MainScript { get; } = + "globalThis.require = require('module').createRequire(process.execPath);\n"; + private static string LibnodePath { get; } = GetLibnodePath(); // The Node.js platform may only be initialized once per process. - internal static NodejsPlatform? NodejsPlatform { get; } = - File.Exists(LibnodePath) ? new(LibnodePath, args: new[] { "node", "--expose-gc" }) : null; + internal static NodeEmbeddingPlatform NodejsPlatform { get; } = + new(LibnodePath, new NodeEmbeddingPlatformSettings + { + Args = new[] { "node", "--expose-gc" } + }); - internal static NodejsEnvironment CreateNodejsEnvironment() + internal static NodeEmbeddingThreadRuntime CreateNodeEmbeddingThreadRuntime() { - Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath); - return NodejsPlatform.CreateEnvironment(Path.Combine(GetRepoRootDirectory(), "test")); + return NodejsPlatform.CreateThreadRuntime( + Path.Combine(GetRepoRootDirectory(), "test"), + new NodeEmbeddingRuntimeSettings { MainScript = MainScript }); } internal static void RunInNodejsEnvironment(Action action) { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.SynchronizationContext.Run(action); } - [SkippableFact] + [Fact] + public void LoadMainScriptNoThread() + { + using NodeEmbeddingRuntime runtime = NodeEmbeddingRuntime.Create(NodejsPlatform, + new NodeEmbeddingRuntimeSettings { MainScript = MainScript }); + runtime.RunEventLoop(); + } + + [Fact] + public void LoadMainScriptWithThread() + { + using NodeEmbeddingThreadRuntime runtime = CreateNodeEmbeddingThreadRuntime(); + } + + [Fact] public void StartEnvironment() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { @@ -52,24 +73,25 @@ public void StartEnvironment() Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public void RestartEnvironment() { - // Create and destory a Node.js environment twice, using the same platform instance. + // Create and destroy a Node.js environment twice, using the same platform instance. StartEnvironment(); StartEnvironment(); } public interface IConsole { void Log(string message); } - [SkippableFact] + [Fact] public void CallFunction() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.SynchronizationContext.Run(() => { - JSFunction func = (JSFunction)JSValue.RunScript("function jsFunction() { }; jsFunction"); + JSFunction func = (JSFunction)JSValue.RunScript( + "function jsFunction() { }; jsFunction"); func.CallAsStatic(); }); @@ -77,10 +99,10 @@ public void CallFunction() Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public void ImportBuiltinModule() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { @@ -97,10 +119,10 @@ public void ImportBuiltinModule() Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public void ImportCommonJSModule() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { @@ -114,10 +136,10 @@ public void ImportCommonJSModule() Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public void ImportCommonJSPackage() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { @@ -131,10 +153,10 @@ public void ImportCommonJSPackage() Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public async Task ImportESModule() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); await nodejs.RunAsync(async () => { @@ -149,10 +171,10 @@ await nodejs.RunAsync(async () => Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public async Task ImportESPackage() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); await nodejs.RunAsync(async () => { @@ -181,10 +203,10 @@ await nodejs.RunAsync(async () => Assert.Equal(0, nodejs.ExitCode); } - [SkippableFact] + [Fact] public void UnhandledRejection() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); string? errorMessage = null; nodejs.UnhandledPromiseRejection += (_, e) => @@ -203,10 +225,10 @@ public void UnhandledRejection() Assert.Equal("test", errorMessage); } - [SkippableFact] + [Fact] public void ErrorPropagation() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); JSException exception = Assert.Throws(() => { @@ -239,7 +261,7 @@ public void ErrorPropagation() (line) => line.StartsWith($"at {typeof(NodejsEmbeddingTests).FullName}.")); } - [SkippableFact] + [Fact] public async Task WorkerIsMainThread() { await TestWorker( @@ -248,15 +270,15 @@ await TestWorker( Assert.True(NodeWorker.IsMainThread); return new NodeWorker.Options { Eval = true }; }, - workerScript: @" -const assert = require('node:assert'); -const { isMainThread } = require('node:worker_threads'); -assert(!isMainThread); -", + workerScript: """ + const assert = require('node:assert'); + const { isMainThread } = require('node:worker_threads'); + assert(!isMainThread); + """, mainRun: (worker) => Task.CompletedTask); } - [SkippableFact] + [Fact] public async Task WorkerArgs() { await TestWorker( @@ -271,18 +293,18 @@ await TestWorker( WorkerData = true, }; }, - workerScript: @" -const assert = require('node:assert'); -const process = require('node:process'); -const { workerData } = require('node:worker_threads'); -assert.deepStrictEqual(process.argv.slice(2), ['test1', 'test2']); -assert.strictEqual(typeof workerData, 'boolean'); -assert(workerData); -", + workerScript: """ + const assert = require('node:assert'); + const process = require('node:process'); + const { workerData } = require('node:worker_threads'); + assert.deepStrictEqual(process.argv.slice(2), ['test1', 'test2']); + assert.strictEqual(typeof workerData, 'boolean'); + assert(workerData); + """, mainRun: (worker) => Task.CompletedTask); } - [SkippableFact] + [Fact] public async Task WorkerEnv() { await TestWorker( @@ -294,15 +316,15 @@ await TestWorker( Eval = true, }; }, - workerScript: @" -const assert = require('node:assert'); -const { getEnvironmentData } = require('node:worker_threads'); -assert.strictEqual(getEnvironmentData('test'), true); -", + workerScript: """ + const assert = require('node:assert'); + const { getEnvironmentData } = require('node:worker_threads'); + assert.strictEqual(getEnvironmentData('test'), true); + """, mainRun: (worker) => Task.CompletedTask); } - [SkippableFact] + [Fact] public async Task WorkerMessages() { await TestWorker( @@ -310,10 +332,10 @@ await TestWorker( { return new NodeWorker.Options { Eval = true }; }, - workerScript: @" -const { parentPort } = require('node:worker_threads'); -parentPort.on('message', (msg) => parentPort.postMessage(msg)); // echo -", + workerScript: """ + const { parentPort } = require('node:worker_threads'); + parentPort.on('message', (msg) => parentPort.postMessage(msg)); // echo + """, mainRun: async (worker) => { TaskCompletionSource echoCompletion = new(); @@ -328,7 +350,7 @@ await TestWorker( }); } - [SkippableFact] + [Fact] public async Task WorkerStdinStdout() { await TestWorker( @@ -362,7 +384,7 @@ private static async Task TestWorker( string workerScript, Func mainRun) { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); await nodejs.RunAsync(async () => { NodeWorker.Options workerOptions = mainPrepare.Invoke(); @@ -391,10 +413,10 @@ await nodejs.RunAsync(async () => /// Tests the functionality of dynamically exporting and marshalling a class type from .NET /// to JS (as opposed to relying on [JSExport] (compile-time code-generation) for marshalling. /// - [SkippableFact] + [Fact] public void MarshalClass() { - using NodejsEnvironment nodejs = CreateNodejsEnvironment(); + using NodeEmbeddingThreadRuntime nodejs = CreateNodeEmbeddingThreadRuntime(); nodejs.Run(() => { diff --git a/test/TestCases/napi-dotnet/ThreadSafety.cs b/test/TestCases/napi-dotnet/ThreadSafety.cs index 48276c49..ce91c628 100644 --- a/test/TestCases/napi-dotnet/ThreadSafety.cs +++ b/test/TestCases/napi-dotnet/ThreadSafety.cs @@ -1,8 +1,9 @@ -// Copyright (c) Microsoft Corporation. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Microsoft.JavaScript.NodeApi.TestCases; @@ -36,7 +37,7 @@ private static void ValidateNotOnJSThread() public static async Task CallDelegateFromOtherThread(Action action) { - await Task.Run(() => + await RunInThread(() => { ValidateNotOnJSThread(); @@ -48,7 +49,7 @@ public static async Task CallInterfaceMethodFromOtherThread( ISmpleInterface interfaceObj, string value) { - return await Task.Run(() => + return await RunInThread(() => { ValidateNotOnJSThread(); @@ -59,7 +60,7 @@ public static async Task CallInterfaceMethodFromOtherThread( public static async Task EnumerateCollectionFromOtherThread( IReadOnlyCollection collection) { - return await Task.Run(() => + return await RunInThread(() => { ValidateNotOnJSThread(); @@ -76,7 +77,7 @@ public static async Task EnumerateCollectionFromOtherThread( public static async Task EnumerateDictionaryFromOtherThread( IReadOnlyDictionary dictionary) { - return await Task.Run(() => + return await RunInThread(() => { ValidateNotOnJSThread(); @@ -93,11 +94,52 @@ public static async Task EnumerateDictionaryFromOtherThread( public static async Task ModifyDictionaryFromOtherThread( IDictionary dictionary, string keyToRemove) { - return await Task.Run(() => + return await RunInThread(() => { ValidateNotOnJSThread(); return dictionary.Remove(keyToRemove); }); } + + private static Task RunInThread(Action action) + { + TaskCompletionSource threadCompletion = new TaskCompletionSource(); + + Thread thread = new Thread(() => + { + try + { + action(); + threadCompletion.TrySetResult(true); + } + catch (Exception e) + { + threadCompletion.TrySetException(e); + } + }); + thread.Start(); + + return threadCompletion.Task; + } + + private static Task RunInThread(Func func) + { + TaskCompletionSource threadCompletion = new TaskCompletionSource(); + + Thread thread = new Thread(() => + { + try + { + threadCompletion.TrySetResult(func()); + } + catch (Exception e) + { + threadCompletion.TrySetException(e); + } + }); + thread.Start(); + + return threadCompletion.Task; + } } diff --git a/test/TestUtils.cs b/test/TestUtils.cs index 98c44c36..81704549 100644 --- a/test/TestUtils.cs +++ b/test/TestUtils.cs @@ -7,21 +7,25 @@ using System.Runtime.InteropServices; using System.Text; using System.Threading; +using System.Threading.Tasks; namespace Microsoft.JavaScript.NodeApi.Test; public static class TestUtils { - public static string GetRepoRootDirectory() + public static string GetAssemblyLocation() { #if NETFRAMEWORK - string assemblyLocation = new Uri(typeof(TestUtils).Assembly.CodeBase).LocalPath; + return new Uri(typeof(TestUtils).Assembly.CodeBase).LocalPath; #else -#pragma warning disable IL3000 // Assembly.Location returns an empty string for assemblies embedded in a single-file app - string assemblyLocation = typeof(TestUtils).Assembly.Location!; -#pragma warning restore IL3000 + // Assembly.Location returns an empty string for assemblies embedded in a single-file app + return typeof(TestUtils).Assembly.Location; #endif + } + public static string GetRepoRootDirectory() + { + string assemblyLocation = GetAssemblyLocation(); string? solutionDir = string.IsNullOrEmpty(assemblyLocation) ? Environment.CurrentDirectory : Path.GetDirectoryName(assemblyLocation); @@ -73,11 +77,10 @@ public static string GetSharedLibraryExtension() else return ".so"; } - public static string GetLibnodePath() => Path.Combine( - GetRepoRootDirectory(), - "bin", - GetCurrentPlatformRuntimeIdentifier(), - "libnode" + GetSharedLibraryExtension()); + public static string GetLibnodePath() => + Path.Combine( + Path.GetDirectoryName(GetAssemblyLocation()) ?? string.Empty, + "libnode" + GetSharedLibraryExtension()); public static string? LogOutput( Process process, @@ -143,4 +146,25 @@ public static void CopyIfNewer(string sourceFilePath, string targetFilePath) File.Copy(sourceFilePath, targetFilePath, overwrite: true); } } + + public static Task RunInThread(Action action) + { + TaskCompletionSource threadCompletion = new(); + + Thread thread = new(() => + { + try + { + action(); + threadCompletion.TrySetResult(true); + } + catch (Exception e) + { + threadCompletion.TrySetException(e); + } + }); + thread.Start(); + + return threadCompletion.Task; + } }