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;