diff --git a/src/NodeApi.DotNetHost/ManagedHost.cs b/src/NodeApi.DotNetHost/ManagedHost.cs
index 12e4e8ab..87c77f74 100644
--- a/src/NodeApi.DotNetHost/ManagedHost.cs
+++ b/src/NodeApi.DotNetHost/ManagedHost.cs
@@ -393,14 +393,15 @@ public JSValue LoadModule(JSCallbackArgs args)
}
}
+ JSValueScope scope = JSValueScope.Current;
JSValue exports = JSValue.CreateObject();
var result = (napi_value?)initializeMethod.Invoke(
- null, new object[] { (napi_env)JSValueScope.Current, (napi_value)exports });
+ null, new object[] { (napi_env)scope, (napi_value)exports });
if (result != null && result.Value != default)
{
- exports = new JSValue(result.Value);
+ exports = new JSValue(result.Value, scope);
}
if (exports.IsObject())
diff --git a/src/NodeApi.Generator/ModuleGenerator.cs b/src/NodeApi.Generator/ModuleGenerator.cs
index 3e801db5..a5097d42 100644
--- a/src/NodeApi.Generator/ModuleGenerator.cs
+++ b/src/NodeApi.Generator/ModuleGenerator.cs
@@ -282,6 +282,10 @@ private SourceBuilder GenerateModuleInitializer(
s += $"public static class {ModuleInitializerClassName}";
s += "{";
+ // The module scope is not disposed after a successful initialization. It becomes
+ // the parent of callback scopes, allowing the JS runtime instance to be inherited.
+ s += "private static JSValueScope _moduleScope;";
+
// The unmanaged entrypoint is used only when the AOT-compiled module is loaded.
s += "#if !NETFRAMEWORK";
s += $"[UnmanagedCallersOnly(EntryPoint = \"{ModuleRegisterFunctionName}\")]";
@@ -293,11 +297,11 @@ private SourceBuilder GenerateModuleInitializer(
// The main initialization entrypoint is called by the `ManagedHost`, and by the unmanaged entrypoint.
s += $"public static napi_value {ModuleInitializeMethodName}(napi_env env, napi_value exports)";
s += "{";
- s += "var scope = new JSValueScope(JSValueScopeType.Module, env);";
+ s += "_moduleScope = new JSValueScope(JSValueScopeType.Module, env, runtime: default);";
s += "try";
s += "{";
- s += "JSRuntimeContext context = scope.RuntimeContext;";
- s += "JSValue exportsValue = new(exports, scope);";
+ s += "JSRuntimeContext context = _moduleScope.RuntimeContext;";
+ s += "JSValue exportsValue = new(exports, _moduleScope);";
s++;
if (moduleInitializer is IMethodSymbol moduleInitializerMethod)
@@ -327,15 +331,12 @@ private SourceBuilder GenerateModuleInitializer(
s += "return (napi_value)exportsValue;";
}
- // The module scope is not disposed before a successful return. It becomes the parent
- // of callback scopes, allowing the JS runtime instance to be inherited.
-
s += "}";
s += "catch (System.Exception ex)";
s += "{";
s += "System.Console.Error.WriteLine($\"Failed to export module: {ex}\");";
s += "JSError.ThrowError(ex);";
- s += "scope.Dispose();";
+ s += "_moduleScope.Dispose();";
s += "return exports;";
s += "}";
s += "}";
diff --git a/src/NodeApi/DotNetHost/NativeHost.cs b/src/NodeApi/DotNetHost/NativeHost.cs
index e60a86d3..72b1f67c 100644
--- a/src/NodeApi/DotNetHost/NativeHost.cs
+++ b/src/NodeApi/DotNetHost/NativeHost.cs
@@ -23,10 +23,12 @@ internal unsafe partial class NativeHost : IDisposable
private static readonly string s_managedHostTypeName =
typeof(NativeHost).Namespace + ".ManagedHost";
+ private static JSRuntime? s_jsRuntime;
private string? _targetFramework;
private string? _managedHostPath;
private ICLRRuntimeHost* _runtimeHost;
private hostfxr_handle _hostContextHandle;
+ private readonly JSValueScope _hostScope;
private JSReference? _exports;
public static bool IsTracingEnabled { get; } =
@@ -48,15 +50,18 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
{
Trace($"> NativeHost.InitializeModule({env.Handle:X8}, {exports.Handle:X8})");
- JSRuntime runtime = new NodejsRuntime();
- using JSValueScope scope = new(JSValueScopeType.NoContext, env, runtime);
+ s_jsRuntime ??= new NodejsRuntime();
+
+ // The native host JSValueScope is not disposed after a successful initialization. It
+ // becomes the parent of callback scopes, allowing the JS runtime instance to be inherited.
+ JSValueScope hostScope = new(JSValueScopeType.NoContext, env, s_jsRuntime);
try
{
- NativeHost host = new();
+ NativeHost host = new(hostScope);
// Do not use JSModuleBuilder here because it relies on having a current context.
// But the context will be set by the managed host.
- new JSValue(exports, scope).DefineProperties(
+ new JSValue(exports, hostScope).DefineProperties(
// The package index.js will invoke the initialize method with the path to
// the managed host assembly.
JSPropertyDescriptor.Function("initialize", host.InitializeManagedHost));
@@ -65,7 +70,8 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
{
string message = $"Failed to load CLR native host module: {ex}";
Trace(message);
- runtime.Throw(env, (napi_value)JSValue.CreateError(null, (JSValue)message));
+ s_jsRuntime.Throw(env, (napi_value)JSValue.CreateError(null, (JSValue)message));
+ hostScope.Dispose();
}
Trace("< NativeHost.InitializeModule()");
@@ -73,8 +79,9 @@ public static napi_value InitializeModule(napi_env env, napi_value exports)
return exports;
}
- public NativeHost()
+ private NativeHost(JSValueScope hostScope)
{
+ _hostScope = hostScope;
}
///
diff --git a/src/NodeApi/Interop/JSRuntimeContext.cs b/src/NodeApi/Interop/JSRuntimeContext.cs
index 5c9239bb..4ae98e7a 100644
--- a/src/NodeApi/Interop/JSRuntimeContext.cs
+++ b/src/NodeApi/Interop/JSRuntimeContext.cs
@@ -8,6 +8,7 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
+using Microsoft.JavaScript.NodeApi.Runtime;
using static Microsoft.JavaScript.NodeApi.Interop.JSCollectionProxies;
using static Microsoft.JavaScript.NodeApi.JSNativeApi;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
@@ -103,14 +104,31 @@ public sealed class JSRuntimeContext : IDisposable
private readonly ConcurrentDictionary _collectionProxyHandlerMap = new();
- public bool IsDisposed { get; private set; }
+ internal napi_env EnvironmentHandle
+ {
+ get
+ {
+ if (IsDisposed)
+ {
+ throw new ObjectDisposedException(nameof(JSRuntimeContext));
+ }
+
+ return _env;
+ }
+ }
+
+ public static explicit operator napi_env(JSRuntimeContext context)
+ {
+ if (context is null) throw new ArgumentNullException(nameof(context));
+ return context.EnvironmentHandle;
+ }
- public static explicit operator napi_env(JSRuntimeContext context) => context?._env ??
- throw new ArgumentNullException(nameof(context));
public static explicit operator JSRuntimeContext(napi_env env)
=> GetInstanceData(env) as JSRuntimeContext
?? throw new InvalidCastException("Context is not found in napi_env instance data.");
+ public bool IsDisposed { get; private set; }
+
///
/// Gets the current runtime context.
///
@@ -118,13 +136,21 @@ public static explicit operator JSRuntimeContext(napi_env env)
/// thread.
public static JSRuntimeContext Current => JSValueScope.Current.RuntimeContext;
+ public JSRuntime Runtime { get; }
+
public JSSynchronizationContext SynchronizationContext { get; }
- public JSRuntimeContext(napi_env env)
+ internal JSRuntimeContext(
+ napi_env env,
+ JSRuntime runtime,
+ JSSynchronizationContext? synchronizationContext = null)
{
+ if (env.IsNull) throw new ArgumentNullException(nameof(env));
+
_env = env;
+ Runtime = runtime;
SetInstanceData(env, this);
- SynchronizationContext = JSSynchronizationContext.Create();
+ SynchronizationContext = synchronizationContext ?? JSSynchronizationContext.Create();
}
///
@@ -692,22 +718,4 @@ internal void FreeGCHandle(GCHandle handle)
handle.Free();
}
-
- ///
- /// Frees a GC handle previously allocated via
- /// and tracked on the runtime context obtained from environment instance data.
- ///
- /// The handle was not previously allocated
- /// by , or was already freed.
- internal static void FreeGCHandle(GCHandle handle, napi_env env)
- {
- if (GetInstanceData(env) is JSRuntimeContext runtimeContext)
- {
- runtimeContext.FreeGCHandle(handle);
- }
- else
- {
- handle.Free();
- }
- }
}
diff --git a/src/NodeApi/Interop/JSSynchronizationContext.cs b/src/NodeApi/Interop/JSSynchronizationContext.cs
index d42b432b..9c8f5551 100644
--- a/src/NodeApi/Interop/JSSynchronizationContext.cs
+++ b/src/NodeApi/Interop/JSSynchronizationContext.cs
@@ -7,6 +7,25 @@
namespace Microsoft.JavaScript.NodeApi.Interop;
+///
+/// Manages the synchronization context for a JavaScript environment, allowing callbacks and
+/// asynchronous continuations to be invoked on the JavaScript thread that runs the environment.
+///
+///
+/// All JavaScript values are bound to the thread that runs the JS environment and can only be
+/// accessed from the same thread. Attempts to access a JavaScript value from a different thread
+/// will throw .
+///
+/// Use of with continueOnCapturedContext:false
+/// can prevent execution from returning to the JS thread, though it isn't necessarily a problem
+/// as long as there is a top-level continuation that uses continueOnCapturedContext:true
+/// (the default) to return to the JS thread.
+///
+/// Code that makes explicit use of .NET threads or thread pools may need to capture the
+/// context (before switching off the JS thread)
+/// and hold it for later use to call back to JS via ,
+/// , or .
+///
public abstract class JSSynchronizationContext : SynchronizationContext, IDisposable
{
public bool IsDisposed { get; private set; }
@@ -224,7 +243,7 @@ public Task RunAsync(Func> asyncAction)
}
}
-public sealed class JSTsfnSynchronizationContext : JSSynchronizationContext
+internal sealed class JSTsfnSynchronizationContext : JSSynchronizationContext
{
private readonly JSThreadSafeFunction _tsfn;
@@ -233,7 +252,7 @@ public JSTsfnSynchronizationContext()
_tsfn = new JSThreadSafeFunction(
maxQueueSize: 0,
initialThreadCount: 1,
- asyncResourceName: (JSValue)"SynchronizationContext");
+ asyncResourceName: (JSValue)nameof(JSSynchronizationContext));
// Unref TSFN to indicate that this TSFN is not preventing Node.JS shutdown.
_tsfn.Unref();
@@ -295,7 +314,7 @@ public override void Send(SendOrPostCallback callback, object? state)
}
}
-public sealed class JSDispatcherSynchronizationContext : JSSynchronizationContext
+internal sealed class JSDispatcherSynchronizationContext : JSSynchronizationContext
{
private readonly JSDispatcherQueue _queue;
diff --git a/src/NodeApi/Interop/JSThreadSafeFunction.cs b/src/NodeApi/Interop/JSThreadSafeFunction.cs
index a8676201..44b44e5a 100644
--- a/src/NodeApi/Interop/JSThreadSafeFunction.cs
+++ b/src/NodeApi/Interop/JSThreadSafeFunction.cs
@@ -235,7 +235,7 @@ private static unsafe void CustomCallJS(napi_env env, napi_value jsCallback, nin
try
{
- using JSValueScope scope = new(JSValueScopeType.Callback, env);
+ using JSValueScope scope = new(JSValueScopeType.Callback, env, runtime: null);
object? callbackData = null;
if (data != default)
@@ -267,7 +267,7 @@ private static unsafe void DefaultCallJS(napi_env env, napi_value jsCallback, ni
try
{
- using JSValueScope scope = new(JSValueScopeType.Callback, env);
+ using JSValueScope scope = new(JSValueScopeType.Callback, env, runtime: null);
if (data != default)
{
@@ -299,6 +299,9 @@ private static unsafe void DefaultCallJS(napi_env env, napi_value jsCallback, ni
}
catch (Exception ex)
{
+#if DEBUG
+ Console.Error.WriteLine(ex);
+#endif
JSError.Fatal(ex.Message);
}
}
diff --git a/src/NodeApi/JSException.cs b/src/NodeApi/JSException.cs
index 6808dc66..2345f27f 100644
--- a/src/NodeApi/JSException.cs
+++ b/src/NodeApi/JSException.cs
@@ -7,7 +7,7 @@ namespace Microsoft.JavaScript.NodeApi;
///
/// An exception that was caused by an error thrown by JavaScript code or
-/// interactions with the JavaScript engine.
+/// interactions with JavaScript objects.
///
public class JSException : Exception
{
diff --git a/src/NodeApi/JSInvalidThreadAccessException.cs b/src/NodeApi/JSInvalidThreadAccessException.cs
new file mode 100644
index 00000000..8115317a
--- /dev/null
+++ b/src/NodeApi/JSInvalidThreadAccessException.cs
@@ -0,0 +1,87 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using Microsoft.JavaScript.NodeApi.Interop;
+
+namespace Microsoft.JavaScript.NodeApi;
+
+///
+/// An exception that was caused by an attempt to access a JavaScript value without any
+/// established on the current thread, or from a thread associated
+/// with a different environment / root scope.
+///
+///
+/// All JavaScript values are created within a scope that is bound to the thread that runs the
+/// JS environment. They can only be accessed from the same thread and only as long as the scope
+/// is still valid (not disposed).
+///
+///
+public class JSInvalidThreadAccessException : InvalidOperationException
+{
+ ///
+ /// Creates a new instance of with a
+ /// current scope and message.
+ ///
+ public JSInvalidThreadAccessException(
+ JSValueScope? currentScope,
+ string? message = null)
+ : this(currentScope, targetScope: null, message)
+ {
+ }
+
+ ///
+ /// Creates a new instance of with current
+ /// and target scopes and a message.
+ ///
+ public JSInvalidThreadAccessException(
+ JSValueScope? currentScope,
+ JSValueScope? targetScope,
+ string? message = null)
+ : base(message ?? GetMessage(currentScope, targetScope))
+ {
+ CurrentScope = currentScope;
+ TargetScope = targetScope;
+ }
+
+ ///
+ /// Gets the scope associated with the current thread ()
+ /// when the exception was thrown, or null if there was no scope for the thread.
+ ///
+ public JSValueScope? CurrentScope { get; }
+
+ ///
+ /// Gets the scope of the value () that was being accessed when
+ /// the exception was thrown, or null if a static operation was attempted.
+ ///
+ public JSValueScope? TargetScope { get; }
+
+ private static string GetMessage(JSValueScope? currentScope, JSValueScope? targetScope)
+ {
+ int threadId = Environment.CurrentManagedThreadId;
+ string? threadName = Thread.CurrentThread.Name;
+ string threadDescription = string.IsNullOrEmpty(threadName) ?
+ $"#{threadId}" : $"#{threadId} \"{threadName}\"";
+
+ if (targetScope == null)
+ {
+ // If the target scope is null, then this was an attempt to access either a static
+ // operation or a JS reference (which has an environment but no scope).
+ if (currentScope != null)
+ {
+ // In that case if the current scope is NOT null this exception
+ // shouldn't be thrown.
+ throw new ArgumentException("Current scope must be null if target scope is null.");
+ }
+
+ return $"There is no active JS value scope.\nCurrent thread: {threadDescription}. " +
+ $"Consider using the synchronization context to switch to the JS thread.";
+ }
+
+ return "The JS value scope cannot be accessed from the current thread.\n" +
+ $"The scope of type {targetScope.ScopeType} was created on thread" +
+ $"#{targetScope.ThreadId} and is being accessed from {threadDescription}. " +
+ $"Consider using the synchronization context to switch to the JS thread.";
+ }
+}
diff --git a/src/NodeApi/JSProxy.cs b/src/NodeApi/JSProxy.cs
index 07c26dcf..86137ac0 100644
--- a/src/NodeApi/JSProxy.cs
+++ b/src/NodeApi/JSProxy.cs
@@ -72,7 +72,7 @@ public JSProxy(
/// The proxy is not revocable.
public void Revoke()
{
- if (!_revoke.Handle.HasValue)
+ if (_revoke == default)
{
throw new InvalidOperationException("Proxy is not revokable.");
}
diff --git a/src/NodeApi/JSReference.cs b/src/NodeApi/JSReference.cs
index 22cea3e3..8f356a49 100644
--- a/src/NodeApi/JSReference.cs
+++ b/src/NodeApi/JSReference.cs
@@ -3,6 +3,7 @@
using System;
using System.Diagnostics.CodeAnalysis;
+using System.Threading;
using Microsoft.JavaScript.NodeApi.Interop;
using static Microsoft.JavaScript.NodeApi.JSNativeApi;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
@@ -42,12 +43,36 @@ public JSReference(JSValue value, bool isWeak = false)
public JSReference(napi_ref handle, bool isWeak = false)
{
+ JSValueScope currentScope = JSValueScope.Current;
+
+ // Thread access to the env will be checked on reference handle use.
+ _env = currentScope.UncheckedEnvironmentHandle;
_handle = handle;
- _env = (napi_env)JSValueScope.Current;
- _context = JSRuntimeContext.Current;
+ _context = currentScope.RuntimeContext;
IsWeak = isWeak;
}
+ ///
+ /// Gets the value handle, or throws an exception if access from the current thread is invalid.
+ ///
+ /// Access to the reference is not valid on
+ /// the current thread.
+ public napi_ref Handle
+ {
+ get
+ {
+ ThrowIfDisposed();
+ ThrowIfInvalidThreadAccess();
+ return _handle;
+ }
+ }
+
+ public static explicit operator napi_ref(JSReference reference)
+ {
+ if (reference is null) throw new ArgumentNullException(nameof(reference));
+ return reference.Handle;
+ }
+
public static bool TryCreateReference(
JSValue value, bool isWeak, [NotNullWhen(true)] out JSReference? result)
{
@@ -76,29 +101,36 @@ public static bool TryCreateReference(
///
public JSSynchronizationContext? SynchronizationContext => _context?.SynchronizationContext;
+ private napi_env Env
+ {
+ get
+ {
+ ThrowIfDisposed();
+ ThrowIfInvalidThreadAccess();
+ return _env;
+ }
+ }
+
public void MakeWeak()
{
- ThrowIfDisposed();
if (!IsWeak)
{
- JSValueScope.CurrentRuntime.UnrefReference(_env, _handle, out _).ThrowIfFailed();
+ JSValueScope.CurrentRuntime.UnrefReference(Env, _handle, out _).ThrowIfFailed();
IsWeak = true;
}
}
public void MakeStrong()
{
- ThrowIfDisposed();
if (IsWeak)
{
- JSValueScope.CurrentRuntime.RefReference(_env, _handle, out _).ThrowIfFailed();
- IsWeak = true;
+ JSValueScope.CurrentRuntime.RefReference(Env, _handle, out _).ThrowIfFailed();
+ IsWeak = false;
}
}
public JSValue? GetValue()
{
- ThrowIfDisposed();
- JSValueScope.CurrentRuntime.GetReferenceValue(_env, _handle, out napi_value result)
+ JSValueScope.CurrentRuntime.GetReferenceValue(Env, _handle, out napi_value result)
.ThrowIfFailed();
return result;
}
@@ -161,8 +193,6 @@ T GetValueAndRunAction()
}
}
- public static explicit operator napi_ref(JSReference value) => value._handle;
-
public bool IsDisposed { get; private set; }
private void ThrowIfDisposed()
@@ -173,6 +203,28 @@ private void ThrowIfDisposed()
}
}
+ ///
+ /// Checks that the current thread is the thread that is running the JavaScript environment
+ /// that this reference was created in.
+ ///
+ /// The reference cannot be accessed from the
+ /// current thread.
+ private void ThrowIfInvalidThreadAccess()
+ {
+ JSValueScope currentScope = JSValueScope.Current;
+ if ((napi_env)currentScope != _env)
+ {
+ int threadId = Environment.CurrentManagedThreadId;
+ string? threadName = Thread.CurrentThread.Name;
+ string threadDescription = string.IsNullOrEmpty(threadName) ?
+ $"#{threadId}" : $"#{threadId} \"{threadName}\"";
+ string message = "The JS reference cannot be accessed from the current thread.\n" +
+ $"Current thread: {threadDescription}. " +
+ $"Consider using the synchronization context to switch to the JS thread.";
+ throw new JSInvalidThreadAccessException(currentScope, message);
+ }
+ }
+
///
/// Releases the reference.
///
@@ -188,19 +240,19 @@ protected virtual void Dispose(bool disposing)
if (!IsDisposed)
{
IsDisposed = true;
- napi_ref handle = _handle; // To capture in lambda
// The context may be null if the reference was created from a "no-context" scope such
// as the native host. In that case the reference must be disposed from the JS thread.
- if (SynchronizationContext == null)
+ if (_context == null)
{
- JSValueScope.CurrentRuntime.DeleteReference(_env, handle).ThrowIfFailed();
+ ThrowIfInvalidThreadAccess();
+ JSValueScope.CurrentRuntime.DeleteReference(_env, _handle).ThrowIfFailed();
}
else
{
- SynchronizationContext.Post(
- () => JSValueScope.CurrentRuntime.DeleteReference(
- _env, handle).ThrowIfFailed(), allowSync: true);
+ _context.SynchronizationContext.Post(
+ () => _context.Runtime.DeleteReference(
+ _env, _handle).ThrowIfFailed(), allowSync: true);
}
}
}
diff --git a/src/NodeApi/JSValue.cs b/src/NodeApi/JSValue.cs
index 4e59a878..6c5914f4 100644
--- a/src/NodeApi/JSValue.cs
+++ b/src/NodeApi/JSValue.cs
@@ -4,10 +4,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
-using System.Text;
using Microsoft.JavaScript.NodeApi.Interop;
using Microsoft.JavaScript.NodeApi.Runtime;
using static Microsoft.JavaScript.NodeApi.JSNativeApi;
+using static Microsoft.JavaScript.NodeApi.JSValueScope;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
namespace Microsoft.JavaScript.NodeApi;
@@ -21,38 +21,97 @@ namespace Microsoft.JavaScript.NodeApi;
internal JSRuntime Runtime => Scope.Runtime;
- public JSValue() { }
+ ///
+ /// Creates an empty instance of , which implicitly converts to
+ /// when used in any scope.
+ ///
+ public JSValue() : this(default, null) { }
- public JSValue(napi_value handle) : this(handle, JSValueScope.Current)
- {
- }
+ ///
+ /// Creates a new instance of from a handle in the current scope.
+ ///
+ /// Thrown when the handle is null.
+ ///
+ /// WARNING: A JS value handle is a pointer to a location in memory, so an invalid handle here
+ /// may cause an attempt to access an invalid memory location.
+ ///
+ public JSValue(napi_value handle) : this(handle, JSValueScope.Current) { }
+ ///
+ /// Creates a new instance of from a handle in the specified scope.
+ ///
+ /// Thrown when either the handle or scope is null
+ /// (unless they are both null then this becomse an empty value that implicitly converts
+ /// to ).
+ ///
+ /// WARNING: A JS value handle is a pointer to a location in memory, so an invalid handle here
+ /// may cause an attempt to access an invalid memory location.
+ ///
public JSValue(napi_value handle, JSValueScope? scope)
{
- if (!handle.IsNull && scope is null) throw new ArgumentNullException(nameof(scope));
+ if (scope is null)
+ {
+ if (!handle.IsNull) throw new ArgumentNullException(nameof(scope));
+ }
+ else
+ {
+ if (handle.IsNull) throw new ArgumentNullException(nameof(handle));
+ }
+
_handle = handle;
_scope = scope;
}
- public napi_value? Handle
- => !Scope.IsDisposed ? (_handle.Handle != default(nint) ? _handle : Undefined._handle) : null;
+ ///
+ /// Gets the value handle, or throws an exception if the value scope is disposed or
+ /// access from the current thread is invalid.
+ ///
+ /// The scope has been closed.
+ /// The scope is not valid on the current
+ /// thread.
+ public napi_value Handle
+ {
+ get
+ {
+ if (_scope == null)
+ {
+ // If the scope is null, this is an empty (uninitialized) instance.
+ // Implicitly convert to the JS `undefined` value.
+ return Undefined._handle;
+ }
+
+ // Ensure the scope is valid and on the current thread (environment).
+ _scope.ThrowIfDisposed();
+ _scope.ThrowIfInvalidThreadAccess();
+
+ // The handle must be non-null when the scope is non-null.
+ return _handle;
+ }
+ }
- public napi_value GetCheckedHandle()
- => Handle ?? throw new InvalidOperationException(
- "The value handle is invalid because its scope is closed");
+ public static implicit operator JSValue(napi_value handle) => new(handle);
+ public static implicit operator JSValue?(napi_value handle) => handle.Handle != default ? new(handle) : default;
+ public static explicit operator napi_value(JSValue value) => value.Handle;
+ public static explicit operator napi_value(JSValue? value) => value?.Handle ?? default;
- private static napi_env Env => (napi_env)JSValueScope.Current;
+ ///
+ /// Gets the environment handle for the value's scope without checking whether the scope
+ /// is disposed or whether access from the current thread is valid. WARNING: This must only
+ /// be used to avoid redundant handle checks when there is another (checked) access to
+ /// for the same call.
+ ///
+ internal napi_env UncheckedEnvironmentHandle => Scope.UncheckedEnvironmentHandle;
public static JSValue Undefined
- => JSValueScope.CurrentRuntime.GetUndefined(Env, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.GetUndefined(CurrentEnvironmentHandle, out napi_value result).ThrowIfFailed(result);
public static JSValue Null
- => JSValueScope.CurrentRuntime.GetNull(Env, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.GetNull(CurrentEnvironmentHandle, out napi_value result).ThrowIfFailed(result);
public static JSValue Global
- => JSValueScope.CurrentRuntime.GetGlobal(Env, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.GetGlobal(CurrentEnvironmentHandle, out napi_value result).ThrowIfFailed(result);
public static JSValue True => GetBoolean(true);
public static JSValue False => GetBoolean(false);
public static JSValue GetBoolean(bool value)
- => JSValueScope.CurrentRuntime.GetBoolean(Env, value, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.GetBoolean(CurrentEnvironmentHandle, value, out napi_value result).ThrowIfFailed(result);
public JSObject Properties => (JSObject)this;
@@ -77,38 +136,38 @@ public JSValue this[int index]
}
public static JSValue CreateObject()
- => JSValueScope.CurrentRuntime.CreateObject(Env, out napi_value result)
+ => CurrentRuntime.CreateObject(CurrentEnvironmentHandle, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateArray()
- => JSValueScope.CurrentRuntime.CreateArray(Env, out napi_value result)
+ => CurrentRuntime.CreateArray(CurrentEnvironmentHandle, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateArray(int length)
- => JSValueScope.CurrentRuntime.CreateArray(Env, length, out napi_value result)
+ => CurrentRuntime.CreateArray(CurrentEnvironmentHandle, length, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateNumber(double value)
- => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result)
+ => CurrentRuntime.CreateNumber(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateNumber(int value)
- => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result)
+ => CurrentRuntime.CreateNumber(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateNumber(uint value)
- => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result)
+ => CurrentRuntime.CreateNumber(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateNumber(long value)
- => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result)
+ => CurrentRuntime.CreateNumber(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
public static unsafe JSValue CreateStringUtf8(ReadOnlySpan value)
{
fixed (byte* spanPtr = value)
{
- return JSValueScope.CurrentRuntime.CreateString(Env, value, out napi_value result)
+ return CurrentRuntime.CreateString(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
}
}
@@ -117,7 +176,7 @@ public static unsafe JSValue CreateStringUtf16(ReadOnlySpan value)
{
fixed (char* spanPtr = value)
{
- return JSValueScope.CurrentRuntime.CreateString(Env, value, out napi_value result)
+ return CurrentRuntime.CreateString(CurrentEnvironmentHandle, value, out napi_value result)
.ThrowIfFailed(result);
}
}
@@ -126,18 +185,18 @@ public static unsafe JSValue CreateStringUtf16(string value)
{
fixed (char* spanPtr = value)
{
- return JSValueScope.CurrentRuntime.CreateString(Env, value.AsSpan(), out napi_value result)
+ return CurrentRuntime.CreateString(CurrentEnvironmentHandle, value.AsSpan(), out napi_value result)
.ThrowIfFailed(result);
}
}
public static JSValue CreateSymbol(JSValue description)
- => JSValueScope.CurrentRuntime.CreateSymbol(
- Env, (napi_value)description, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.CreateSymbol(
+ CurrentEnvironmentHandle, (napi_value)description, out napi_value result).ThrowIfFailed(result);
public static JSValue SymbolFor(string name)
{
- return JSValueScope.CurrentRuntime.GetSymbolFor(Env, name, out napi_value result)
+ return CurrentRuntime.GetSymbolFor(CurrentEnvironmentHandle, name, out napi_value result)
.ThrowIfFailed(result);
}
@@ -146,8 +205,8 @@ public static JSValue CreateFunction(
napi_callback callback,
nint data)
{
- return JSValueScope.CurrentRuntime.CreateFunction(
- Env, name, callback, data, out napi_value result)
+ return CurrentRuntime.CreateFunction(
+ CurrentEnvironmentHandle, name, callback, data, out napi_value result)
.ThrowIfFailed(result);
}
@@ -167,43 +226,44 @@ public static unsafe JSValue CreateFunction(
}
public static JSValue CreateError(JSValue? code, JSValue message)
- => JSValueScope.CurrentRuntime.CreateError(Env, (napi_value)code, (napi_value)message,
+ => CurrentRuntime.CreateError(CurrentEnvironmentHandle, (napi_value)code, (napi_value)message,
out napi_value result).ThrowIfFailed(result);
public static JSValue CreateTypeError(JSValue? code, JSValue message)
- => JSValueScope.CurrentRuntime.CreateTypeError(Env, (napi_value)code, (napi_value)message,
+ => CurrentRuntime.CreateTypeError(CurrentEnvironmentHandle, (napi_value)code, (napi_value)message,
out napi_value result).ThrowIfFailed(result);
public static JSValue CreateRangeError(JSValue? code, JSValue message)
- => JSValueScope.CurrentRuntime.CreateRangeError(Env, (napi_value)code, (napi_value)message,
+ => CurrentRuntime.CreateRangeError(CurrentEnvironmentHandle, (napi_value)code, (napi_value)message,
out napi_value result).ThrowIfFailed(result);
public static JSValue CreateSyntaxError(JSValue? code, JSValue message)
- => JSValueScope.CurrentRuntime.CreateSyntaxError(Env, (napi_value)code, (napi_value)message,
+ => CurrentRuntime.CreateSyntaxError(CurrentEnvironmentHandle, (napi_value)code, (napi_value)message,
out napi_value result).ThrowIfFailed(result);
public static unsafe JSValue CreateExternal(object value)
{
- GCHandle valueHandle = JSRuntimeContext.Current.AllocGCHandle(value);
- return JSValueScope.CurrentRuntime.CreateExternal(
- Env,
+ JSValueScope currentScope = JSValueScope.Current;
+ GCHandle valueHandle = currentScope.RuntimeContext.AllocGCHandle(value);
+ return CurrentRuntime.CreateExternal(
+ (napi_env)currentScope,
(nint)valueHandle,
new napi_finalize(s_finalizeGCHandle),
- default,
+ currentScope.RuntimeContextHandle,
out napi_value result)
.ThrowIfFailed(result);
}
public static unsafe JSValue CreateArrayBuffer(int byteLength)
{
- JSValueScope.CurrentRuntime.CreateArrayBuffer(Env, byteLength, out nint _, out napi_value result)
+ CurrentRuntime.CreateArrayBuffer(CurrentEnvironmentHandle, byteLength, out nint _, out napi_value result)
.ThrowIfFailed();
return result;
}
public static unsafe JSValue CreateArrayBuffer(ReadOnlySpan data)
{
- JSValueScope.CurrentRuntime.CreateArrayBuffer(Env, data.Length, out nint buffer, out napi_value result)
+ CurrentRuntime.CreateArrayBuffer(CurrentEnvironmentHandle, data.Length, out nint buffer, out napi_value result)
.ThrowIfFailed();
data.CopyTo(new Span((void*)buffer, data.Length));
return result;
@@ -213,26 +273,26 @@ public static unsafe JSValue CreateExternalArrayBuffer(
Memory memory, object? external = null) where T : struct
{
var pinnedMemory = new PinnedMemory(memory, external);
- return JSValueScope.CurrentRuntime.CreateArrayBuffer(
- Env,
+ return CurrentRuntime.CreateArrayBuffer(
+ CurrentEnvironmentHandle,
(nint)pinnedMemory.Pointer,
pinnedMemory.Length,
// We pass object to finalize as a hint parameter
- new napi_finalize(s_finalizeHintHandle),
- (nint)JSRuntimeContext.Current.AllocGCHandle(pinnedMemory),
+ new napi_finalize(s_finalizeGCHandleToPinnedMemory),
+ (nint)pinnedMemory.RuntimeContext.AllocGCHandle(pinnedMemory),
out napi_value result)
.ThrowIfFailed(result);
}
public static JSValue CreateDataView(int length, JSValue arrayBuffer, int byteOffset)
- => JSValueScope.CurrentRuntime.CreateDataView(
- Env, length, (napi_value)arrayBuffer, byteOffset, out napi_value result)
+ => CurrentRuntime.CreateDataView(
+ CurrentEnvironmentHandle, length, (napi_value)arrayBuffer, byteOffset, out napi_value result)
.ThrowIfFailed(result);
public static JSValue CreateTypedArray(
JSTypedArrayType type, int length, JSValue arrayBuffer, int byteOffset)
- => JSValueScope.CurrentRuntime.CreateTypedArray(
- Env,
+ => CurrentRuntime.CreateTypedArray(
+ CurrentEnvironmentHandle,
(napi_typedarray_type)type,
length,
(napi_value)arrayBuffer,
@@ -242,24 +302,24 @@ public static JSValue CreateTypedArray(
public static JSValue CreatePromise(out JSPromise.Deferred deferred)
{
- JSValueScope.CurrentRuntime.CreatePromise(Env, out napi_deferred deferred_, out napi_value promise)
+ CurrentRuntime.CreatePromise(CurrentEnvironmentHandle, out napi_deferred deferred_, out napi_value promise)
.ThrowIfFailed();
deferred = new JSPromise.Deferred(deferred_);
return promise;
}
public static JSValue CreateDate(double time)
- => JSValueScope.CurrentRuntime.CreateDate(Env, time, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.CreateDate(CurrentEnvironmentHandle, time, out napi_value result).ThrowIfFailed(result);
public static JSValue CreateBigInt(long value)
- => JSValueScope.CurrentRuntime.CreateBigInt(Env, value, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.CreateBigInt(CurrentEnvironmentHandle, value, out napi_value result).ThrowIfFailed(result);
public static JSValue CreateBigInt(ulong value)
- => JSValueScope.CurrentRuntime.CreateBigInt(Env, value, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.CreateBigInt(CurrentEnvironmentHandle, value, out napi_value result).ThrowIfFailed(result);
public static JSValue CreateBigInt(int signBit, ReadOnlySpan words)
{
- return JSValueScope.CurrentRuntime.CreateBigInt(Env, signBit, words, out napi_value result)
+ return CurrentRuntime.CreateBigInt(CurrentEnvironmentHandle, signBit, words, out napi_value result)
.ThrowIfFailed(result);
}
@@ -319,11 +379,6 @@ public static JSValue CreateBigInt(int signBit, ReadOnlySpan words)
public static explicit operator float?(JSValue value) => ValueOrDefault(value, value => (float)value.GetValueDouble());
public static explicit operator double?(JSValue value) => ValueOrDefault(value, value => value.GetValueDouble());
- public static implicit operator JSValue(napi_value handle) => new(handle);
- public static implicit operator JSValue?(napi_value handle) => handle.Handle != default ? new JSValue(handle) : default;
- public static explicit operator napi_value(JSValue value) => value.GetCheckedHandle();
- public static explicit operator napi_value(JSValue? value) => value?.GetCheckedHandle() ?? default;
-
private static JSValue ValueOrDefault(T? value, Func convert) where T : struct
=> value.HasValue ? convert(value.Value) : default;
diff --git a/src/NodeApi/JSValueScope.cs b/src/NodeApi/JSValueScope.cs
index 225fa354..aa528cae 100644
--- a/src/NodeApi/JSValueScope.cs
+++ b/src/NodeApi/JSValueScope.cs
@@ -2,7 +2,7 @@
// Licensed under the MIT License.
using System;
-using System.Diagnostics;
+using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.JavaScript.NodeApi.Interop;
using Microsoft.JavaScript.NodeApi.Runtime;
@@ -71,7 +71,9 @@ public enum JSValueScopeType
public sealed class JSValueScope : IDisposable
{
private readonly JSValueScope? _parentScope;
+#pragma warning disable IDE0032 // Use auto property
private readonly napi_env _env;
+#pragma warning restore IDE0032
private readonly SynchronizationContext? _previousSyncContext;
private readonly nint _scopeHandle;
@@ -82,25 +84,91 @@ public sealed class JSValueScope : IDisposable
///
/// Gets the current JS value scope.
///
- /// No scope was established for the current
+ /// No scope was established for the current
/// thread.
public static JSValueScope Current => s_currentScope ??
- throw new InvalidOperationException("No current scope.");
+ throw new JSInvalidThreadAccessException(currentScope: null);
+
+ ///
+ /// Gets the envionment handle for the scope, or throws an exception if the scope is
+ /// disposed or access from the current thread is invalid.
+ ///
+ /// The scope has been closed.
+ /// The scope is not valid on the current
+ /// thread.
+ public napi_env EnvironmentHandle
+ {
+ get
+ {
+ ThrowIfDisposed();
+ ThrowIfInvalidThreadAccess();
+ return _env;
+ }
+ }
+
+ public static explicit operator napi_env(JSValueScope scope)
+ {
+ if (scope is null) throw new ArgumentNullException(nameof(scope));
+ return scope.EnvironmentHandle;
+ }
+
+ ///
+ /// Gets the environment handle without checking whether the scope is disposed or
+ /// whether access from the current thread is valid. WARNING: This must only be used
+ /// to avoid redundant handle checks when there is another (checked) access to
+ /// for the same call.
+ ///
+ internal napi_env UncheckedEnvironmentHandle => _env;
+
+ ///
+ /// Gets the environment handle for the current thread scope, or throws an exception if
+ /// there is no environment for the current thread. For use only with static operations
+ /// not related to any ; for value operations use
+ /// instead.
+ ///
+ /// No scope was established for the current
+ /// thread.
+ internal static napi_env CurrentEnvironmentHandle => Current.EnvironmentHandle;
+
+ internal int ThreadId { get; }
public bool IsDisposed { get; private set; }
public JSRuntime Runtime { get; }
public JSRuntimeContext RuntimeContext { get; }
+ internal nint RuntimeContextHandle { get; }
internal static JSRuntime CurrentRuntime => Current.Runtime;
internal static JSRuntimeContext? CurrentRuntimeContext => s_currentScope?.RuntimeContext;
public JSModuleContext? ModuleContext { get; internal set; }
+ ///
+ /// Creates a new instance of a with a specified scope type.
+ ///
+ /// The type of scope to create; default is
+ /// .
+ public JSValueScope(JSValueScopeType scopeType = JSValueScopeType.Handle)
+ : this(scopeType, env: default, runtime: default)
+ {
+ }
+
+ ///
+ /// Creates a new instance of a , which may be a parentless scope
+ /// with initial enviroment handle and JS runtime.
+ ///
+ /// The type of scope to create.
+ /// JS environment handle, required only for creating a scope
+ /// without a parent, otherwise the environment is inherited from the parent scope.
+ /// JS runtime interface, required only for creating a scope
+ /// without a parent, otherwise the JS runtime is inherited from the parent scope.
+ /// Optional synchronization context to use for async
+ /// operations; if omitted then a default synchronization context is used.
public JSValueScope(
- JSValueScopeType scopeType = JSValueScopeType.Handle,
- napi_env env = default,
- JSRuntime? runtime = null)
+ JSValueScopeType scopeType,
+ napi_env env,
+ JSRuntime? runtime,
+ JSSynchronizationContext? synchronizationContext = null)
{
ScopeType = scopeType;
@@ -125,6 +193,7 @@ public JSValueScope(
_parentScope = null;
_env = env;
+ ThreadId = Environment.CurrentManagedThreadId;
Runtime = runtime;
}
else if (scopeType == JSValueScopeType.Root)
@@ -157,11 +226,13 @@ public JSValueScope(
}
_env = env;
+ ThreadId = Environment.CurrentManagedThreadId;
Runtime = runtime;
}
else
{
_parentScope = s_currentScope;
+
if (scopeType == JSValueScopeType.Module &&
_parentScope != null && _parentScope.ScopeType == JSValueScopeType.Module)
{
@@ -174,35 +245,49 @@ public JSValueScope(
// Module scopes may be created without a parent scope (for AOT modules).
if (scopeType != JSValueScopeType.Module)
{
- throw new InvalidOperationException("Parent scope not found.");
+ throw new InvalidOperationException(
+ $"A {scopeType} scope cannot be created without a parent scope.");
}
// AOT module scopes are constructed with an env parameter
// but without a pre-initialized runtime.
_env = env.IsNull ? throw new ArgumentNullException(nameof(env)) : env;
+ ThreadId = Environment.CurrentManagedThreadId;
Runtime = runtime ?? new NodejsRuntime();
}
+ else if (_parentScope.IsDisposed)
+ {
+ // This should never happen because disposing a scope removes it from
+ // s_currentScope (which is used to initialize _parentScope above).
+ throw new InvalidOperationException("Parent scope is disposed.");
+ }
+ else if (scopeType == JSValueScopeType.Callback &&
+ _parentScope.ScopeType != JSValueScopeType.Callback &&
+ _parentScope.ScopeType != JSValueScopeType.Module &&
+ _parentScope.ScopeType != JSValueScopeType.Root &&
+ _parentScope.ScopeType != JSValueScopeType.NoContext)
+ {
+ throw new InvalidOperationException(
+ $"A Callback scope must be created within a Root, Module, or Callback scope. " +
+ $"Current scope: {scopeType}");
+ }
+ else if (!env.IsNull && env != _parentScope._env)
+ {
+ throw new ArgumentException(
+ "Environment must not be provided for a non-root scope.",
+ nameof(env));
+ }
+ else if (runtime != null && runtime != _parentScope.Runtime)
+ {
+ throw new ArgumentException(
+ "Runtime must not be provided for a non-root scope.",
+ nameof(runtime));
+ }
else
{
- if (_parentScope.IsDisposed)
- {
- throw new InvalidOperationException("Parent scope is disposed.");
- }
-
- if (!env.IsNull && env != _parentScope._env)
- {
- throw new ArgumentException(
- "Environment must not be provided for a non-root scope.",
- nameof(env));
- }
- else if (runtime != null && runtime != _parentScope.Runtime)
- {
- throw new ArgumentException(
- "Runtime must not be provided for a non-root scope.",
- nameof(runtime));
- }
-
+ _parentScope.ThrowIfInvalidThreadAccess();
_env = _parentScope._env;
+ ThreadId = _parentScope.ThreadId;
Runtime = _parentScope.Runtime;
}
@@ -238,8 +323,24 @@ public JSValueScope(
{
s_currentScope = this;
- RuntimeContext = scopeType == JSValueScopeType.NoContext ? null! :
- _parentScope?.RuntimeContext ?? new JSRuntimeContext(env);
+ if (scopeType == JSValueScopeType.NoContext)
+ {
+ // NoContext scopes do not have a runtime context.
+ RuntimeContext = null!;
+ RuntimeContextHandle = default;
+ }
+ else if (_parentScope?.RuntimeContext != null)
+ {
+ // Nested scopes inherit the runtime context from the parent scope.
+ RuntimeContext = _parentScope.RuntimeContext;
+ RuntimeContextHandle = _parentScope.RuntimeContextHandle;
+ }
+ else
+ {
+ // Unparented scopes initialize a new runtime context.
+ RuntimeContext = new JSRuntimeContext(env, Runtime, synchronizationContext);
+ RuntimeContextHandle = (nint)GCHandle.Alloc(RuntimeContext);
+ }
if (scopeType == JSValueScopeType.Root || scopeType == JSValueScopeType.Callback)
{
@@ -262,7 +363,7 @@ public void Dispose()
if (ScopeType != JSValueScopeType.NoContext)
{
- napi_env env = (napi_env)RuntimeContext;
+ napi_env env = RuntimeContext.EnvironmentHandle;
switch (ScopeType)
{
@@ -278,9 +379,9 @@ public void Dispose()
SynchronizationContext.SetSynchronizationContext(_previousSyncContext);
break;
}
-
- s_currentScope = _parentScope;
}
+
+ s_currentScope = _parentScope;
}
public JSValue Escape(JSValue value)
@@ -300,9 +401,29 @@ public JSValue Escape(JSValue value)
return new JSValue(result, _parentScope);
}
- public static explicit operator napi_env(JSValueScope scope)
+ ///
+ /// Checks that this scope has not been closed (disposed).
+ ///
+ /// The scope is closed.
+ internal void ThrowIfDisposed()
{
- if (scope is null) throw new ArgumentNullException(nameof(scope));
- return scope!._env;
+ if (IsDisposed)
+ {
+ throw new JSValueScopeClosedException(scope: this);
+ }
+ }
+
+ ///
+ /// Checks that the current thread is the thread that is running the JavaScript environment
+ /// that this scope is in.
+ ///
+ /// The scope cannot be accessed from the current
+ /// thread.
+ internal void ThrowIfInvalidThreadAccess()
+ {
+ if (s_currentScope?._env != _env)
+ {
+ throw new JSInvalidThreadAccessException(currentScope: s_currentScope, targetScope: this);
+ }
}
}
diff --git a/src/NodeApi/JSValueScopeClosedException.cs b/src/NodeApi/JSValueScopeClosedException.cs
new file mode 100644
index 00000000..370389f9
--- /dev/null
+++ b/src/NodeApi/JSValueScopeClosedException.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Microsoft.JavaScript.NodeApi;
+
+///
+/// An exception that was caused by an attempt to access a (or a more
+/// specific JS value type, such as or )
+/// after its was closed.
+///
+public class JSValueScopeClosedException : ObjectDisposedException
+{
+ ///
+ /// Creates a new instance of with an optional
+ /// object name and message.
+ ///
+ public JSValueScopeClosedException(JSValueScope scope, string? message = null)
+ : base(scope.ScopeType.ToString(), message ?? GetMessage(scope))
+ {
+ Scope = scope;
+ }
+
+ public JSValueScope Scope { get; }
+
+ private static string GetMessage(JSValueScope scope)
+ {
+ return $"The JS value scope of type {scope.ScopeType} was closed.\n" +
+ "Values created within a scope are no longer available after their scope is " +
+ "closed. Consider using an escapable scope to promote a value to the parent scope, " +
+ "or a reference to make a value available to a future callback scope.";
+ }
+}
diff --git a/src/NodeApi/Native/JSNativeApi.cs b/src/NodeApi/Native/JSNativeApi.cs
index 621fc55a..302a6d0a 100644
--- a/src/NodeApi/Native/JSNativeApi.cs
+++ b/src/NodeApi/Native/JSNativeApi.cs
@@ -6,9 +6,8 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
-using System.Text;
using Microsoft.JavaScript.NodeApi.Interop;
-using Microsoft.JavaScript.NodeApi.Runtime;
+using static Microsoft.JavaScript.NodeApi.JSValueScope;
using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
namespace Microsoft.JavaScript.NodeApi;
@@ -16,85 +15,80 @@ namespace Microsoft.JavaScript.NodeApi;
// Node API managed wrappers
public static partial class JSNativeApi
{
- ///
- /// Hint to a finalizer callback that indicates the object referenced by the handle should be
- /// disposed when finalizing.
- ///
- private const nint DisposeHint = (nint)1;
-
public static unsafe void AddGCHandleFinalizer(this JSValue thisValue, nint handle)
{
if (handle != default)
{
thisValue.Runtime.AddFinalizer(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
handle,
new napi_finalize(s_finalizeGCHandle),
- default,
+ thisValue.Scope.RuntimeContextHandle,
out _).ThrowIfFailed();
}
}
- public static unsafe JSValueType TypeOf(this JSValue value)
- => value.Runtime.GetValueType(Env, (napi_value)value, out napi_valuetype result)
+ public static unsafe JSValueType TypeOf(this JSValue thisValue)
+ => thisValue.Runtime.GetValueType(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_valuetype result)
.ThrowIfFailed((JSValueType)result);
- public static unsafe bool IsUndefined(this JSValue value)
- => value.TypeOf() == JSValueType.Undefined;
+ public static unsafe bool IsUndefined(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Undefined;
- public static unsafe bool IsNull(this JSValue value)
- => value.TypeOf() == JSValueType.Null;
+ public static unsafe bool IsNull(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Null;
- public static unsafe bool IsNullOrUndefined(this JSValue value) => value.TypeOf() switch
+ public static unsafe bool IsNullOrUndefined(this JSValue thisValue) => thisValue.TypeOf() switch
{
JSValueType.Null => true,
JSValueType.Undefined => true,
_ => false,
};
- public static unsafe bool IsBoolean(this JSValue value)
- => value.TypeOf() == JSValueType.Boolean;
+ public static unsafe bool IsBoolean(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Boolean;
- public static unsafe bool IsNumber(this JSValue value)
- => value.TypeOf() == JSValueType.Number;
+ public static unsafe bool IsNumber(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Number;
- public static unsafe bool IsString(this JSValue value)
- => value.TypeOf() == JSValueType.String;
+ public static unsafe bool IsString(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.String;
- public static unsafe bool IsSymbol(this JSValue value)
- => value.TypeOf() == JSValueType.Symbol;
+ public static unsafe bool IsSymbol(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Symbol;
- public static unsafe bool IsObject(this JSValue value)
+ public static unsafe bool IsObject(this JSValue thisValue)
{
- JSValueType valueType = value.TypeOf();
+ JSValueType valueType = thisValue.TypeOf();
return (valueType == JSValueType.Object) || (valueType == JSValueType.Function);
}
- public static unsafe bool IsFunction(this JSValue value)
- => value.TypeOf() == JSValueType.Function;
+ public static unsafe bool IsFunction(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.Function;
- public static unsafe bool IsExternal(this JSValue value)
- => value.TypeOf() == JSValueType.External;
+ public static unsafe bool IsExternal(this JSValue thisValue)
+ => thisValue.TypeOf() == JSValueType.External;
- public static double GetValueDouble(this JSValue value)
- => value.Runtime.GetValueDouble(Env, (napi_value)value, out double result)
+ public static double GetValueDouble(this JSValue thisValue)
+ => thisValue.Runtime.GetValueDouble(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out double result)
.ThrowIfFailed(result);
- public static int GetValueInt32(this JSValue value)
- => value.Runtime.GetValueInt32(Env, (napi_value)value, out int result)
+ public static int GetValueInt32(this JSValue thisValue)
+ => thisValue.Runtime.GetValueInt32(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out int result)
.ThrowIfFailed(result);
- public static uint GetValueUInt32(this JSValue value)
- => value.Runtime.GetValueUInt32(Env, (napi_value)value, out uint result)
+ public static uint GetValueUInt32(this JSValue thisValue)
+ => thisValue.Runtime.GetValueUInt32(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out uint result)
.ThrowIfFailed(result);
- public static long GetValueInt64(this JSValue value)
- => value.Runtime.GetValueInt64(Env, (napi_value)value, out long result)
+ public static long GetValueInt64(this JSValue thisValue)
+ => thisValue.Runtime.GetValueInt64(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out long result)
.ThrowIfFailed(result);
- public static bool GetValueBool(this JSValue value)
- => value.Runtime.GetValueBool(Env, (napi_value)value, out bool result)
+ public static bool GetValueBool(this JSValue thisValue)
+ => thisValue.Runtime.GetValueBool(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static unsafe int GetValueStringUtf8(this JSValue thisValue, Span buffer)
@@ -102,20 +96,20 @@ public static unsafe int GetValueStringUtf8(this JSValue thisValue, Span b
if (buffer.IsEmpty)
{
return thisValue.Runtime.GetValueStringUtf8(
- Env, (napi_value)thisValue, [], out int result)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, [], out int result)
.ThrowIfFailed(result);
}
return thisValue.Runtime.GetValueStringUtf8(
- Env, (napi_value)thisValue, buffer, out int result2)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, buffer, out int result2)
.ThrowIfFailed(result2);
}
- public static byte[] GetValueStringUtf8(this JSValue value)
+ public static byte[] GetValueStringUtf8(this JSValue thisValue)
{
- int length = GetValueStringUtf8(value, []);
+ int length = GetValueStringUtf8(thisValue, []);
byte[] result = new byte[length + 1];
- GetValueStringUtf8(value, new Span(result));
+ GetValueStringUtf8(thisValue, new Span(result));
// Remove the zero terminating character
Array.Resize(ref result, length);
return result;
@@ -126,96 +120,112 @@ public static unsafe int GetValueStringUtf16(this JSValue thisValue, Span
if (buffer.IsEmpty)
{
return thisValue.Runtime.GetValueStringUtf16(
- Env, (napi_value)thisValue, [], out int result)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, [], out int result)
.ThrowIfFailed(result);
}
return thisValue.Runtime.GetValueStringUtf16(
- Env, (napi_value)thisValue, buffer, out int result2)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, buffer, out int result2)
.ThrowIfFailed(result2);
}
- public static char[] GetValueStringUtf16AsCharArray(this JSValue value)
+ public static char[] GetValueStringUtf16AsCharArray(this JSValue thisValue)
{
- int length = GetValueStringUtf16(value, []);
+ int length = GetValueStringUtf16(thisValue, []);
char[] result = new char[length + 1];
- GetValueStringUtf16(value, new Span(result));
+ GetValueStringUtf16(thisValue, new Span(result));
// Remove the zero terminating character
Array.Resize(ref result, length);
return result;
}
- public static string GetValueStringUtf16(this JSValue value)
- => new(GetValueStringUtf16AsCharArray(value));
+ public static string GetValueStringUtf16(this JSValue thisValue)
+ => new(GetValueStringUtf16AsCharArray(thisValue));
- public static JSValue CoerceToBoolean(this JSValue value)
- => value.Runtime.CoerceToBool(Env, (napi_value)value, out napi_value result)
+ public static JSValue CoerceToBoolean(this JSValue thisValue)
+ => thisValue.Runtime.CoerceToBool(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
- public static JSValue CoerceToNumber(this JSValue value)
- => value.Runtime.CoerceToNumber(Env, (napi_value)value, out napi_value result)
+ public static JSValue CoerceToNumber(this JSValue thisValue)
+ => thisValue.Runtime.CoerceToNumber(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
- public static JSValue CoerceToObject(this JSValue value)
- => value.Runtime.CoerceToObject(Env, (napi_value)value, out napi_value result)
+ public static JSValue CoerceToObject(this JSValue thisValue)
+ => thisValue.Runtime.CoerceToObject(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
- public static JSValue CoerceToString(this JSValue value)
- => value.Runtime.CoerceToString(Env, (napi_value)value, out napi_value result)
+ public static JSValue CoerceToString(this JSValue thisValue)
+ => thisValue.Runtime.CoerceToString(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
- public static JSValue GetPrototype(this JSValue value)
- => value.Runtime.GetPrototype(Env, (napi_value)value, out napi_value result)
+ public static JSValue GetPrototype(this JSValue thisValue)
+ => thisValue.Runtime.GetPrototype(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
- public static JSValue GetPropertyNames(this JSValue value)
- => value.Runtime.GetPropertyNames(Env, (napi_value)value, out napi_value result)
+ public static JSValue GetPropertyNames(this JSValue thisValue)
+ => thisValue.Runtime.GetPropertyNames(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
public static void SetProperty(this JSValue thisValue, JSValue key, JSValue value)
{
- thisValue.Runtime.SetProperty(Env, (napi_value)thisValue, (napi_value)key, (napi_value)value)
+ thisValue.Runtime.SetProperty(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, key.Handle, value.Handle)
.ThrowIfFailed();
}
public static bool HasProperty(this JSValue thisValue, JSValue key)
- => thisValue.Runtime.HasProperty(Env, (napi_value)thisValue, (napi_value)key, out bool result)
+ => thisValue.Runtime.HasProperty(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, key.Handle, out bool result)
.ThrowIfFailed(result);
public static JSValue GetProperty(this JSValue thisValue, JSValue key)
- => thisValue.Runtime.GetProperty(Env, (napi_value)thisValue, (napi_value)key, out napi_value result)
+ => thisValue.Runtime.GetProperty(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, key.Handle, out napi_value result)
.ThrowIfFailed(result);
public static bool DeleteProperty(this JSValue thisValue, JSValue key)
- => thisValue.Runtime.DeleteProperty(Env, (napi_value)thisValue, (napi_value)key, out bool result)
+ => thisValue.Runtime.DeleteProperty(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, key.Handle, out bool result)
.ThrowIfFailed(result);
public static bool HasOwnProperty(this JSValue thisValue, JSValue key)
- => thisValue.Runtime.HasOwnProperty(Env, (napi_value)thisValue, (napi_value)key, out bool result)
+ => thisValue.Runtime.HasOwnProperty(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, key.Handle, out bool result)
.ThrowIfFailed(result);
public static void SetElement(this JSValue thisValue, int index, JSValue value)
{
- thisValue.Runtime.SetElement(Env, (napi_value)thisValue, (uint)index, (napi_value)value)
+ thisValue.Runtime.SetElement(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, (uint)index, value.Handle)
.ThrowIfFailed();
}
public static bool HasElement(this JSValue thisValue, int index)
- => thisValue.Runtime.HasElement(Env, (napi_value)thisValue, (uint)index, out bool result)
+ => thisValue.Runtime.HasElement(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, (uint)index, out bool result)
.ThrowIfFailed(result);
public static JSValue GetElement(this JSValue thisValue, int index)
- => thisValue.Runtime.GetElement(Env, (napi_value)thisValue, (uint)index, out napi_value result)
+ => thisValue.Runtime.GetElement(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, (uint)index, out napi_value result)
.ThrowIfFailed(result);
public static bool DeleteElement(this JSValue thisValue, int index)
- => thisValue.Runtime.DeleteElement(Env, (napi_value)thisValue, (uint)index, out bool result)
+ => thisValue.Runtime.DeleteElement(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, (uint)index, out bool result)
.ThrowIfFailed(result);
public static unsafe void DefineProperties(this JSValue thisValue, IReadOnlyCollection descriptors)
{
nint[] handles = ToUnmanagedPropertyDescriptors(string.Empty, descriptors, (_, descriptorsPtr) =>
- thisValue.Runtime.DefineProperties(Env, (napi_value)thisValue, descriptorsPtr)
+ thisValue.Runtime.DefineProperties(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, descriptorsPtr)
.ThrowIfFailed());
Array.ForEach(handles, handle => thisValue.AddGCHandleFinalizer(handle));
}
@@ -223,45 +233,50 @@ public static unsafe void DefineProperties(this JSValue thisValue, IReadOnlyColl
public static unsafe void DefineProperties(this JSValue thisValue, params JSPropertyDescriptor[] descriptors)
{
nint[] handles = ToUnmanagedPropertyDescriptors(string.Empty, descriptors, (_, descriptorsPtr) =>
- thisValue.Runtime.DefineProperties(Env, (napi_value)thisValue, descriptorsPtr)
+ thisValue.Runtime.DefineProperties(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, descriptorsPtr)
.ThrowIfFailed());
Array.ForEach(handles, handle => thisValue.AddGCHandleFinalizer(handle));
}
public static bool IsArray(this JSValue thisValue)
- => thisValue.Runtime.IsArray(Env, (napi_value)thisValue, out bool result)
+ => thisValue.Runtime.IsArray(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static int GetArrayLength(this JSValue thisValue)
- => thisValue.Runtime.GetArrayLength(Env, (napi_value)thisValue, out int result)
+ => thisValue.Runtime.GetArrayLength(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out int result)
.ThrowIfFailed(result);
// Internal because JSValue structs all implement IEquatable, which calls this method.
internal static bool StrictEquals(this JSValue thisValue, JSValue other)
- => thisValue.Runtime.StrictEquals(Env, (napi_value)thisValue, (napi_value)other, out bool result)
+ => thisValue.Runtime.StrictEquals(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, other.Handle, out bool result)
.ThrowIfFailed(result);
public static unsafe JSValue Call(this JSValue thisValue)
=> thisValue.Runtime.CallFunction(
- Env, (napi_value)JSValue.Undefined, (napi_value)thisValue, Array.Empty(), out napi_value result).ThrowIfFailed(result);
+ thisValue.UncheckedEnvironmentHandle, JSValue.Undefined.Handle, thisValue.Handle, Array.Empty(), out napi_value result).ThrowIfFailed(result);
public static unsafe JSValue Call(this JSValue thisValue, JSValue thisArg)
- => thisValue.Runtime.CallFunction(Env, (napi_value)thisArg, (napi_value)thisValue, Array.Empty(), out napi_value result).ThrowIfFailed(result);
+ => thisValue.Runtime.CallFunction(
+ thisValue.UncheckedEnvironmentHandle, thisArg.Handle, thisValue.Handle, Array.Empty(), out napi_value result).ThrowIfFailed(result);
public static unsafe JSValue Call(this JSValue thisValue, JSValue thisArg, JSValue arg0)
{
- Span args = stackalloc napi_value[] { (napi_value)arg0 };
+ Span args = stackalloc napi_value[] { arg0.Handle };
return thisValue.Runtime.CallFunction(
- Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisArg.Handle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
public static unsafe JSValue Call(
this JSValue thisValue, JSValue thisArg, JSValue arg0, JSValue arg1)
{
- Span args = stackalloc napi_value[] { (napi_value)arg0, (napi_value)arg1 };
+ Span args = stackalloc napi_value[] { arg0.Handle, arg1.Handle };
return thisValue.Runtime.CallFunction(
- Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisArg.Handle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
@@ -270,12 +285,12 @@ public static unsafe JSValue Call(
{
Span args = stackalloc napi_value[]
{
- (napi_value)arg0,
- (napi_value)arg1,
- (napi_value)arg2
+ arg0.Handle,
+ arg1.Handle,
+ arg2.Handle
};
return thisValue.Runtime.CallFunction(
- Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisArg.Handle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
@@ -290,13 +305,13 @@ public static unsafe JSValue Call(
Span argv = stackalloc napi_value[argc];
for (int i = 0; i < argc; ++i)
{
- argv[i] = (napi_value)args[i];
+ argv[i] = args[i].Handle;
}
return thisValue.Runtime.CallFunction(
- Env,
- (napi_value)thisArg,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisArg.Handle,
+ thisValue.Handle,
argv,
out napi_value result)
.ThrowIfFailed(result);
@@ -306,9 +321,9 @@ public static unsafe JSValue Call(
this JSValue thisValue, napi_value thisArg, ReadOnlySpan args)
{
return thisValue.Runtime.CallFunction(
- Env,
+ thisValue.UncheckedEnvironmentHandle,
thisArg,
- (napi_value)thisValue,
+ thisValue.Handle,
args,
out napi_value result)
.ThrowIfFailed(result);
@@ -316,22 +331,22 @@ public static unsafe JSValue Call(
public static unsafe JSValue CallAsConstructor(this JSValue thisValue)
=> thisValue.Runtime.NewInstance(
- Env, (napi_value)thisValue, [], out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, [], out napi_value result)
.ThrowIfFailed(result);
public static unsafe JSValue CallAsConstructor(this JSValue thisValue, JSValue arg0)
{
- napi_value argValue0 = (napi_value)arg0;
+ napi_value argValue0 = arg0.Handle;
Span args = stackalloc napi_value[1] { argValue0 };
- return thisValue.Runtime.NewInstance(Env, (napi_value)thisValue, args, out napi_value result)
+ return thisValue.Runtime.NewInstance(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
public static unsafe JSValue CallAsConstructor(
this JSValue thisValue, JSValue arg0, JSValue arg1)
{
- Span args = stackalloc napi_value[2] { (napi_value)arg0, (napi_value)arg1 };
- return thisValue.Runtime.NewInstance(Env, (napi_value)thisValue, args, out napi_value result)
+ Span args = stackalloc napi_value[2] { arg0.Handle, arg1.Handle };
+ return thisValue.Runtime.NewInstance(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
@@ -339,11 +354,11 @@ public static unsafe JSValue CallAsConstructor(
this JSValue thisValue, JSValue arg0, JSValue arg1, JSValue arg2)
{
Span args = stackalloc napi_value[3] {
- (napi_value)arg0,
- (napi_value)arg1,
- (napi_value)arg2
+ arg0.Handle,
+ arg1.Handle,
+ arg2.Handle
};
- return thisValue.Runtime.NewInstance(Env, (napi_value)thisValue, args, out napi_value result)
+ return thisValue.Runtime.NewInstance(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
@@ -357,11 +372,11 @@ public static unsafe JSValue CallAsConstructor(
Span argv = stackalloc napi_value[argc];
for (int i = 0; i < argc; ++i)
{
- argv[i] = (napi_value)args[i];
+ argv[i] = args[i].Handle;
}
return thisValue.Runtime.NewInstance(
- Env, (napi_value)thisValue, argv, out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, argv, out napi_value result)
.ThrowIfFailed(result);
}
@@ -369,7 +384,7 @@ public static unsafe JSValue CallAsConstructor(
this JSValue thisValue, ReadOnlySpan args)
{
return thisValue.Runtime.NewInstance(
- Env, (napi_value)thisValue, args, out napi_value result)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, args, out napi_value result)
.ThrowIfFailed(result);
}
@@ -397,10 +412,11 @@ public static JSValue CallMethod(
public static JSValue CallMethod(
this JSValue thisValue, JSValue methodName, ReadOnlySpan args)
- => thisValue.GetProperty(methodName).Call((napi_value)thisValue, args);
+ => thisValue.GetProperty(methodName).Call(thisValue.Handle, args);
public static bool InstanceOf(this JSValue thisValue, JSValue constructor)
- => thisValue.Runtime.InstanceOf(Env, (napi_value)thisValue, (napi_value)constructor, out bool result)
+ => thisValue.Runtime.InstanceOf(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, constructor.Handle, out bool result)
.ThrowIfFailed(result);
public static unsafe JSValue DefineClass(
@@ -409,8 +425,8 @@ public static unsafe JSValue DefineClass(
nint data,
ReadOnlySpan descriptors)
{
- return JSValueScope.CurrentRuntime.DefineClass(
- Env,
+ return CurrentRuntime.DefineClass(
+ CurrentEnvironmentHandle,
name,
callback,
data,
@@ -427,7 +443,7 @@ public static unsafe JSValue DefineClass(
GCHandle descriptorHandle = JSRuntimeContext.Current.AllocGCHandle(constructorDescriptor);
JSValue? func = null;
napi_callback callback = new(
- JSValueScope.Current?.ScopeType == JSValueScopeType.NoContext
+ Current?.ScopeType == JSValueScopeType.NoContext
? s_invokeJSCallbackNC : s_invokeJSCallback);
nint[] handles = ToUnmanagedPropertyDescriptors(
@@ -449,13 +465,13 @@ public static unsafe JSValue DefineClass(
/// The JS wrapper.
public static unsafe JSValue Wrap(this JSValue wrapper, object value)
{
- GCHandle valueHandle = JSRuntimeContext.Current.AllocGCHandle(value);
+ GCHandle valueHandle = wrapper.Scope.RuntimeContext.AllocGCHandle(value);
wrapper.Runtime.Wrap(
- Env,
- (napi_value)wrapper,
+ wrapper.UncheckedEnvironmentHandle,
+ wrapper.Handle,
(nint)valueHandle,
new napi_finalize(s_finalizeGCHandle),
- default,
+ wrapper.Scope.RuntimeContextHandle,
out _).ThrowIfFailed();
return wrapper;
}
@@ -471,13 +487,13 @@ public static unsafe JSValue Wrap(this JSValue wrapper, object value)
public static unsafe JSValue Wrap(
this JSValue wrapper, object value, out JSReference wrapperWeakRef)
{
- GCHandle valueHandle = JSRuntimeContext.Current.AllocGCHandle(value);
+ GCHandle valueHandle = wrapper.Scope.RuntimeContext.AllocGCHandle(value);
wrapper.Runtime.Wrap(
- Env,
- (napi_value)wrapper,
+ wrapper.UncheckedEnvironmentHandle,
+ wrapper.Handle,
(nint)valueHandle,
new napi_finalize(s_finalizeGCHandle),
- default,
+ wrapper.Scope.RuntimeContextHandle,
out napi_ref weakRef).ThrowIfFailed();
wrapperWeakRef = new JSReference(weakRef, isWeak: true);
return wrapper;
@@ -491,7 +507,7 @@ public static unsafe JSValue Wrap(
/// True if a wrapped object was found and returned, else false.
public static bool TryUnwrap(this JSValue thisValue, out object? value)
{
- napi_status status = thisValue.Runtime.Unwrap(Env, (napi_value)thisValue, out nint result);
+ napi_status status = thisValue.Runtime.Unwrap(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result);
// The invalid arg error code is returned if there was nothing to unwrap. It doesn't
// distinguish from an invalid handle, but either way the unwrap failed.
@@ -513,7 +529,7 @@ public static bool TryUnwrap(this JSValue thisValue, out object? value)
/// The unwrapped object, or null if nothing was wrapped.
public static object? TryUnwrap(this JSValue thisValue)
{
- napi_status status = thisValue.Runtime.Unwrap(Env, (napi_value)thisValue, out nint result);
+ napi_status status = thisValue.Runtime.Unwrap(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result);
// The invalid arg error code is returned if there was nothing to unwrap. It doesn't
// distinguish from an invalid handle, but either way the unwrap failed.
@@ -532,7 +548,7 @@ public static bool TryUnwrap(this JSValue thisValue, out object? value)
///
public static object Unwrap(this JSValue thisValue, string? unwrapType = null)
{
- napi_status status = thisValue.Runtime.Unwrap(Env, (napi_value)thisValue, out nint result);
+ napi_status status = thisValue.Runtime.Unwrap(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result);
if (status == napi_status.napi_invalid_arg && unwrapType != null)
{
@@ -551,7 +567,7 @@ public static object Unwrap(this JSValue thisValue, string? unwrapType = null)
/// True if a wrapped object was found and removed, else false.
public static bool RemoveWrap(this JSValue thisValue, out object? value)
{
- napi_status status = thisValue.Runtime.RemoveWrap(Env, (napi_value)thisValue, out nint result);
+ napi_status status = thisValue.Runtime.RemoveWrap(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result);
// The invalid arg error code is returned if there was nothing to remove.
if (status == napi_status.napi_invalid_arg)
@@ -571,7 +587,7 @@ public static bool RemoveWrap(this JSValue thisValue, out object? value)
///
public static unsafe object GetValueExternal(this JSValue thisValue)
{
- thisValue.Runtime.GetValueExternal(Env, (napi_value)thisValue, out nint result)
+ thisValue.Runtime.GetValueExternal(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result)
.ThrowIfFailed();
return GCHandle.FromIntPtr(result).Target!;
}
@@ -583,7 +599,7 @@ public static unsafe object GetValueExternal(this JSValue thisValue)
public static unsafe object? TryGetValueExternal(this JSValue thisValue)
{
napi_status status = thisValue.Runtime.GetValueExternal(
- Env, (napi_value)thisValue, out nint result);
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint result);
// The invalid arg error code is returned if there was no external value.
if (status == napi_status.napi_invalid_arg)
@@ -603,36 +619,39 @@ public static JSReference CreateWeakReference(this JSValue thisValue)
public static bool IsError(this JSValue thisValue)
=> thisValue.Runtime.IsError(
- Env, (napi_value)thisValue, out bool result).ThrowIfFailed(result);
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result).ThrowIfFailed(result);
public static bool IsExceptionPending()
- => JSValueScope.CurrentRuntime.IsExceptionPending(Env, out bool result).ThrowIfFailed(result);
+ => CurrentRuntime.IsExceptionPending(
+ CurrentEnvironmentHandle, out bool result).ThrowIfFailed(result);
public static JSValue GetAndClearLastException()
- => JSValueScope.CurrentRuntime.GetAndClearLastException(Env, out napi_value result).ThrowIfFailed(result);
+ => CurrentRuntime.GetAndClearLastException(
+ CurrentEnvironmentHandle, out napi_value result).ThrowIfFailed(result);
public static bool IsArrayBuffer(this JSValue thisValue)
=> thisValue.Runtime.IsArrayBuffer(
- Env, (napi_value)thisValue, out bool result).ThrowIfFailed(result);
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result).ThrowIfFailed(result);
public static unsafe Span GetArrayBufferInfo(this JSValue thisValue)
{
- thisValue.Runtime.GetArrayBufferInfo(Env, (napi_value)thisValue, out nint data, out nuint length)
+ thisValue.Runtime.GetArrayBufferInfo(
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out nint data, out nuint length)
.ThrowIfFailed();
return new Span((void*)data, (int)length);
}
public static bool IsTypedArray(this JSValue thisValue)
=> thisValue.Runtime.IsTypedArray(
- Env, (napi_value)thisValue, out bool result).ThrowIfFailed(result);
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result).ThrowIfFailed(result);
public static unsafe int GetTypedArrayLength(
this JSValue thisValue,
out JSTypedArrayType type)
{
thisValue.Runtime.GetTypedArrayInfo(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
out napi_typedarray_type arrayType,
out nuint length,
out nint _,
@@ -646,8 +665,8 @@ public static unsafe Span GetTypedArrayData(
this JSValue thisValue) where T : struct
{
thisValue.Runtime.GetTypedArrayInfo(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
out napi_typedarray_type arrayType,
out nuint length,
out nint data,
@@ -684,8 +703,8 @@ public static unsafe void GetTypedArrayBuffer(
out int byteOffset)
{
thisValue.Runtime.GetTypedArrayInfo(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
out napi_typedarray_type type_,
out nuint length_,
out nint _,
@@ -698,7 +717,7 @@ public static unsafe void GetTypedArrayBuffer(
}
public static bool IsDataView(this JSValue thisValue)
- => thisValue.Runtime.IsDataView(Env, (napi_value)thisValue, out bool result)
+ => thisValue.Runtime.IsDataView(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static unsafe void GetDataViewInfo(
@@ -708,8 +727,8 @@ public static unsafe void GetDataViewInfo(
out int byteOffset)
{
thisValue.Runtime.GetDataViewInfo(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
out nuint byteLength,
out nint data,
out napi_value arrayBuffer_,
@@ -720,46 +739,49 @@ public static unsafe void GetDataViewInfo(
}
public static uint GetVersion()
- => JSValueScope.CurrentRuntime.GetVersion(Env, out uint result).ThrowIfFailed(result);
+ => CurrentRuntime.GetVersion(
+ CurrentEnvironmentHandle, out uint result).ThrowIfFailed(result);
public static bool IsPromise(this JSValue thisValue)
- => thisValue.Runtime.IsPromise(Env, (napi_value)thisValue, out bool result)
+ => thisValue.Runtime.IsPromise(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static JSValue RunScript(this JSValue thisValue)
- => thisValue.Runtime.RunScript(Env, (napi_value)thisValue, out napi_value result)
+ => thisValue.Runtime.RunScript(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out napi_value result)
.ThrowIfFailed(result);
public static bool IsDate(this JSValue thisValue)
- => thisValue.Runtime.IsDate(Env, (napi_value)thisValue, out bool result)
+ => thisValue.Runtime.IsDate(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static double GetDateValue(this JSValue thisValue)
- => thisValue.Runtime.GetValueDate(Env, (napi_value)thisValue, out double result)
+ => thisValue.Runtime.GetValueDate(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out double result)
.ThrowIfFailed(result);
public static unsafe void AddFinalizer(this JSValue thisValue, Action finalize)
{
- GCHandle finalizeHandle = JSRuntimeContext.Current.AllocGCHandle(finalize);
+ JSValueScope currentScope = thisValue.Scope;
+ GCHandle finalizeHandle = currentScope.RuntimeContext.AllocGCHandle(finalize);
thisValue.Runtime.AddFinalizer(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
(nint)finalizeHandle,
new napi_finalize(s_callFinalizeAction),
- default,
+ currentScope.RuntimeContextHandle,
out _).ThrowIfFailed();
}
public static unsafe void AddFinalizer(
this JSValue thisValue, Action finalize, out JSReference finalizerRef)
{
- GCHandle finalizeHandle = JSRuntimeContext.Current.AllocGCHandle(finalize);
+ JSValueScope currentScope = thisValue.Scope;
+ GCHandle finalizeHandle = currentScope.RuntimeContext.AllocGCHandle(finalize);
thisValue.Runtime.AddFinalizer(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
(nint)finalizeHandle,
new napi_finalize(s_callFinalizeAction),
- default,
+ currentScope.RuntimeContextHandle,
out napi_ref reference).ThrowIfFailed();
finalizerRef = new JSReference(reference, isWeak: true);
}
@@ -767,7 +789,7 @@ public static unsafe void AddFinalizer(
public static long GetValueBigIntInt64(this JSValue thisValue, out bool isLossless)
{
thisValue.Runtime.GetValueBigInt64(
- Env, (napi_value)thisValue, out long result, out bool lossless).ThrowIfFailed();
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out long result, out bool lossless).ThrowIfFailed();
isLossless = lossless;
return result;
}
@@ -775,7 +797,7 @@ public static long GetValueBigIntInt64(this JSValue thisValue, out bool isLossle
public static ulong GetValueBigIntUInt64(this JSValue thisValue, out bool isLossless)
{
thisValue.Runtime.GetValueBigInt64(
- Env, (napi_value)thisValue, out ulong result, out bool lossless).ThrowIfFailed();
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out ulong result, out bool lossless).ThrowIfFailed();
isLossless = lossless;
return result;
}
@@ -783,11 +805,11 @@ public static ulong GetValueBigIntUInt64(this JSValue thisValue, out bool isLoss
public static unsafe ulong[] GetValueBigIntWords(this JSValue thisValue, out int signBit)
{
thisValue.Runtime.GetValueBigInt(
- Env, (napi_value)thisValue, out _, [], out nuint wordCount)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out _, [], out nuint wordCount)
.ThrowIfFailed();
ulong[] words = new ulong[wordCount];
thisValue.Runtime.GetValueBigInt(
- Env, (napi_value)thisValue, out signBit, words.AsSpan(), out _)
+ thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out signBit, words.AsSpan(), out _)
.ThrowIfFailed();
return words;
}
@@ -799,8 +821,8 @@ public static JSValue GetAllPropertyNames(
JSKeyConversion conversion)
{
return thisValue.Runtime.GetAllPropertyNames(
- Env,
- (napi_value)thisValue,
+ thisValue.UncheckedEnvironmentHandle,
+ thisValue.Handle,
(napi_key_collection_mode)mode,
(napi_key_filter)filter,
(napi_key_conversion)conversion,
@@ -809,7 +831,7 @@ public static JSValue GetAllPropertyNames(
internal static unsafe void SetInstanceData(napi_env env, object? data)
{
- JSValueScope.CurrentRuntime.GetInstanceData(env, out nint handlePtr).ThrowIfFailed();
+ CurrentRuntime.GetInstanceData(env, out nint handlePtr).ThrowIfFailed();
if (handlePtr != default)
{
// Current napi_set_instance_data implementation does not call finalizer when we replace existing instance data.
@@ -820,42 +842,40 @@ internal static unsafe void SetInstanceData(napi_env env, object? data)
if (data != null)
{
GCHandle handle = GCHandle.Alloc(data);
- JSValueScope.CurrentRuntime.SetInstanceData(
+ CurrentRuntime.SetInstanceData(
env,
(nint)handle,
- new napi_finalize(s_finalizeGCHandle),
- DisposeHint).ThrowIfFailed();
+ new napi_finalize(s_finalizeGCHandleToDisposable),
+ finalizeHint: default).ThrowIfFailed();
}
}
internal static object? GetInstanceData(napi_env env)
{
- JSValueScope.CurrentRuntime.GetInstanceData(env, out nint data).ThrowIfFailed();
+ CurrentRuntime.GetInstanceData(env, out nint data).ThrowIfFailed();
return (data != default) ? GCHandle.FromIntPtr(data).Target : null;
}
public static void DetachArrayBuffer(this JSValue thisValue)
- => thisValue.Runtime.DetachArrayBuffer(Env, (napi_value)thisValue).ThrowIfFailed();
+ => thisValue.Runtime.DetachArrayBuffer(thisValue.UncheckedEnvironmentHandle, thisValue.Handle).ThrowIfFailed();
public static bool IsDetachedArrayBuffer(this JSValue thisValue)
- => thisValue.Runtime.IsDetachedArrayBuffer(Env, (napi_value)thisValue, out bool result)
+ => thisValue.Runtime.IsDetachedArrayBuffer(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, out bool result)
.ThrowIfFailed(result);
public static void SetObjectTypeTag(this JSValue thisValue, Guid typeTag)
- => thisValue.Runtime.SetObjectTypeTag(Env, (napi_value)thisValue, typeTag)
+ => thisValue.Runtime.SetObjectTypeTag(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, typeTag)
.ThrowIfFailed();
public static bool CheckObjectTypeTag(this JSValue thisValue, Guid typeTag)
- => thisValue.Runtime.CheckObjectTypeTag(Env, (napi_value)thisValue, typeTag, out bool result)
+ => thisValue.Runtime.CheckObjectTypeTag(thisValue.UncheckedEnvironmentHandle, thisValue.Handle, typeTag, out bool result)
.ThrowIfFailed(result);
public static void Freeze(this JSValue thisValue)
- => thisValue.Runtime.Freeze(Env, (napi_value)thisValue).ThrowIfFailed();
+ => thisValue.Runtime.Freeze(thisValue.UncheckedEnvironmentHandle, thisValue.Handle).ThrowIfFailed();
public static void Seal(this JSValue thisValue)
- => thisValue.Runtime.Seal(Env, (napi_value)thisValue).ThrowIfFailed();
-
- private static napi_env Env => (napi_env)JSValueScope.Current;
+ => thisValue.Runtime.Seal(thisValue.UncheckedEnvironmentHandle, thisValue.Handle).ThrowIfFailed();
#if NETFRAMEWORK
internal static readonly napi_callback.Delegate s_invokeJSCallback = InvokeJSCallback;
@@ -868,7 +888,8 @@ public static void Seal(this JSValue thisValue)
internal static readonly napi_callback.Delegate s_invokeJSSetterNC = InvokeJSSetterNoContext;
internal static readonly napi_finalize.Delegate s_finalizeGCHandle = FinalizeGCHandle;
- internal static readonly napi_finalize.Delegate s_finalizeHintHandle = FinalizeHintHandle;
+ internal static readonly napi_finalize.Delegate s_finalizeGCHandleToDisposable = FinalizeGCHandleToDisposable;
+ internal static readonly napi_finalize.Delegate s_finalizeGCHandleToPinnedMemory = FinalizeGCHandleToPinnedMemory;
internal static readonly napi_finalize.Delegate s_callFinalizeAction = CallFinalizeAction;
#else
internal static readonly unsafe delegate* unmanaged[Cdecl]
@@ -891,7 +912,9 @@ internal static readonly unsafe delegate* unmanaged[Cdecl]
internal static readonly unsafe delegate* unmanaged[Cdecl]
s_finalizeGCHandle = &FinalizeGCHandle;
internal static readonly unsafe delegate* unmanaged[Cdecl]
- s_finalizeHintHandle = &FinalizeHintHandle;
+ s_finalizeGCHandleToDisposable = &FinalizeGCHandleToDisposable;
+ internal static readonly unsafe delegate* unmanaged[Cdecl]
+ s_finalizeGCHandleToPinnedMemory = &FinalizeGCHandleToPinnedMemory;
internal static readonly unsafe delegate* unmanaged[Cdecl]
s_callFinalizeAction = &CallFinalizeAction;
#endif
@@ -984,7 +1007,7 @@ private static unsafe napi_value InvokeCallback(
JSValueScopeType scopeType,
Func getCallbackDescriptor)
{
- using var scope = new JSValueScope(scopeType);
+ using var scope = new JSValueScope(scopeType, env, runtime: default);
try
{
JSCallbackArgs.GetDataAndLength(scope, callbackInfo, out object? data, out int length);
@@ -1005,27 +1028,64 @@ private static unsafe napi_value InvokeCallback(
internal static unsafe void FinalizeGCHandle(napi_env env, nint data, nint hint)
{
GCHandle handle = GCHandle.FromIntPtr(data);
+ if (hint != default)
+ {
+ GCHandle contextHandle = GCHandle.FromIntPtr(hint);
+ JSRuntimeContext context = (JSRuntimeContext)contextHandle.Target!;
+ context.FreeGCHandle(handle);
+ }
+ else
+ {
+ handle.Free();
+ }
+ }
- if (hint == DisposeHint)
+ [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
+ internal static unsafe void FinalizeGCHandleToDisposable(napi_env env, nint data, nint hint)
+ {
+ GCHandle handle = GCHandle.FromIntPtr(data);
+ try
{
(handle.Target as IDisposable)?.Dispose();
}
-
- JSRuntimeContext.FreeGCHandle(handle, env);
+ finally
+ {
+ if (hint != default)
+ {
+ GCHandle contextHandle = GCHandle.FromIntPtr(hint);
+ JSRuntimeContext context = (JSRuntimeContext)contextHandle.Target!;
+ context.FreeGCHandle(handle);
+ }
+ else
+ {
+ handle.Free();
+ }
+ }
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
- internal static unsafe void FinalizeHintHandle(napi_env env, nint _2, nint hint)
+ internal static unsafe void FinalizeGCHandleToPinnedMemory(napi_env env, nint data, nint hint)
{
+ // The GC handle is passed via the hint parameter.
+ // (The data parameter is the pointer to raw memory.)
GCHandle handle = GCHandle.FromIntPtr(hint);
- (handle.Target as IDisposable)?.Dispose();
- JSRuntimeContext.FreeGCHandle(handle, env);
+ PinnedMemory pinnedMemory = (PinnedMemory)handle.Target!;
+ try
+ {
+ pinnedMemory.Dispose();
+ }
+ finally
+ {
+ pinnedMemory.RuntimeContext.FreeGCHandle(handle);
+ }
}
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })]
private static unsafe void CallFinalizeAction(napi_env env, nint data, nint hint)
{
GCHandle gcHandle = GCHandle.FromIntPtr(data);
+ GCHandle contextHandle = GCHandle.FromIntPtr(hint);
+ JSRuntimeContext context = (JSRuntimeContext)contextHandle.Target!;
try
{
// TODO: [vmoroz] In future we will be not allowed to run JS in finalizers.
@@ -1035,7 +1095,7 @@ private static unsafe void CallFinalizeAction(napi_env env, nint data, nint hint
}
finally
{
- JSRuntimeContext.FreeGCHandle(gcHandle, env);
+ context.FreeGCHandle(gcHandle);
}
}
@@ -1096,24 +1156,25 @@ private static unsafe nint[] ToUnmanagedPropertyDescriptors(
private unsafe delegate void UseUnmanagedDescriptors(
string name, ReadOnlySpan descriptors);
- internal sealed class PinnedMemory : IDisposable where T : struct
+ internal abstract class PinnedMemory : IDisposable
{
private bool _disposed = false;
- private readonly Memory _memory;
private MemoryHandle _memoryHandle;
- public object? Owner { get; private set; }
-
- public PinnedMemory(Memory memory, object? owner)
+ protected PinnedMemory(MemoryHandle memoryHandle, object? owner)
{
+ _memoryHandle = memoryHandle;
Owner = owner;
- _memory = memory;
- _memoryHandle = _memory.Pin();
+ RuntimeContext = JSRuntimeContext.Current;
}
+ public abstract int Length { get; }
+
+ public object? Owner { get; private set; }
+
public unsafe void* Pointer => _memoryHandle.Pointer;
- public int Length => _memory.Length * Unsafe.SizeOf();
+ public JSRuntimeContext RuntimeContext { get; }
public void Dispose()
{
@@ -1124,5 +1185,18 @@ public void Dispose()
Owner = null;
}
}
+
+ }
+
+ internal sealed class PinnedMemory : PinnedMemory where T : struct
+ {
+ private readonly Memory _memory;
+
+ public PinnedMemory(Memory memory, object? owner) : base(memory.Pin(), owner)
+ {
+ _memory = memory;
+ }
+
+ public override int Length => _memory.Length * Unsafe.SizeOf();
}
}
diff --git a/src/NodeApi/Runtime/JSRuntime.cs b/src/NodeApi/Runtime/JSRuntime.cs
index 96740c4c..c0712338 100644
--- a/src/NodeApi/Runtime/JSRuntime.cs
+++ b/src/NodeApi/Runtime/JSRuntime.cs
@@ -41,7 +41,7 @@ public abstract partial class JSRuntime
private static NotSupportedException NS([CallerMemberName] string name = "")
=> new($"The {name} method is not supported by the current JS runtime.");
- public abstract bool IsAvailable(string functionName);
+ public virtual bool IsAvailable(string functionName) => true;
public virtual napi_status GetVersion(napi_env env, out uint result) => throw NS();
@@ -66,8 +66,8 @@ public virtual napi_status GetInstanceData(
public virtual napi_status SetInstanceData(
napi_env env,
nint data,
- napi_finalize finalize_cb,
- nint finalize_hint) => throw NS();
+ napi_finalize finalizeCallback,
+ nint finalizeHint) => throw NS();
#endregion
diff --git a/src/NodeApi/Runtime/NodejsRuntime.JS.cs b/src/NodeApi/Runtime/NodejsRuntime.JS.cs
index 53a5c81f..24b433d2 100644
--- a/src/NodeApi/Runtime/NodejsRuntime.JS.cs
+++ b/src/NodeApi/Runtime/NodejsRuntime.JS.cs
@@ -91,10 +91,10 @@ public override napi_status GetInstanceData(napi_env env, out nint result)
public override napi_status SetInstanceData(
napi_env env,
nint data,
- napi_finalize finalize_cb,
- nint finalize_hint)
+ napi_finalize finalizeCallback,
+ nint finalizeHint)
{
- return Import(ref napi_set_instance_data)(env, data, finalize_cb, finalize_hint);
+ return Import(ref napi_set_instance_data)(env, data, finalizeCallback, finalizeHint);
}
#endregion
diff --git a/test/JSReferenceTests.cs b/test/JSReferenceTests.cs
new file mode 100644
index 00000000..db8bc5aa
--- /dev/null
+++ b/test/JSReferenceTests.cs
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading.Tasks;
+using Xunit;
+using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
+
+namespace Microsoft.JavaScript.NodeApi.Test;
+
+public class JSReferenceTests
+{
+ private readonly MockJSRuntime _mockRuntime = new();
+
+ private JSValueScope TestScope(JSValueScopeType scopeType)
+ {
+ napi_env env = new(Environment.CurrentManagedThreadId);
+ return new(scopeType, env, _mockRuntime, new MockJSRuntime.SynchronizationContext());
+ }
+
+ [Fact]
+ public void GetReferenceFromSameScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ JSValue value = JSValue.CreateObject();
+ JSReference reference = new(value);
+ Assert.True(reference.GetValue()?.IsObject() ?? false);
+ }
+
+ [Fact]
+ public void GetReferenceFromParentScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ JSReference reference;
+ using (JSValueScope handleScope = new(JSValueScopeType.Handle))
+ {
+ JSValue value = JSValue.CreateObject();
+ reference = new JSReference(value);
+ }
+
+ Assert.True(reference.GetValue()?.IsObject() ?? false);
+ }
+
+ [Fact]
+ public void GetReferenceFromDifferentThread()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ JSValue value = JSValue.CreateObject();
+ JSReference reference = new(value);
+
+ // Run in a new thread which will not have any current scope.
+ Task.Run(() =>
+ {
+ Assert.Throws(() => reference.GetValue());
+ }).Wait();
+ }
+
+ [Fact]
+ public void GetReferenceFromDifferentRootScope()
+ {
+ using JSValueScope rootScope1 = TestScope(JSValueScopeType.Root);
+
+ JSValue value = JSValue.CreateObject();
+ JSReference reference = new(value);
+
+ // Run in a new thread and establish another root scope there.
+ Task.Run(() =>
+ {
+ using JSValueScope rootScope2 = TestScope(JSValueScopeType.Root);
+ Assert.Throws(() => reference.GetValue());
+ }).Wait();
+ }
+}
diff --git a/test/JSValueScopeTests.cs b/test/JSValueScopeTests.cs
new file mode 100644
index 00000000..9e914693
--- /dev/null
+++ b/test/JSValueScopeTests.cs
@@ -0,0 +1,373 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime;
+
+namespace Microsoft.JavaScript.NodeApi.Test;
+
+///
+/// Unit tests for . Validates that scopes can be initialized and nested
+/// with intended limitations, and that values can be used only within the scope (and thread)
+/// with which they were created.
+///
+public class JSValueScopeTests
+{
+ private readonly MockJSRuntime _mockRuntime = new();
+
+ private JSValueScope TestScope(JSValueScopeType scopeType)
+ {
+ napi_env env = new(Environment.CurrentManagedThreadId);
+ return new(scopeType, env, _mockRuntime, new MockJSRuntime.SynchronizationContext());
+ }
+
+ [Fact]
+ public void CreateNoContextScope()
+ {
+ using JSValueScope noContextScope = TestScope(JSValueScopeType.NoContext);
+ Assert.Null(noContextScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.NoContext, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateRootScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+ Assert.NotNull(rootScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateModuleScopeWithinNoContextScope()
+ {
+ using JSValueScope noContextScope = TestScope(JSValueScopeType.NoContext);
+
+ using (JSValueScope moduleScope = TestScope(JSValueScopeType.Module))
+ {
+ Assert.NotNull(moduleScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.NoContext, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateModuleScopeWithinRootScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ using (JSValueScope moduleScope = new(JSValueScopeType.Module))
+ {
+ Assert.NotNull(moduleScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateModuleScopeWithoutRoot()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+ Assert.NotNull(moduleScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateCallbackScope()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+
+ using (JSValueScope callbackScope = new(JSValueScopeType.Callback))
+ {
+ Assert.NotNull(moduleScope.RuntimeContext);
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateHandleScopeWithinRoot()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ using (JSValueScope handleScope = new(JSValueScopeType.Handle))
+ {
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateHandleScopeWithinModule()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+
+ using (JSValueScope handleScope = new(JSValueScopeType.Handle))
+ {
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateHandleScopeWithinCallback()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+
+ using (JSValueScope callbackScope = new(JSValueScopeType.Callback))
+ {
+ using (JSValueScope handleScope = new(JSValueScopeType.Handle))
+ {
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void CreateEscapableScopeWithinCallback()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+
+ using (JSValueScope callbackScope = new(JSValueScopeType.Callback))
+ {
+ using (JSValueScope escapableScope = new(JSValueScopeType.Escapable))
+ {
+ Assert.Equal(JSValueScopeType.Escapable, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+ }
+
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void InvalidNoContextScopeNesting()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+ Assert.Throws(() =>
+ {
+ using JSValueScope noContextScope = new(JSValueScopeType.NoContext);
+ });
+ Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType);
+
+ using JSValueScope moduleScope = new(JSValueScopeType.Module);
+ Assert.Throws(() =>
+ {
+ using JSValueScope noContextScope = new(JSValueScopeType.NoContext);
+ });
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+
+ using JSValueScope callbackScope = new(JSValueScopeType.Callback);
+ Assert.Throws(() =>
+ {
+ using JSValueScope noContextScope = new(JSValueScopeType.NoContext);
+ });
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+
+ using JSValueScope handleScope = new(JSValueScopeType.Handle);
+ Assert.Throws(() =>
+ {
+ using JSValueScope noContextScope = new(JSValueScopeType.NoContext);
+ });
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+
+ using JSValueScope escapableScope = new(JSValueScopeType.Escapable);
+ Assert.Throws(() =>
+ {
+ using JSValueScope noContextScope = new(JSValueScopeType.NoContext);
+ });
+ Assert.Equal(JSValueScopeType.Escapable, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void InvalidRootContextScopeNesting()
+ {
+ using JSValueScope noContextScope = TestScope(JSValueScopeType.NoContext);
+ Assert.Throws(() =>
+ {
+ using JSValueScope rootScope = new(JSValueScopeType.Root);
+ });
+ Assert.Equal(JSValueScopeType.NoContext, JSValueScope.Current.ScopeType);
+
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+ Assert.Throws(() =>
+ {
+ using JSValueScope rootScope = new(JSValueScopeType.Root);
+ });
+ Assert.Equal(JSValueScopeType.Module, JSValueScope.Current.ScopeType);
+
+ using JSValueScope callbackScope = new(JSValueScopeType.Callback);
+ Assert.Throws(() =>
+ {
+ using JSValueScope rootScope = new(JSValueScopeType.Root);
+ });
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+
+ using JSValueScope handleScope = new(JSValueScopeType.Handle);
+ Assert.Throws(() =>
+ {
+ using JSValueScope rootScope = new(JSValueScopeType.Root);
+ });
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+
+ using JSValueScope escapableScope = new(JSValueScopeType.Escapable);
+ Assert.Throws(() =>
+ {
+ using JSValueScope rootScope = new(JSValueScopeType.Root);
+ });
+ Assert.Equal(JSValueScopeType.Escapable, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void InvalidModuleContextScopeNesting()
+ {
+ using JSValueScope moduleScope = TestScope(JSValueScopeType.Module);
+ using JSValueScope callbackScope = new(JSValueScopeType.Callback);
+ Assert.Throws(() =>
+ {
+ using JSValueScope nestedModuleScope = new(JSValueScopeType.Module);
+ });
+ Assert.Equal(JSValueScopeType.Callback, JSValueScope.Current.ScopeType);
+
+ using JSValueScope handleScope = new(JSValueScopeType.Handle);
+ Assert.Throws(() =>
+ {
+ using JSValueScope nestedModuleScope = new(JSValueScopeType.Module);
+ });
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+
+ using JSValueScope escapableScope = new(JSValueScopeType.Escapable);
+ Assert.Throws(() =>
+ {
+ using JSValueScope nestedModuleScope = new(JSValueScopeType.Module);
+ });
+ Assert.Equal(JSValueScopeType.Escapable, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void InvalidCallbackContextScopeNesting()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ using JSValueScope handleScope = new(JSValueScopeType.Handle);
+ Assert.Throws(() =>
+ {
+ using JSValueScope callbackScope = new(JSValueScopeType.Callback);
+ });
+ Assert.Equal(JSValueScopeType.Handle, JSValueScope.Current.ScopeType);
+
+ using JSValueScope escapableScope = new(JSValueScopeType.Escapable);
+ Assert.Throws(() =>
+ {
+ using JSValueScope callbackScope = new(JSValueScopeType.Callback);
+ });
+ Assert.Equal(JSValueScopeType.Escapable, JSValueScope.Current.ScopeType);
+ }
+
+ [Fact]
+ public void AccessValueFromClosedScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ JSValueScope handleScope;
+ JSValue objectValue;
+ using (handleScope = new(JSValueScopeType.Handle))
+ {
+ objectValue = JSValue.CreateObject();
+ Assert.True(objectValue.IsObject());
+ }
+
+ Assert.True(handleScope.IsDisposed);
+ JSValueScopeClosedException ex = Assert.Throws(
+ () => objectValue.IsObject());
+ Assert.Equal(handleScope, ex.Scope);
+ }
+
+ [Fact]
+ public void AccessPropertyKeyFromClosedScope()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ JSValue objectValue = JSValue.CreateObject();
+ JSValue propertyKey;
+
+ JSValueScope handleScope;
+ using (handleScope = new(JSValueScopeType.Handle))
+ {
+ propertyKey = "test";
+ Assert.True(propertyKey.IsString());
+ }
+
+ // The property key scope was closed so it's not valid to use as a method argument.
+ Assert.True(handleScope.IsDisposed);
+ JSValueScopeClosedException ex = Assert.Throws(
+ () => objectValue[propertyKey]);
+ Assert.Equal(handleScope, ex.Scope);
+
+ // The object value scope was not closed so it's still valid.
+ Assert.True(objectValue.IsObject());
+ }
+
+ [Fact]
+ public void CreateValueFromDifferentThread()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+
+ // Run in a new thread which will not have any current scope.
+ Task.Run(() =>
+ {
+ Assert.Throws(() => JSValueScope.Current);
+ JSInvalidThreadAccessException ex = Assert.Throws(
+ () => new JSObject());
+ Assert.Null(ex.CurrentScope);
+ Assert.Null(ex.TargetScope);
+ }).Wait();
+ }
+
+ [Fact]
+ public void AccessValueFromDifferentThread()
+ {
+ using JSValueScope rootScope = TestScope(JSValueScopeType.Root);
+ JSValue objectValue = JSValue.CreateObject();
+
+ // Run in a new thread which will not have any current scope.
+ Task.Run(() =>
+ {
+ Assert.Throws(() => JSValueScope.Current);
+ JSInvalidThreadAccessException ex = Assert.Throws(
+ () => objectValue.IsObject());
+ Assert.Null(ex.CurrentScope);
+ Assert.Equal(rootScope, ex.TargetScope);
+ }).Wait();
+ }
+
+ [Fact]
+ public void AccessValueFromDifferentRootScope()
+ {
+ using JSValueScope rootScope1 = TestScope(JSValueScopeType.Root);
+ JSValue objectValue = JSValue.CreateObject();
+
+ // Run in a new thread and establish another root scope there.
+ Task.Run(() =>
+ {
+ using JSValueScope rootScope2 = TestScope(JSValueScopeType.Root);
+ Assert.Equal(JSValueScopeType.Root, JSValueScope.Current.ScopeType);
+ JSInvalidThreadAccessException ex = Assert.Throws(
+ () => objectValue.IsObject());
+ Assert.Equal(rootScope2, ex.CurrentScope);
+ Assert.Equal(rootScope1, ex.TargetScope);
+ }).Wait();
+ }
+}
diff --git a/test/MockJSRuntime.cs b/test/MockJSRuntime.cs
new file mode 100644
index 00000000..f4f738bc
--- /dev/null
+++ b/test/MockJSRuntime.cs
@@ -0,0 +1,198 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.JavaScript.NodeApi.Interop;
+using Microsoft.JavaScript.NodeApi.Runtime;
+using Xunit;
+using static Microsoft.JavaScript.NodeApi.Runtime.JSRuntime.napi_status;
+
+namespace Microsoft.JavaScript.NodeApi.Test;
+
+///
+/// Mocks just enough JS runtime behavior to support unit-testing the library API
+/// layer above the JS runtime.
+///
+internal class MockJSRuntime : JSRuntime
+{
+ private static nint s_handleCounter = 0;
+
+ private nint _instanceData;
+ private readonly List _handleScopes = new();
+ private readonly List _escapableScopes = new();
+ private readonly Dictionary _values = new();
+ private readonly Dictionary _references = new();
+
+ private class MockJSValue
+ {
+ public napi_valuetype ValueType { get; init; }
+ public object? Value { get; set; }
+ }
+
+ private class MockJSRef
+ {
+ public nint ValueHandle { get; set; }
+ public uint RefCount { get; set; }
+ }
+
+ public override napi_status GetInstanceData(
+ napi_env env, out nint result)
+ {
+ result = _instanceData;
+ return napi_ok;
+ }
+
+ public override napi_status SetInstanceData(
+ napi_env env, nint data, napi_finalize finalize_cb, nint finalize_hint)
+ {
+ _instanceData = data;
+ return napi_ok;
+ }
+
+ public override napi_status OpenHandleScope(
+ napi_env env, out napi_handle_scope result)
+ {
+ nint scope = ++s_handleCounter;
+ _handleScopes.Add(scope);
+ result = new napi_handle_scope(scope);
+ return napi_ok;
+ }
+
+ public override napi_status CloseHandleScope(
+ napi_env env, napi_handle_scope scope)
+ {
+ Assert.True(_handleScopes.Remove(scope.Handle));
+ return napi_ok;
+ }
+
+ public override napi_status OpenEscapableHandleScope(
+ napi_env env, out napi_escapable_handle_scope result)
+ {
+ nint scope = ++s_handleCounter;
+ _escapableScopes.Add(scope);
+ result = new napi_escapable_handle_scope(scope);
+ return napi_ok;
+ }
+
+ public override napi_status CloseEscapableHandleScope(
+ napi_env env, napi_escapable_handle_scope scope)
+ {
+ Assert.True(_escapableScopes.Remove(scope.Handle));
+ return napi_ok;
+ }
+
+ public override napi_status CreateString(
+ napi_env env, ReadOnlySpan utf16Str, out napi_value result)
+ {
+ nint handle = ++s_handleCounter;
+ _values.Add(handle, new MockJSValue
+ {
+ ValueType = napi_valuetype.napi_string,
+ Value = utf16Str.ToString(),
+ });
+ result = new napi_value(handle);
+ return napi_ok;
+ }
+
+ public override napi_status CreateObject(
+ napi_env env, out napi_value result)
+ {
+ nint handle = ++s_handleCounter;
+ _values.Add(handle, new MockJSValue { ValueType = napi_valuetype.napi_object });
+ result = new napi_value(handle);
+ return napi_ok;
+ }
+
+ public override napi_status GetValueType(
+ napi_env env, napi_value value, out napi_valuetype result)
+ {
+ if (_values.TryGetValue(value.Handle, out MockJSValue? mockValue))
+ {
+ result = mockValue.ValueType;
+ return napi_ok;
+ }
+ else
+ {
+ result = default;
+ return napi_invalid_arg;
+ }
+ }
+
+ public override napi_status CreateReference(
+ napi_env env, napi_value value, uint initialRefcount, out napi_ref result)
+ {
+ nint handle = ++s_handleCounter;
+ _references.Add(handle, new MockJSRef
+ {
+ ValueHandle = value.Handle,
+ RefCount = initialRefcount,
+ });
+ result = new napi_ref(handle);
+ return napi_ok;
+ }
+
+ public override napi_status GetReferenceValue(
+ napi_env env, napi_ref @ref, out napi_value result)
+ {
+ if (_references.TryGetValue(@ref.Handle, out MockJSRef? mockRef))
+ {
+ result = new napi_value(mockRef.ValueHandle);
+ return napi_ok;
+ }
+ else
+ {
+ result = default;
+ return napi_invalid_arg;
+ }
+ }
+
+ public override napi_status RefReference(
+ napi_env env, napi_ref @ref, out uint result)
+ {
+ if (_references.TryGetValue(@ref.Handle, out MockJSRef? mockRef))
+ {
+ result = ++mockRef.RefCount;
+ return napi_ok;
+ }
+ else
+ {
+ result = default;
+ return napi_invalid_arg;
+ }
+ }
+
+ public override napi_status UnrefReference(
+ napi_env env, napi_ref @ref, out uint result)
+ {
+ if (_references.TryGetValue(@ref.Handle, out MockJSRef? mockRef))
+ {
+ result = --mockRef.RefCount;
+ if (result == 0)
+ {
+ _references.Remove(@ref.Handle);
+ }
+
+ return napi_ok;
+ }
+ else
+ {
+ result = default;
+ return napi_invalid_arg;
+ }
+ }
+
+ public override napi_status DeleteReference(napi_env env, napi_ref @ref)
+ {
+ return _references.Remove(@ref.Handle) ? napi_ok : napi_invalid_arg;
+ }
+
+ // Mocking the sync context prevents the runtime mock from having to implement APIs
+ // to support initializing the thread-safe-function for the sync context.
+ // Unit tests that use the mock runtime don't currently use the sync context.
+ public class SynchronizationContext : JSSynchronizationContext
+ {
+ public override void CloseAsyncScope() => throw new NotImplementedException();
+ public override void OpenAsyncScope() => throw new NotImplementedException();
+ }
+}
diff --git a/test/NodejsEmbeddingTests.cs b/test/NodejsEmbeddingTests.cs
index 70a24503..347c81af 100644
--- a/test/NodejsEmbeddingTests.cs
+++ b/test/NodejsEmbeddingTests.cs
@@ -4,7 +4,6 @@
using System;
using System.IO;
using System.Linq;
-using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.JavaScript.NodeApi.Runtime;
using Xunit;
@@ -20,11 +19,22 @@ public class NodejsEmbeddingTests
internal static NodejsPlatform? NodejsPlatform { get; } =
File.Exists(LibnodePath) ? new(LibnodePath, args: new[] { "node", "--expose-gc" }) : null;
+ internal static NodejsEnvironment CreateNodejsEnvironment()
+ {
+ Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath);
+ return NodejsPlatform.CreateEnvironment();
+ }
+
+ internal static void RunInNodejsEnvironment(Action action)
+ {
+ using NodejsEnvironment nodejs = CreateNodejsEnvironment();
+ nodejs.SynchronizationContext.Run(action);
+ }
+
[SkippableFact]
public void NodejsStart()
{
- Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath);
- using NodejsEnvironment nodejs = NodejsPlatform.CreateEnvironment();
+ using NodejsEnvironment nodejs = CreateNodejsEnvironment();
nodejs.SynchronizationContext.Run(() =>
{
@@ -39,8 +49,7 @@ public void NodejsStart()
[SkippableFact]
public void NodejsCallFunction()
{
- Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath);
- using NodejsEnvironment nodejs = NodejsPlatform.CreateEnvironment();
+ using NodejsEnvironment nodejs = CreateNodejsEnvironment();
nodejs.SynchronizationContext.Run(() =>
{
@@ -55,8 +64,7 @@ public void NodejsCallFunction()
[SkippableFact]
public void NodejsUnhandledRejection()
{
- Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath);
- using NodejsEnvironment nodejs = NodejsPlatform.CreateEnvironment();
+ using NodejsEnvironment nodejs = CreateNodejsEnvironment();
string? errorMessage = null;
nodejs.UnhandledPromiseRejection += (_, e) =>
@@ -78,8 +86,7 @@ public void NodejsUnhandledRejection()
[SkippableFact]
public void NodejsErrorPropagation()
{
- Skip.If(NodejsPlatform == null, "Node shared library not found at " + LibnodePath);
- using NodejsEnvironment nodejs = NodejsPlatform.CreateEnvironment();
+ using NodejsEnvironment nodejs = CreateNodejsEnvironment();
string? exceptionMessage = null;
string? exceptionStack = null;