From 7696d864e2698108021a364c6accc1e24045473a Mon Sep 17 00:00:00 2001 From: Jason Ginchereau Date: Fri, 10 Nov 2023 17:57:34 -1000 Subject: [PATCH] Handle scope validation tests --- src/NodeApi.DotNetHost/ManagedHost.cs | 5 +- src/NodeApi.Generator/ModuleGenerator.cs | 15 +- src/NodeApi.Generator/Program.cs | 9 +- src/NodeApi/DotNetHost/NativeHost.cs | 19 +- src/NodeApi/Interop/JSRuntimeContext.cs | 29 +- .../Interop/JSSynchronizationContext.cs | 25 +- src/NodeApi/Interop/JSThreadSafeFunction.cs | 7 +- src/NodeApi/JSException.cs | 2 +- src/NodeApi/JSInvalidScopeException.cs | 87 ++++ src/NodeApi/JSProxy.cs | 2 +- src/NodeApi/JSReference.cs | 53 ++- src/NodeApi/JSValue.cs | 117 +++--- src/NodeApi/JSValueScope.cs | 158 ++++++-- src/NodeApi/JSValueScopeClosedException.cs | 33 ++ src/NodeApi/Native/JSNativeApi.cs | 372 ++++++++++-------- src/NodeApi/Runtime/JSRuntime.cs | 6 +- src/NodeApi/Runtime/NodejsRuntime.JS.cs | 6 +- test/JSReferenceTests.cs | 76 ++++ test/JSValueScopeTests.cs | 348 ++++++++++++++++ test/MockJSRuntime.cs | 185 +++++++++ test/NodejsEmbeddingTests.cs | 25 +- 21 files changed, 1264 insertions(+), 315 deletions(-) create mode 100644 src/NodeApi/JSInvalidScopeException.cs create mode 100644 src/NodeApi/JSValueScopeClosedException.cs create mode 100644 test/JSReferenceTests.cs create mode 100644 test/JSValueScopeTests.cs create mode 100644 test/MockJSRuntime.cs 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 c3339eaf..7586cb15 100644 --- a/src/NodeApi.Generator/ModuleGenerator.cs +++ b/src/NodeApi.Generator/ModuleGenerator.cs @@ -280,6 +280,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}\")]"; @@ -291,11 +295,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) @@ -325,15 +329,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.Generator/Program.cs b/src/NodeApi.Generator/Program.cs index 896f09be..6aa33262 100644 --- a/src/NodeApi.Generator/Program.cs +++ b/src/NodeApi.Generator/Program.cs @@ -281,17 +281,20 @@ private static IEnumerable SplitWithQuotes(string line) { StringBuilder s = new(); bool inQuotes = false; + bool foundQuotes = false; foreach (char c in line) { if (c == '"') { inQuotes = !inQuotes; + foundQuotes = true; } else if (c == ' ' && !inQuotes) { - if (s.Length > 0) + if (s.Length > 0 || foundQuotes) { yield return s.ToString(); + foundQuotes = false; s.Clear(); } } @@ -301,7 +304,7 @@ private static IEnumerable SplitWithQuotes(string line) } } - if (s.Length > 0) + if (s.Length > 0 || foundQuotes) { yield return s.ToString(); } @@ -344,7 +347,7 @@ private static void ResolveSystemAssemblies( { if (targetingPacks.Count == 0) { - // If no targeting packs were specified, use the deafult targeting pack for .NET. + // If no targeting packs were specified, use the default targeting pack for .NET. targetingPacks.Add("Microsoft.NETCore.App"); } 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..f6b273eb 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; @@ -118,13 +119,19 @@ 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) { _env = env; + Runtime = runtime; SetInstanceData(env, this); - SynchronizationContext = JSSynchronizationContext.Create(); + SynchronizationContext = synchronizationContext ?? JSSynchronizationContext.Create(); } /// @@ -692,22 +699,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..9359f3df 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/JSInvalidScopeException.cs b/src/NodeApi/JSInvalidScopeException.cs new file mode 100644 index 00000000..58c9e272 --- /dev/null +++ b/src/NodeApi/JSInvalidScopeException.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 JSInvalidScopeException : InvalidOperationException +{ + /// + /// Creates a new instance of with a + /// current scope and message. + /// + public JSInvalidScopeException( + JSValueScope? currentScope, + string? message = null) + : this(currentScope, targetScope: null, message) + { + } + + /// + /// Creates a new instance of with current + /// and target scopes and a message. + /// + public JSInvalidScopeException( + 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; internal set; } + + /// + /// 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; internal set; } + + 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 {nameof(JSSynchronizationContext)} 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 {nameof(JSSynchronizationContext)} 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..58d27ba4 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; @@ -76,29 +77,39 @@ public static bool TryCreateReference( /// public JSSynchronizationContext? SynchronizationContext => _context?.SynchronizationContext; + private napi_env Env + { + get + { + CheckDisposed(); + CheckThreadAccess(); + return _env; + } + } + public void MakeWeak() { - ThrowIfDisposed(); + CheckDisposed(); if (!IsWeak) { - JSValueScope.CurrentRuntime.UnrefReference(_env, _handle, out _).ThrowIfFailed(); + JSValueScope.CurrentRuntime.UnrefReference(Env, _handle, out _).ThrowIfFailed(); IsWeak = true; } } public void MakeStrong() { - ThrowIfDisposed(); + CheckDisposed(); if (IsWeak) { - JSValueScope.CurrentRuntime.RefReference(_env, _handle, out _).ThrowIfFailed(); + JSValueScope.CurrentRuntime.RefReference(Env, _handle, out _).ThrowIfFailed(); IsWeak = true; } } public JSValue? GetValue() { - ThrowIfDisposed(); - JSValueScope.CurrentRuntime.GetReferenceValue(_env, _handle, out napi_value result) + CheckDisposed(); + JSValueScope.CurrentRuntime.GetReferenceValue(Env, _handle, out napi_value result) .ThrowIfFailed(); return result; } @@ -165,7 +176,7 @@ T GetValueAndRunAction() public bool IsDisposed { get; private set; } - private void ThrowIfDisposed() + private void CheckDisposed() { if (IsDisposed) { @@ -173,6 +184,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 CheckThreadAccess() + { + 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 {nameof(JSSynchronizationContext)} to switch to the JS thread."; + throw new JSInvalidScopeException(currentScope, message); + } + } + /// /// Releases the reference. /// @@ -192,14 +225,14 @@ protected virtual void Dispose(bool disposing) // 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(); } else { - SynchronizationContext.Post( - () => JSValueScope.CurrentRuntime.DeleteReference( + _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..877fc518 100644 --- a/src/NodeApi/JSValue.cs +++ b/src/NodeApi/JSValue.cs @@ -4,7 +4,6 @@ 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; @@ -21,12 +20,21 @@ namespace Microsoft.JavaScript.NodeApi; internal JSRuntime Runtime => Scope.Runtime; + /// + /// Creates an empty instance of , which implicitly converts to + /// when used in any scope. + /// public JSValue() { } - public JSValue(napi_value handle) : this(handle, JSValueScope.Current) - { - } - + /// + /// Creates a new instance of from a handle in the specified scope. + /// + /// Thrown when the scope is null (unless the handle + /// is also 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, JSValueScope? scope) { if (!handle.IsNull && scope is null) throw new ArgumentNullException(nameof(scope)); @@ -34,25 +42,34 @@ public JSValue(napi_value handle, JSValueScope? scope) _scope = scope; } - public napi_value? Handle - => !Scope.IsDisposed ? (_handle.Handle != default(nint) ? _handle : Undefined._handle) : null; + public napi_value Handle => _handle.Handle != default ? _handle : Undefined._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, JSValueScope.Current); + public static implicit operator JSValue?(napi_value handle) => handle.Handle != default ? new(handle, JSValueScope.Current) : 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 current instance. + /// + internal napi_env Env => (napi_env)Scope; + + /// + /// Gets the environment handle for the current thread scope. For use only in static methods; + /// for instance methods use instead. + /// + internal static napi_env CurrentEnv => (napi_env)JSValueScope.Current; public static JSValue Undefined - => JSValueScope.CurrentRuntime.GetUndefined(Env, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.GetUndefined(CurrentEnv, out napi_value result).ThrowIfFailed(result); public static JSValue Null - => JSValueScope.CurrentRuntime.GetNull(Env, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.GetNull(CurrentEnv, out napi_value result).ThrowIfFailed(result); public static JSValue Global - => JSValueScope.CurrentRuntime.GetGlobal(Env, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.GetGlobal(CurrentEnv, 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); + => JSValueScope.CurrentRuntime.GetBoolean(CurrentEnv, value, out napi_value result).ThrowIfFailed(result); public JSObject Properties => (JSObject)this; @@ -77,38 +94,38 @@ public JSValue this[int index] } public static JSValue CreateObject() - => JSValueScope.CurrentRuntime.CreateObject(Env, out napi_value result) + => JSValueScope.CurrentRuntime.CreateObject(CurrentEnv, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateArray() - => JSValueScope.CurrentRuntime.CreateArray(Env, out napi_value result) + => JSValueScope.CurrentRuntime.CreateArray(CurrentEnv, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateArray(int length) - => JSValueScope.CurrentRuntime.CreateArray(Env, length, out napi_value result) + => JSValueScope.CurrentRuntime.CreateArray(CurrentEnv, length, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateNumber(double value) - => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result) + => JSValueScope.CurrentRuntime.CreateNumber(CurrentEnv, value, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateNumber(int value) - => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result) + => JSValueScope.CurrentRuntime.CreateNumber(CurrentEnv, value, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateNumber(uint value) - => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result) + => JSValueScope.CurrentRuntime.CreateNumber(CurrentEnv, value, out napi_value result) .ThrowIfFailed(result); public static JSValue CreateNumber(long value) - => JSValueScope.CurrentRuntime.CreateNumber(Env, value, out napi_value result) + => JSValueScope.CurrentRuntime.CreateNumber(CurrentEnv, 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 JSValueScope.CurrentRuntime.CreateString(CurrentEnv, value, out napi_value result) .ThrowIfFailed(result); } } @@ -117,7 +134,7 @@ public static unsafe JSValue CreateStringUtf16(ReadOnlySpan value) { fixed (char* spanPtr = value) { - return JSValueScope.CurrentRuntime.CreateString(Env, value, out napi_value result) + return JSValueScope.CurrentRuntime.CreateString(CurrentEnv, value, out napi_value result) .ThrowIfFailed(result); } } @@ -126,18 +143,18 @@ public static unsafe JSValue CreateStringUtf16(string value) { fixed (char* spanPtr = value) { - return JSValueScope.CurrentRuntime.CreateString(Env, value.AsSpan(), out napi_value result) + return JSValueScope.CurrentRuntime.CreateString(CurrentEnv, 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); + CurrentEnv, (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 JSValueScope.CurrentRuntime.GetSymbolFor(CurrentEnv, name, out napi_value result) .ThrowIfFailed(result); } @@ -147,7 +164,7 @@ public static JSValue CreateFunction( nint data) { return JSValueScope.CurrentRuntime.CreateFunction( - Env, name, callback, data, out napi_value result) + CurrentEnv, name, callback, data, out napi_value result) .ThrowIfFailed(result); } @@ -167,43 +184,44 @@ public static unsafe JSValue CreateFunction( } public static JSValue CreateError(JSValue? code, JSValue message) - => JSValueScope.CurrentRuntime.CreateError(Env, (napi_value)code, (napi_value)message, + => JSValueScope.CurrentRuntime.CreateError(CurrentEnv, (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, + => JSValueScope.CurrentRuntime.CreateTypeError(CurrentEnv, (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, + => JSValueScope.CurrentRuntime.CreateRangeError(CurrentEnv, (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, + => JSValueScope.CurrentRuntime.CreateSyntaxError(CurrentEnv, (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); + JSValueScope currentScope = JSValueScope.Current; + GCHandle valueHandle = currentScope.RuntimeContext.AllocGCHandle(value); return JSValueScope.CurrentRuntime.CreateExternal( - Env, + CurrentEnv, (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) + JSValueScope.CurrentRuntime.CreateArrayBuffer(CurrentEnv, 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) + JSValueScope.CurrentRuntime.CreateArrayBuffer(CurrentEnv, data.Length, out nint buffer, out napi_value result) .ThrowIfFailed(); data.CopyTo(new Span((void*)buffer, data.Length)); return result; @@ -214,25 +232,25 @@ public static unsafe JSValue CreateExternalArrayBuffer( { var pinnedMemory = new PinnedMemory(memory, external); return JSValueScope.CurrentRuntime.CreateArrayBuffer( - Env, + CurrentEnv, (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) + CurrentEnv, 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, + CurrentEnv, (napi_typedarray_type)type, length, (napi_value)arrayBuffer, @@ -242,24 +260,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) + JSValueScope.CurrentRuntime.CreatePromise(CurrentEnv, 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); + => JSValueScope.CurrentRuntime.CreateDate(CurrentEnv, time, out napi_value result).ThrowIfFailed(result); public static JSValue CreateBigInt(long value) - => JSValueScope.CurrentRuntime.CreateBigInt(Env, value, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.CreateBigInt(CurrentEnv, value, out napi_value result).ThrowIfFailed(result); public static JSValue CreateBigInt(ulong value) - => JSValueScope.CurrentRuntime.CreateBigInt(Env, value, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.CreateBigInt(CurrentEnv, 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 JSValueScope.CurrentRuntime.CreateBigInt(CurrentEnv, signBit, words, out napi_value result) .ThrowIfFailed(result); } @@ -319,11 +337,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..a2bc996b 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; @@ -82,25 +82,50 @@ 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 JSInvalidScopeException(currentScope: null); + + 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; @@ -110,7 +135,8 @@ public JSValueScope( _parentScope = s_currentScope; if (_parentScope != null && _parentScope.ScopeType != JSValueScopeType.NoContext) { - throw new InvalidOperationException( + throw new JSInvalidScopeException( + currentScope: s_currentScope, "A NoContext scope cannot be created within another type of scope."); } @@ -125,6 +151,7 @@ public JSValueScope( _parentScope = null; _env = env; + ThreadId = Environment.CurrentManagedThreadId; Runtime = runtime; } else if (scopeType == JSValueScopeType.Root) @@ -140,8 +167,9 @@ public JSValueScope( } else { - throw new InvalidOperationException( - "A Root scope cannot be created within another scope."); + throw new JSInvalidScopeException( + currentScope: s_currentScope, + $"A Root scope cannot be created within another scope."); } } @@ -157,11 +185,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 +204,52 @@ 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 JSInvalidScopeException( + currentScope: null, + $"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 JSInvalidScopeException( + currentScope: s_currentScope, "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 JSInvalidScopeException( + currentScope: s_currentScope, + $"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.CheckThreadAccess(); _env = _parentScope._env; + ThreadId = _parentScope.ThreadId; Runtime = _parentScope.Runtime; } @@ -210,7 +257,8 @@ public JSValueScope( { if (_parentScope?.ModuleContext != null) { - throw new InvalidOperationException("Module scope cannot be nested."); + throw new JSInvalidScopeException( + currentScope: s_currentScope, "Module scope cannot be nested."); } ModuleContext = new JSModuleContext(); @@ -238,8 +286,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) { @@ -278,9 +342,9 @@ public void Dispose() SynchronizationContext.SetSynchronizationContext(_previousSyncContext); break; } - - s_currentScope = _parentScope; } + + s_currentScope = _parentScope; } public JSValue Escape(JSValue value) @@ -300,9 +364,37 @@ public JSValue Escape(JSValue value) return new JSValue(result, _parentScope); } + /// + /// Checks that this scope has not been closed (disposed). + /// + /// The scope is closed. + private void CheckDisposed() + { + 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. + private void CheckThreadAccess() + { + if (s_currentScope?._env != _env) + { + throw new JSInvalidScopeException(currentScope: s_currentScope, targetScope: this); + } + } + public static explicit operator napi_env(JSValueScope scope) { if (scope is null) throw new ArgumentNullException(nameof(scope)); + scope.CheckDisposed(); + scope.CheckThreadAccess(); return scope!._env; } } diff --git a/src/NodeApi/JSValueScopeClosedException.cs b/src/NodeApi/JSValueScopeClosedException.cs new file mode 100644 index 00000000..9abbc649 --- /dev/null +++ b/src/NodeApi/JSValueScopeClosedException.cs @@ -0,0 +1,33 @@ +// 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 the scope are no longer available after their scope is " + + "closed. Consider using an escapable scope to promote a value to the parent scope."; + } +} diff --git a/src/NodeApi/Native/JSNativeApi.cs b/src/NodeApi/Native/JSNativeApi.cs index 621fc55a..1132e12c 100644 --- a/src/NodeApi/Native/JSNativeApi.cs +++ b/src/NodeApi/Native/JSNativeApi.cs @@ -6,7 +6,6 @@ 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.Runtime.JSRuntime; @@ -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, + thisValue.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, [], out int result) .ThrowIfFailed(result); } return thisValue.Runtime.GetValueStringUtf8( - Env, (napi_value)thisValue, buffer, out int result2) + thisValue.Env, (napi_value)thisValue, 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,96 @@ public static unsafe int GetValueStringUtf16(this JSValue thisValue, Span if (buffer.IsEmpty) { return thisValue.Runtime.GetValueStringUtf16( - Env, (napi_value)thisValue, [], out int result) + thisValue.Env, (napi_value)thisValue, [], out int result) .ThrowIfFailed(result); } return thisValue.Runtime.GetValueStringUtf16( - Env, (napi_value)thisValue, buffer, out int result2) + thisValue.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, (napi_value)key, (napi_value)value) .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.Env, (napi_value)thisValue, (napi_value)key, 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.Env, (napi_value)thisValue, (napi_value)key, 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.Env, (napi_value)thisValue, (napi_value)key, 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.Env, (napi_value)thisValue, (napi_value)key, 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.Env, (napi_value)thisValue, (uint)index, (napi_value)value) .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.Env, (napi_value)thisValue, (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.Env, (napi_value)thisValue, (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.Env, (napi_value)thisValue, (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.Env, (napi_value)thisValue, descriptorsPtr) .ThrowIfFailed()); Array.ForEach(handles, handle => thisValue.AddGCHandleFinalizer(handle)); } @@ -223,36 +217,36 @@ 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, (napi_value)other, 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.Env, (napi_value)JSValue.Undefined, (napi_value)thisValue, 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.Env, (napi_value)thisArg, (napi_value)thisValue, 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 }; return thisValue.Runtime.CallFunction( - Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) + thisValue.Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -261,7 +255,7 @@ public static unsafe JSValue Call( { Span args = stackalloc napi_value[] { (napi_value)arg0, (napi_value)arg1 }; return thisValue.Runtime.CallFunction( - Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) + thisValue.Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -275,7 +269,7 @@ public static unsafe JSValue Call( (napi_value)arg2 }; return thisValue.Runtime.CallFunction( - Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) + thisValue.Env, (napi_value)thisArg, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -294,7 +288,7 @@ public static unsafe JSValue Call( } return thisValue.Runtime.CallFunction( - Env, + thisValue.Env, (napi_value)thisArg, (napi_value)thisValue, argv, @@ -306,7 +300,7 @@ public static unsafe JSValue Call( this JSValue thisValue, napi_value thisArg, ReadOnlySpan args) { return thisValue.Runtime.CallFunction( - Env, + thisValue.Env, thisArg, (napi_value)thisValue, args, @@ -316,14 +310,14 @@ 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.Env, (napi_value)thisValue, [], out napi_value result) .ThrowIfFailed(result); public static unsafe JSValue CallAsConstructor(this JSValue thisValue, JSValue arg0) { napi_value argValue0 = (napi_value)arg0; 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.Env, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -331,7 +325,7 @@ 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) + return thisValue.Runtime.NewInstance(thisValue.Env, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -343,7 +337,7 @@ public static unsafe JSValue CallAsConstructor( (napi_value)arg1, (napi_value)arg2 }; - return thisValue.Runtime.NewInstance(Env, (napi_value)thisValue, args, out napi_value result) + return thisValue.Runtime.NewInstance(thisValue.Env, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -361,7 +355,7 @@ public static unsafe JSValue CallAsConstructor( } return thisValue.Runtime.NewInstance( - Env, (napi_value)thisValue, argv, out napi_value result) + thisValue.Env, (napi_value)thisValue, argv, out napi_value result) .ThrowIfFailed(result); } @@ -369,7 +363,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.Env, (napi_value)thisValue, args, out napi_value result) .ThrowIfFailed(result); } @@ -400,7 +394,7 @@ public static JSValue CallMethod( => thisValue.GetProperty(methodName).Call((napi_value)thisValue, 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.Env, (napi_value)thisValue, (napi_value)constructor, out bool result) .ThrowIfFailed(result); public static unsafe JSValue DefineClass( @@ -410,7 +404,7 @@ public static unsafe JSValue DefineClass( ReadOnlySpan descriptors) { return JSValueScope.CurrentRuntime.DefineClass( - Env, + JSValue.CurrentEnv, name, callback, data, @@ -449,13 +443,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, + wrapper.Env, (napi_value)wrapper, (nint)valueHandle, new napi_finalize(s_finalizeGCHandle), - default, + wrapper.Scope.RuntimeContextHandle, out _).ThrowIfFailed(); return wrapper; } @@ -471,13 +465,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, + wrapper.Env, (napi_value)wrapper, (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 +485,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.Env, (napi_value)thisValue, 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 +507,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.Env, (napi_value)thisValue, 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 +526,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.Env, (napi_value)thisValue, out nint result); if (status == napi_status.napi_invalid_arg && unwrapType != null) { @@ -551,7 +545,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.Env, (napi_value)thisValue, 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 +565,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.Env, (napi_value)thisValue, out nint result) .ThrowIfFailed(); return GCHandle.FromIntPtr(result).Target!; } @@ -583,7 +577,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.Env, (napi_value)thisValue, out nint result); // The invalid arg error code is returned if there was no external value. if (status == napi_status.napi_invalid_arg) @@ -603,35 +597,38 @@ 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.Env, (napi_value)thisValue, out bool result).ThrowIfFailed(result); public static bool IsExceptionPending() - => JSValueScope.CurrentRuntime.IsExceptionPending(Env, out bool result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.IsExceptionPending( + JSValue.CurrentEnv, out bool result).ThrowIfFailed(result); public static JSValue GetAndClearLastException() - => JSValueScope.CurrentRuntime.GetAndClearLastException(Env, out napi_value result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.GetAndClearLastException( + JSValue.CurrentEnv, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, out bool result).ThrowIfFailed(result); public static unsafe int GetTypedArrayLength( this JSValue thisValue, out JSTypedArrayType type) { thisValue.Runtime.GetTypedArrayInfo( - Env, + thisValue.Env, (napi_value)thisValue, out napi_typedarray_type arrayType, out nuint length, @@ -646,7 +643,7 @@ public static unsafe Span GetTypedArrayData( this JSValue thisValue) where T : struct { thisValue.Runtime.GetTypedArrayInfo( - Env, + thisValue.Env, (napi_value)thisValue, out napi_typedarray_type arrayType, out nuint length, @@ -684,7 +681,7 @@ public static unsafe void GetTypedArrayBuffer( out int byteOffset) { thisValue.Runtime.GetTypedArrayInfo( - Env, + thisValue.Env, (napi_value)thisValue, out napi_typedarray_type type_, out nuint length_, @@ -698,7 +695,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.Env, (napi_value)thisValue, out bool result) .ThrowIfFailed(result); public static unsafe void GetDataViewInfo( @@ -708,7 +705,7 @@ public static unsafe void GetDataViewInfo( out int byteOffset) { thisValue.Runtime.GetDataViewInfo( - Env, + thisValue.Env, (napi_value)thisValue, out nuint byteLength, out nint data, @@ -720,46 +717,49 @@ public static unsafe void GetDataViewInfo( } public static uint GetVersion() - => JSValueScope.CurrentRuntime.GetVersion(Env, out uint result).ThrowIfFailed(result); + => JSValueScope.CurrentRuntime.GetVersion( + JSValue.CurrentEnv, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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, + thisValue.Env, (napi_value)thisValue, (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, + thisValue.Env, (napi_value)thisValue, (nint)finalizeHandle, new napi_finalize(s_callFinalizeAction), - default, + currentScope.RuntimeContextHandle, out napi_ref reference).ThrowIfFailed(); finalizerRef = new JSReference(reference, isWeak: true); } @@ -767,7 +767,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.Env, (napi_value)thisValue, out long result, out bool lossless).ThrowIfFailed(); isLossless = lossless; return result; } @@ -775,7 +775,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.Env, (napi_value)thisValue, out ulong result, out bool lossless).ThrowIfFailed(); isLossless = lossless; return result; } @@ -783,11 +783,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.Env, (napi_value)thisValue, out _, [], out nuint wordCount) .ThrowIfFailed(); ulong[] words = new ulong[wordCount]; thisValue.Runtime.GetValueBigInt( - Env, (napi_value)thisValue, out signBit, words.AsSpan(), out _) + thisValue.Env, (napi_value)thisValue, out signBit, words.AsSpan(), out _) .ThrowIfFailed(); return words; } @@ -799,7 +799,7 @@ public static JSValue GetAllPropertyNames( JSKeyConversion conversion) { return thisValue.Runtime.GetAllPropertyNames( - Env, + thisValue.Env, (napi_value)thisValue, (napi_key_collection_mode)mode, (napi_key_filter)filter, @@ -823,8 +823,8 @@ internal static unsafe void SetInstanceData(napi_env env, object? data) JSValueScope.CurrentRuntime.SetInstanceData( env, (nint)handle, - new napi_finalize(s_finalizeGCHandle), - DisposeHint).ThrowIfFailed(); + new napi_finalize(s_finalizeGCHandleToDisposable), + finalizeHint: default).ThrowIfFailed(); } } @@ -835,27 +835,25 @@ internal static unsafe void SetInstanceData(napi_env env, object? data) } public static void DetachArrayBuffer(this JSValue thisValue) - => thisValue.Runtime.DetachArrayBuffer(Env, (napi_value)thisValue).ThrowIfFailed(); + => thisValue.Runtime.DetachArrayBuffer(thisValue.Env, (napi_value)thisValue).ThrowIfFailed(); public static bool IsDetachedArrayBuffer(this JSValue thisValue) - => thisValue.Runtime.IsDetachedArrayBuffer(Env, (napi_value)thisValue, out bool result) + => thisValue.Runtime.IsDetachedArrayBuffer(thisValue.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue, 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.Env, (napi_value)thisValue).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.Env, (napi_value)thisValue).ThrowIfFailed(); #if NETFRAMEWORK internal static readonly napi_callback.Delegate s_invokeJSCallback = InvokeJSCallback; @@ -868,7 +866,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 +890,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 +985,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 +1006,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 +1073,7 @@ private static unsafe void CallFinalizeAction(napi_env env, nint data, nint hint } finally { - JSRuntimeContext.FreeGCHandle(gcHandle, env); + context.FreeGCHandle(gcHandle); } } @@ -1096,24 +1134,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 +1163,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..d55d1314 --- /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..755aad41 --- /dev/null +++ b/test/JSValueScopeTests.cs @@ -0,0 +1,348 @@ +// 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 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); + JSInvalidScopeException 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); + JSInvalidScopeException 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); + JSInvalidScopeException 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..8067c590 --- /dev/null +++ b/test/MockJSRuntime.cs @@ -0,0 +1,185 @@ +// 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 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;