// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ClearScript.JavaScript;
using Microsoft.ClearScript.Util;
using Microsoft.ClearScript.V8.FastProxy;
using Newtonsoft.Json;
namespace Microsoft.ClearScript.V8
{
// ReSharper disable once PartialTypeWithSinglePart
///
/// Represents an instance of the V8 JavaScript engine.
///
///
/// Unlike WindowsScriptEngine instances, V8ScriptEngine instances do not have
/// thread affinity. The underlying script engine is not thread-safe, however, so this class
/// uses internal locks to automatically serialize all script code execution for a given
/// instance. Script delegates and event handlers are invoked on the calling thread without
/// marshaling.
///
public sealed partial class V8ScriptEngine : ScriptEngine, IJavaScriptEngine
{
#region data
private static readonly DocumentInfo initScriptInfo = new(MiscHelpers.FormatInvariant("{0} [internal]", nameof(V8ScriptEngine)));
private readonly V8Runtime runtime;
private readonly bool usingPrivateRuntime;
private readonly V8ContextProxy proxy;
private readonly V8ScriptItem script;
private readonly InterlockedOneWayFlag disposedFlag = new();
private const int continuationInterval = 2000;
private bool inContinuationTimerScope;
private bool? awaitDebuggerAndPause;
private List documentNames;
private bool suppressInstanceMethodEnumeration;
private bool suppressExtensionMethodEnumeration;
private CommonJSManager commonJSManager;
private JsonModuleManager jsonDocumentManager;
#endregion
#region constructors
///
/// Initializes a new V8 script engine instance.
///
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine()
: this(null, null)
{
}
///
/// Initializes a new V8 script engine instance with the specified name.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name)
: this(name, null)
{
}
///
/// Initializes a new V8 script engine instance with the specified resource constraints.
///
/// Resource constraints for the V8 runtime (see remarks).
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(V8RuntimeConstraints constraints)
: this(null, constraints)
{
}
///
/// Initializes a new V8 script engine instance with the specified name and resource constraints.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// Resource constraints for the V8 runtime (see remarks).
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name, V8RuntimeConstraints constraints)
: this(name, constraints, V8ScriptEngineFlags.None)
{
}
///
/// Initializes a new V8 script engine instance with the specified options.
///
/// A value that selects options for the operation.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(V8ScriptEngineFlags flags)
: this(flags, 0)
{
}
///
/// Initializes a new V8 script engine instance with the specified options and debug port.
///
/// A value that selects options for the operation.
/// A TCP port on which to listen for a debugger connection.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(V8ScriptEngineFlags flags, int debugPort)
: this(null, null, flags, debugPort)
{
}
///
/// Initializes a new V8 script engine instance with the specified name and options.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// A value that selects options for the operation.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name, V8ScriptEngineFlags flags)
: this(name, flags, 0)
{
}
///
/// Initializes a new V8 script engine instance with the specified name, options, and debug port.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// A value that selects options for the operation.
/// A TCP port on which to listen for a debugger connection.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name, V8ScriptEngineFlags flags, int debugPort)
: this(name, null, flags, debugPort)
{
}
///
/// Initializes a new V8 script engine instance with the specified resource constraints and options.
///
/// Resource constraints for the V8 runtime (see remarks).
/// A value that selects options for the operation.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(V8RuntimeConstraints constraints, V8ScriptEngineFlags flags)
: this(constraints, flags, 0)
{
}
///
/// Initializes a new V8 script engine instance with the specified resource constraints, options, and debug port.
///
/// Resource constraints for the V8 runtime (see remarks).
/// A value that selects options for the operation.
/// A TCP port on which to listen for a debugger connection.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(V8RuntimeConstraints constraints, V8ScriptEngineFlags flags, int debugPort)
: this(null, constraints, flags, debugPort)
{
}
///
/// Initializes a new V8 script engine instance with the specified name, resource constraints, and options.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// Resource constraints for the V8 runtime (see remarks).
/// A value that selects options for the operation.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name, V8RuntimeConstraints constraints, V8ScriptEngineFlags flags)
: this(name, constraints, flags, 0)
{
}
///
/// Initializes a new V8 script engine instance with the specified name, resource constraints, options, and debug port.
///
/// A name to associate with the instance. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// Resource constraints for the V8 runtime (see remarks).
/// A value that selects options for the operation.
/// A TCP port on which to listen for a debugger connection.
///
/// A separate V8 runtime is created for the new script engine instance.
///
public V8ScriptEngine(string name, V8RuntimeConstraints constraints, V8ScriptEngineFlags flags, int debugPort)
: this(null, name, constraints, flags, debugPort)
{
}
internal V8ScriptEngine(V8Runtime runtime, string name, V8RuntimeConstraints constraints, V8ScriptEngineFlags flags, int debugPort)
: base((runtime is not null) ? runtime.Name + ":" + name : name, "js")
{
if (runtime is not null)
{
this.runtime = runtime;
}
else
{
this.runtime = runtime = new V8Runtime(name, constraints);
usingPrivateRuntime = true;
}
DocumentNameManager = runtime.DocumentNameManager;
HostItemCollateral = runtime.HostItemCollateral;
Flags = flags;
proxy = V8ContextProxy.Create(runtime.IsolateProxy, Name, flags, debugPort);
script = (V8ScriptItem)GetRootItem();
if (flags.HasAllFlags(V8ScriptEngineFlags.EnableStringifyEnhancements))
{
script.SetProperty("toJson", new Func(new JsonHelper(this).ToJson));
}
Execute(initScriptInfo, initScript);
if (flags.HasAllFlags(V8ScriptEngineFlags.EnableDebugging | V8ScriptEngineFlags.AwaitDebuggerAndPauseOnStart))
{
awaitDebuggerAndPause = true;
}
}
#endregion
#region public members
///
/// Resumes script execution if the script engine is waiting for a debugger connection.
///
///
/// This method can be called safely from any thread.
///
public void CancelAwaitDebugger()
{
VerifyNotDisposed();
proxy.CancelAwaitDebugger();
}
///
/// Gets or sets a soft limit for the size of the V8 runtime's heap.
///
///
///
/// This property is specified in bytes. When it is set to the default value, heap size
/// monitoring is disabled, and scripts with memory leaks or excessive memory usage
/// can cause unrecoverable errors and process termination.
///
///
/// A V8 runtime unconditionally terminates the process when it exceeds its resource
/// constraints (see ). This property enables external
/// heap size monitoring that can prevent termination in some scenarios. To be effective,
/// it should be set to a value that is significantly lower than
/// . Note that enabling heap size
/// monitoring results in slower script execution.
///
///
/// Exceeding this limit causes the V8 runtime to behave in accordance with
/// .
///
///
/// Note that
/// ArrayBuffer
/// memory is allocated outside the runtime's heap and is therefore not tracked by heap
/// size monitoring. See for
/// additional information.
///
///
public UIntPtr MaxRuntimeHeapSize
{
get
{
VerifyNotDisposed();
return proxy.MaxIsolateHeapSize;
}
set
{
VerifyNotDisposed();
proxy.MaxIsolateHeapSize = value;
}
}
///
/// Gets or sets the minimum time interval between consecutive heap size samples.
///
///
/// This property is effective only when heap size monitoring is enabled (see
/// ).
///
public TimeSpan RuntimeHeapSizeSampleInterval
{
get
{
VerifyNotDisposed();
return proxy.IsolateHeapSizeSampleInterval;
}
set
{
VerifyNotDisposed();
proxy.IsolateHeapSizeSampleInterval = value;
}
}
///
/// Gets or sets the maximum amount by which the V8 runtime is permitted to grow the stack during script execution.
///
///
///
/// This property is specified in bytes. When it is set to the default value, no stack
/// usage limit is enforced, and scripts with unchecked recursion or other excessive stack
/// usage can cause unrecoverable errors and process termination.
///
///
/// Note that the V8 runtime does not monitor stack usage while a host call is in progress.
/// Monitoring is resumed when control returns to the runtime.
///
///
public UIntPtr MaxRuntimeStackUsage
{
get
{
VerifyNotDisposed();
return proxy.MaxIsolateStackUsage;
}
set
{
VerifyNotDisposed();
proxy.MaxIsolateStackUsage = value;
}
}
///
/// Enables or disables instance method enumeration.
///
///
/// By default, a host object's instance methods are exposed as enumerable properties.
/// Setting this property to true causes instance methods to be excluded from
/// property enumeration. This affects all host objects exposed in the current script
/// engine. Note that instance methods remain both retrievable and invocable regardless of
/// this property's value.
///
public bool SuppressInstanceMethodEnumeration
{
get => suppressInstanceMethodEnumeration;
set
{
suppressInstanceMethodEnumeration = value;
OnEnumerationSettingsChanged();
}
}
///
/// Enables or disables extension method enumeration.
///
///
///
/// By default, all exposed extension methods appear as enumerable properties of all host
/// objects, regardless of type. Setting this property to true causes extension
/// methods to be excluded from property enumeration. This affects all host objects exposed
/// in the current script engine. Note that extension methods remain both retrievable and
/// invocable regardless of this property's value.
///
///
/// This property has no effect if is set
/// to true .
///
///
public bool SuppressExtensionMethodEnumeration
{
get => suppressExtensionMethodEnumeration;
set
{
suppressExtensionMethodEnumeration = value;
RebuildExtensionMethodSummary();
}
}
///
/// Enables or disables interrupt propagation in the V8 runtime.
///
///
/// By default, when nested script execution is interrupted via , an
/// instance of , if not handled by the host, is
/// wrapped and delivered to the parent script frame as a normal exception that JavaScript
/// code can catch. Setting this property to true causes the V8 runtime to remain in
/// the interrupted state until its outermost script frame has been processed.
///
public bool EnableRuntimeInterruptPropagation
{
get
{
VerifyNotDisposed();
return proxy.EnableIsolateInterruptPropagation;
}
set
{
VerifyNotDisposed();
proxy.EnableIsolateInterruptPropagation = value;
}
}
///
/// Gets or sets the V8 runtime's behavior in response to a violation of the maximum heap size.
///
public V8RuntimeViolationPolicy RuntimeHeapSizeViolationPolicy
{
get
{
VerifyNotDisposed();
return proxy.DisableIsolateHeapSizeViolationInterrupt ? V8RuntimeViolationPolicy.Exception : V8RuntimeViolationPolicy.Interrupt;
}
set
{
VerifyNotDisposed();
switch (value)
{
case V8RuntimeViolationPolicy.Interrupt:
proxy.DisableIsolateHeapSizeViolationInterrupt = false;
return;
case V8RuntimeViolationPolicy.Exception:
proxy.DisableIsolateHeapSizeViolationInterrupt = true;
return;
default:
throw new ArgumentException(MiscHelpers.FormatInvariant("Invalid {0} value", nameof(V8RuntimeViolationPolicy)), nameof(value));
}
}
}
///
/// Creates a compiled script.
///
/// The script code to compile.
/// A compiled script that can be executed multiple times without recompilation.
public V8Script Compile(string code)
{
return Compile(null, code);
}
///
/// Creates a compiled script with an associated document name.
///
/// A document name for the compiled script. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// The script code to compile.
/// A compiled script that can be executed multiple times without recompilation.
public V8Script Compile(string documentName, string code)
{
return Compile(new DocumentInfo(documentName), code);
}
///
/// Creates a compiled script with the specified document meta-information.
///
/// A structure containing meta-information for the script document.
/// The script code to compile.
/// A compiled script that can be executed multiple times without recompilation.
public V8Script Compile(DocumentInfo documentInfo, string code)
{
VerifyNotDisposed();
return ScriptInvoke(static ctx => ctx.self.CompileInternal(ctx.documentInfo.MakeUnique(ctx.self), ctx.code), (self: this, documentInfo, code));
}
///
/// Creates a compiled script, generating cache data for accelerated recompilation.
///
/// The script code to compile.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// The generated cache data can be stored externally and is usable in other V8 script
/// engines and application processes.
///
///
public V8Script Compile(string code, V8CacheKind cacheKind, out byte[] cacheBytes)
{
return Compile(null, code, cacheKind, out cacheBytes);
}
///
/// Creates a compiled script with an associated document name, generating cache data for accelerated recompilation.
///
/// A document name for the compiled script. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// The script code to compile.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// The generated cache data can be stored externally and is usable in other V8 script
/// engines and application processes.
///
///
public V8Script Compile(string documentName, string code, V8CacheKind cacheKind, out byte[] cacheBytes)
{
return Compile(new DocumentInfo(documentName), code, cacheKind, out cacheBytes);
}
///
/// Creates a compiled script with the specified document meta-information, generating cache data for accelerated recompilation.
///
/// A structure containing meta-information for the script document.
/// The script code to compile.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// The generated cache data can be stored externally and is usable in other V8 script
/// engines and application processes.
///
///
public V8Script Compile(DocumentInfo documentInfo, string code, V8CacheKind cacheKind, out byte[] cacheBytes)
{
VerifyNotDisposed();
var ctx = (self: this, documentInfo, code, cacheKind, cacheBytes: (byte[])null);
var tempScript = ScriptInvoke(
static pCtx =>
{
ref var ctx = ref pCtx.AsRef();
return ctx.self.CompileInternal(ctx.documentInfo.MakeUnique(ctx.self), ctx.code, ctx.cacheKind, out ctx.cacheBytes);
},
StructPtr.FromRef(ref ctx)
);
cacheBytes = ctx.cacheBytes;
return tempScript;
}
///
/// Creates a compiled script, consuming previously generated cache data.
///
/// The script code to compile.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
///
public V8Script Compile(string code, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
return Compile(null, code, cacheKind, cacheBytes, out cacheAccepted);
}
///
/// Creates a compiled script with an associated document name, consuming previously generated cache data.
///
/// A document name for the compiled script. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// The script code to compile.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
///
public V8Script Compile(string documentName, string code, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
return Compile(new DocumentInfo(documentName), code, cacheKind, cacheBytes, out cacheAccepted);
}
///
/// Creates a compiled script with an associated document name, consuming previously generated cache data.
///
/// A structure containing meta-information for the script document.
/// The script code to compile.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
///
public V8Script Compile(DocumentInfo documentInfo, string code, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
VerifyNotDisposed();
var ctx = (self: this, documentInfo, code, cacheKind, cacheBytes, cacheAccepted: false);
var tempScript = ScriptInvoke(
static pCtx =>
{
ref var ctx = ref pCtx.AsRef();
return ctx.self.CompileInternal(ctx.documentInfo.MakeUnique(ctx.self), ctx.code, ctx.cacheKind, ctx.cacheBytes, out ctx.cacheAccepted);
},
StructPtr.FromRef(ref ctx)
);
cacheAccepted = ctx.cacheAccepted;
return tempScript;
}
///
/// Creates a compiled script, consuming previously generated cache data and updating it if necessary.
///
/// The script code to compile.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script Compile(string code, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
return Compile(null, code, cacheKind, ref cacheBytes, out cacheResult);
}
///
/// Creates a compiled script with an associated document name, consuming previously generated cache data and updating it if necessary.
///
/// A document name for the compiled script. Currently, this name is used only as a label in presentation contexts such as debugger user interfaces.
/// The script code to compile.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script Compile(string documentName, string code, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
return Compile(new DocumentInfo(documentName), code, cacheKind, ref cacheBytes, out cacheResult);
}
///
/// Creates a compiled script with the specified document meta-information, consuming previously generated cache data and updating it if necessary.
///
/// A structure containing meta-information for the script document.
/// The script code to compile.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed multiple times without recompilation.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script Compile(DocumentInfo documentInfo, string code, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
VerifyNotDisposed();
var ctx = (self: this, documentInfo, code, cacheKind, cacheBytes, cacheResult: V8CacheResult.Disabled);
var tempScript = ScriptInvoke(
static pCtx =>
{
ref var ctx = ref pCtx.AsRef();
return ctx.self.CompileInternal(ctx.documentInfo.MakeUnique(ctx.self), ctx.code, ctx.cacheKind, ref ctx.cacheBytes, out ctx.cacheResult);
},
StructPtr.FromRef(ref ctx)
);
if (ctx.cacheResult == V8CacheResult.Updated)
{
cacheBytes = ctx.cacheBytes;
}
cacheResult = ctx.cacheResult;
return tempScript;
}
///
/// Loads and compiles a script document.
///
/// A string specifying the document to be loaded and compiled.
/// A compiled script that can be executed by multiple V8 script engine instances.
public V8Script CompileDocument(string specifier)
{
return CompileDocument(specifier, null);
}
///
/// Loads and compiles a document with the specified category.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// A compiled script that can be executed by multiple V8 script engine instances.
public V8Script CompileDocument(string specifier, DocumentCategory category)
{
return CompileDocument(specifier, category, null);
}
///
/// Loads and compiles a document with the specified category and context callback.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// A compiled script that can be executed by multiple V8 script engine instances.
public V8Script CompileDocument(string specifier, DocumentCategory category, DocumentContextCallback contextCallback)
{
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
var document = DocumentSettings.LoadDocument(null, specifier, category, contextCallback);
return Compile(document.Info, document.GetTextContents());
}
///
/// Loads and compiles a script document, generating cache data for accelerated recompilation.
///
/// A string specifying the document to be loaded and compiled.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// The generated cache data can be stored externally and is usable in other V8 runtimes
/// and application processes.
///
public V8Script CompileDocument(string specifier, V8CacheKind cacheKind, out byte[] cacheBytes)
{
return CompileDocument(specifier, null, cacheKind, out cacheBytes);
}
///
/// Loads and compiles a document with the specified category, generating cache data for accelerated recompilation.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// The generated cache data can be stored externally and is usable in other V8 runtimes
/// and application processes.
///
public V8Script CompileDocument(string specifier, DocumentCategory category, V8CacheKind cacheKind, out byte[] cacheBytes)
{
return CompileDocument(specifier, category, null, cacheKind, out cacheBytes);
}
///
/// Loads and compiles a document with the specified category and context callback, generating cache data for accelerated recompilation.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// The kind of cache data to be generated.
/// Cache data for accelerated recompilation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// The generated cache data can be stored externally and is usable in other V8 runtimes
/// and application processes.
///
public V8Script CompileDocument(string specifier, DocumentCategory category, DocumentContextCallback contextCallback, V8CacheKind cacheKind, out byte[] cacheBytes)
{
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
var document = DocumentSettings.LoadDocument(null, specifier, category, contextCallback);
return Compile(document.Info, document.GetTextContents(), cacheKind, out cacheBytes);
}
///
/// Loads and compiles a script document, consuming previously generated cache data.
///
/// A string specifying the document to be loaded and compiled.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
public V8Script CompileDocument(string specifier, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
return CompileDocument(specifier, null, cacheKind, cacheBytes, out cacheAccepted);
}
///
/// Loads and compiles a document with the specified category, consuming previously generated cache data.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
public V8Script CompileDocument(string specifier, DocumentCategory category, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
return CompileDocument(specifier, category, null, cacheKind, cacheBytes, out cacheAccepted);
}
///
/// Loads and compiles a document with the specified category and context callback, consuming previously generated cache data.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// The kind of cache data to be consumed.
/// Cache data for accelerated compilation.
/// True if was accepted and used to accelerate script compilation, false otherwise.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. Note that script compilation may be bypassed if a suitable compiled
/// script already exists in the V8 runtime's memory. In that case, the cache data is
/// ignored and is set to false .
///
public V8Script CompileDocument(string specifier, DocumentCategory category, DocumentContextCallback contextCallback, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
var document = DocumentSettings.LoadDocument(null, specifier, category, contextCallback);
return Compile(document.Info, document.GetTextContents(), cacheKind, cacheBytes, out cacheAccepted);
}
///
/// Loads and compiles a script document, consuming previously generated cache data and updating it if necessary.
///
/// A string specifying the document to be loaded and compiled.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script CompileDocument(string specifier, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
return CompileDocument(specifier, null, cacheKind, ref cacheBytes, out cacheResult);
}
///
/// Loads and compiles a document with the specified category, consuming previously generated cache data and updating it if necessary.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script CompileDocument(string specifier, DocumentCategory category, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
return CompileDocument(specifier, category, null, cacheKind, ref cacheBytes, out cacheResult);
}
///
/// Loads and compiles a document with the specified category and context callback, consuming previously generated cache data and updating it if necessary.
///
/// A string specifying the document to be loaded and compiled.
/// An optional category for the requested document.
/// An optional context callback for the requested document.
/// The kind of cache data to be processed.
/// Cache data for accelerated compilation.
/// The cache data processing result for the operation.
/// A compiled script that can be executed by multiple V8 script engine instances.
///
/// To be accepted, the cache data must have been generated for identical script code by
/// the same V8 build. If returned, the updated cache data can be stored externally and is
/// usable in other V8 script engines and application processes.
///
public V8Script CompileDocument(string specifier, DocumentCategory category, DocumentContextCallback contextCallback, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
MiscHelpers.VerifyNonBlankArgument(specifier, nameof(specifier), "Invalid document specifier");
var document = DocumentSettings.LoadDocument(null, specifier, category, contextCallback);
return Compile(document.Info, document.GetTextContents(), cacheKind, ref cacheBytes, out cacheResult);
}
// ReSharper disable ParameterHidesMember
///
/// Evaluates a compiled script.
///
/// The compiled script to evaluate.
/// The result value.
///
/// For information about the types of result values that script code can return, see
/// .
///
public object Evaluate(V8Script script)
{
return Execute(script, true);
}
///
/// Executes a compiled script.
///
/// The compiled script to execute.
///
/// This method is similar to with the exception that it
/// does not marshal a result value to the host. It can provide a performance advantage
/// when the result value is not needed.
///
public void Execute(V8Script script)
{
Execute(script, false);
}
// ReSharper restore ParameterHidesMember
///
/// Cancels any pending request to interrupt script execution.
///
///
/// This method can be called safely from any thread.
///
///
public void CancelInterrupt()
{
VerifyNotDisposed();
proxy.CancelInterrupt();
}
///
/// Returns memory usage information for the V8 runtime.
///
/// A object containing memory usage information for the V8 runtime.
public V8RuntimeHeapInfo GetRuntimeHeapInfo()
{
VerifyNotDisposed();
return proxy.GetIsolateHeapInfo();
}
///
/// Begins collecting a new CPU profile.
///
/// A name for the profile.
/// True if the profile was created successfully, false otherwise.
///
/// A V8 script engine can collect multiple CPU profiles simultaneously.
///
public bool BeginCpuProfile(string name)
{
return BeginCpuProfile(name, V8CpuProfileFlags.None);
}
///
/// Begins collecting a new CPU profile with the specified options.
///
/// A name for the profile.
/// Options for creating the profile.
/// True if the profile was created successfully, false otherwise.
///
/// A V8 script engine can collect multiple CPU profiles simultaneously.
///
public bool BeginCpuProfile(string name, V8CpuProfileFlags flags)
{
VerifyNotDisposed();
return proxy.BeginCpuProfile(Name + ':' + name, flags);
}
///
/// Completes and returns a CPU profile.
///
/// The name of the profile.
/// The profile if it was found and completed successfully, null otherwise.
///
/// An empty argument selects the most recently created CPU profile.
///
public V8CpuProfile EndCpuProfile(string name)
{
VerifyNotDisposed();
return proxy.EndCpuProfile(Name + ':' + name);
}
///
/// Collects a sample in all CPU profiles active in the V8 runtime.
///
public void CollectCpuProfileSample()
{
VerifyNotDisposed();
proxy.CollectCpuProfileSample();
}
///
/// Gets or sets the time interval between automatic CPU profile samples, in microseconds.
///
///
/// Assigning this property has no effect on CPU profiles already active in the V8 runtime.
/// The default value is 1000.
///
public uint CpuProfileSampleInterval
{
get
{
VerifyNotDisposed();
return proxy.CpuProfileSampleInterval;
}
set
{
VerifyNotDisposed();
proxy.CpuProfileSampleInterval = value;
}
}
///
/// Writes a snapshot of the V8 runtime's heap to the given stream.
///
/// The stream to which to write the heap snapshot.
///
/// This method generates a heap snapshot in JSON format with ASCII encoding.
///
public void WriteRuntimeHeapSnapshot(Stream stream)
{
MiscHelpers.VerifyNonNullArgument(stream, nameof(stream));
VerifyNotDisposed();
ScriptInvoke(static ctx => ctx.proxy.WriteIsolateHeapSnapshot(ctx.stream), (proxy, stream));
}
#endregion
#region internal members
internal V8ScriptEngineFlags Flags { get; }
internal V8Runtime.Statistics GetRuntimeStatistics()
{
VerifyNotDisposed();
return proxy.GetIsolateStatistics();
}
internal Statistics GetStatistics()
{
VerifyNotDisposed();
return ScriptInvoke(
static self =>
{
var statistics = self.proxy.GetStatistics();
if (self.commonJSManager is not null)
{
statistics.CommonJSModuleCacheSize = self.commonJSManager.ModuleCacheSize;
}
return statistics;
},
this
);
}
internal bool Equals(V8ScriptItem left, V8ScriptItem right)
{
if ((left.Engine is V8ScriptEngine leftEngine) && (right.Engine is V8ScriptEngine rightEngine) && (leftEngine.runtime == rightEngine.runtime) && (left.GetHashCode() == right.GetHashCode()))
{
var engineInternal = (ScriptObject)script.GetProperty("EngineInternal");
return (bool)engineInternal.InvokeMethod("strictEquals", left, right);
}
return false;
}
internal CommonJSManager CommonJSManager => commonJSManager ?? (commonJSManager = new CommonJSManager(this));
internal JsonModuleManager JsonModuleManager => jsonDocumentManager ?? (jsonDocumentManager = new JsonModuleManager(this));
private object GetRootItem()
{
return MarshalToHost(ScriptInvoke(static proxy => proxy.GetRootItem(), proxy), false);
}
private void VerifyNotDisposed()
{
if (disposedFlag.IsSet)
{
throw new ObjectDisposedException(ToString());
}
}
private string BaseExecuteCommand(string command) => base.ExecuteCommand(command);
// ReSharper disable ParameterHidesMember
private object Execute(V8Script script, bool evaluate)
{
MiscHelpers.VerifyNonNullArgument(script, nameof(script));
VerifyNotDisposed();
return MarshalToHost(
ScriptInvoke(
static ctx =>
{
if (ctx.self.inContinuationTimerScope || (ctx.self.ContinuationCallback is null))
{
if (ctx.self.ShouldAwaitDebuggerAndPause(ctx.script.DocumentInfo))
{
ctx.self.proxy.AwaitDebuggerAndPause();
}
return ctx.self.ExecuteInternal(ctx.script, ctx.evaluate);
}
var state = new Timer[] { null };
using (state[0] = new Timer(_ => ctx.self.OnContinuationTimer(state[0]), null, Timeout.Infinite, Timeout.Infinite))
{
ctx.self.inContinuationTimerScope = true;
try
{
state[0].Change(continuationInterval, Timeout.Infinite);
if (ctx.self.ShouldAwaitDebuggerAndPause(ctx.script.DocumentInfo))
{
ctx.self.proxy.AwaitDebuggerAndPause();
}
return ctx.self.ExecuteInternal(ctx.script, ctx.evaluate);
}
finally
{
ctx.self.inContinuationTimerScope = false;
}
}
},
(self: this, script, evaluate)
),
false
);
}
// ReSharper restore ParameterHidesMember
private V8Script CompileInternal(UniqueDocumentInfo documentInfo, string code)
{
if (FormatCode)
{
code = MiscHelpers.FormatCode(code);
}
CommonJSManager.Module module = null;
if (documentInfo.Category == ModuleCategory.CommonJS)
{
module = CommonJSManager.GetOrCreateModule(documentInfo, code);
code = CommonJSManager.Module.GetAugmentedCode(code);
}
else if ((documentInfo.Category != DocumentCategory.Script) && (documentInfo.Category != ModuleCategory.Standard))
{
throw new NotSupportedException("The script engine cannot compile documents of type '" + documentInfo.Category + "'");
}
// ReSharper disable once LocalVariableHidesMember
var script = proxy.Compile(documentInfo, code);
if (module is not null)
{
module.Evaluator = () => proxy.Execute(script, true);
}
return script;
}
private V8Script CompileInternal(UniqueDocumentInfo documentInfo, string code, V8CacheKind cacheKind, out byte[] cacheBytes)
{
if (FormatCode)
{
code = MiscHelpers.FormatCode(code);
}
CommonJSManager.Module module = null;
if (documentInfo.Category == ModuleCategory.CommonJS)
{
module = CommonJSManager.GetOrCreateModule(documentInfo, code);
code = CommonJSManager.Module.GetAugmentedCode(code);
}
else if ((documentInfo.Category != DocumentCategory.Script) && (documentInfo.Category != ModuleCategory.Standard))
{
throw new NotSupportedException("The script engine cannot compile documents of type '" + documentInfo.Category + "'");
}
// ReSharper disable once LocalVariableHidesMember
var script = proxy.Compile(documentInfo, code, cacheKind, out cacheBytes);
if (module is not null)
{
module.Evaluator = () => proxy.Execute(script, true);
}
return script;
}
private V8Script CompileInternal(UniqueDocumentInfo documentInfo, string code, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted)
{
if (FormatCode)
{
code = MiscHelpers.FormatCode(code);
}
CommonJSManager.Module module = null;
if (documentInfo.Category == ModuleCategory.CommonJS)
{
module = CommonJSManager.GetOrCreateModule(documentInfo, code);
code = CommonJSManager.Module.GetAugmentedCode(code);
}
else if ((documentInfo.Category != DocumentCategory.Script) && (documentInfo.Category != ModuleCategory.Standard))
{
throw new NotSupportedException("The script engine cannot compile documents of type '" + documentInfo.Category + "'");
}
// ReSharper disable once LocalVariableHidesMember
var script = proxy.Compile(documentInfo, code, cacheKind, cacheBytes, out cacheAccepted);
if (module is not null)
{
module.Evaluator = () => proxy.Execute(script, true);
}
return script;
}
private V8Script CompileInternal(UniqueDocumentInfo documentInfo, string code, V8CacheKind cacheKind, ref byte[] cacheBytes, out V8CacheResult cacheResult)
{
if (FormatCode)
{
code = MiscHelpers.FormatCode(code);
}
CommonJSManager.Module module = null;
if (documentInfo.Category == ModuleCategory.CommonJS)
{
module = CommonJSManager.GetOrCreateModule(documentInfo, code);
code = CommonJSManager.Module.GetAugmentedCode(code);
}
else if ((documentInfo.Category != DocumentCategory.Script) && (documentInfo.Category != ModuleCategory.Standard))
{
throw new NotSupportedException("The script engine cannot compile documents of type '" + documentInfo.Category + "'");
}
// ReSharper disable once LocalVariableHidesMember
var script = proxy.Compile(documentInfo, code, cacheKind, ref cacheBytes, out cacheResult);
if (module is not null)
{
module.Evaluator = () => proxy.Execute(script, true);
}
return script;
}
private object ExecuteInternal(UniqueDocumentInfo documentInfo, string code, bool evaluate)
{
if (FormatCode)
{
code = MiscHelpers.FormatCode(code);
}
if (documentInfo.Category == ModuleCategory.CommonJS)
{
var module = CommonJSManager.GetOrCreateModule(documentInfo, code);
return module.Process();
}
if ((documentInfo.Category != DocumentCategory.Script) && (documentInfo.Category != ModuleCategory.Standard))
{
throw new NotSupportedException("The script engine cannot execute documents of type '" + documentInfo.Category + "'");
}
return ExecuteRaw(documentInfo, code, evaluate);
}
// ReSharper disable ParameterHidesMember
private object ExecuteInternal(V8Script script, bool evaluate)
{
if (script.UniqueDocumentInfo.Category == ModuleCategory.CommonJS)
{
var module = CommonJSManager.GetOrCreateModule(script.UniqueDocumentInfo, script.CodeDigest, () => proxy.Execute(script, evaluate));
return module.Process();
}
return proxy.Execute(script, evaluate);
}
// ReSharper restore ParameterHidesMember
private bool ShouldAwaitDebuggerAndPause(DocumentInfo documentInfo)
{
if (!awaitDebuggerAndPause.HasValue)
{
if (documentInfo.Flags.GetValueOrDefault().HasAllFlags(DocumentFlags.AwaitDebuggerAndPause))
{
awaitDebuggerAndPause = false;
return true;
}
return false;
}
if (awaitDebuggerAndPause.Value)
{
awaitDebuggerAndPause = false;
return true;
}
return false;
}
private void OnContinuationTimer(Timer timer)
{
try
{
var callback = ContinuationCallback;
if ((callback is not null) && !callback())
{
Interrupt();
}
else
{
timer.Change(continuationInterval, Timeout.Infinite);
}
}
catch (ObjectDisposedException)
{
}
}
private object CreatePromise(Action executor)
{
VerifyNotDisposed();
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
return V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createPromise", executor));
}
private object CreateSettledPromise(Task task)
{
VerifyNotDisposed();
Func getResult = () => task.Result;
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
return V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createSettledPromiseWithResult", getResult));
}
private object CreateSettledPromise(Task task)
{
VerifyNotDisposed();
Action wait = task.Wait;
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
return V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createSettledPromise", wait));
}
private object CreateSettledPromise(ValueTask valueTask)
{
VerifyNotDisposed();
Func getResult = () => valueTask.Result;
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
return V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createSettledPromiseWithResult", getResult));
}
private object CreateSettledPromise(ValueTask valueTask)
{
VerifyNotDisposed();
Action wait = () => WaitForValueTask(valueTask);
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
return V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createSettledPromise", wait));
}
private void CompletePromise(Task task, object resolve, object reject)
{
Func getResult = () => task.Result;
var engineInternal = (ScriptObject)script.GetProperty("EngineInternal");
engineInternal.InvokeMethod("completePromiseWithResult", getResult, resolve, reject);
}
private void CompletePromise(Task task, object resolve, object reject)
{
Action wait = task.Wait;
var engineInternal = (ScriptObject)script.GetProperty("EngineInternal");
engineInternal.InvokeMethod("completePromise", wait, resolve, reject);
}
private static void WaitForValueTask(ValueTask valueTask)
{
if (valueTask.IsCompletedSuccessfully)
{
return;
}
if (valueTask.IsCanceled)
{
throw new TaskCanceledException();
}
valueTask.AsTask().Wait();
}
#endregion
#region ScriptEngine overrides (public members)
///
/// Gets the script engine's recommended file name extension for script files.
///
///
/// instances return "js" for this property.
///
public override string FileNameExtension => "js";
///
public override dynamic Script
{
get
{
VerifyNotDisposed();
return script;
}
}
///
public override ScriptObject Global
{
get
{
VerifyNotDisposed();
return script;
}
}
///
/// Executes script code as a command.
///
/// The script command to execute.
/// The command output.
///
///
/// This method is similar to but optimized for
/// command consoles. The specified command must be limited to a single expression or
/// statement. Script engines can override this method to customize command execution as
/// well as the process of converting the result to a string for console output.
///
///
/// The version of this method attempts to use
/// toString
/// to convert the return value.
///
///
public override string ExecuteCommand(string command)
{
return ScriptInvoke(
static ctx =>
{
var engineInternal = (ScriptObject)ctx.self.script.GetProperty("EngineInternal");
var commandHolder = (ScriptObject)engineInternal.GetProperty("commandHolder");
commandHolder.SetProperty("command", ctx.command);
return ctx.self.BaseExecuteCommand("EngineInternal.getCommandResult(eval(EngineInternal.commandHolder.command))");
},
(self: this, command)
);
}
///
public override string GetStackTrace()
{
var engineInternal = (ScriptObject)script.GetProperty("EngineInternal");
var stackTrace = (string)engineInternal.InvokeMethod("getStackTrace");
var lines = stackTrace.Split('\n');
return string.Join("\n", lines.Skip(2));
}
///
/// Interrupts script execution and causes the script engine to throw an exception.
///
///
/// This method can be called safely from any thread.
///
///
public override void Interrupt()
{
VerifyNotDisposed();
proxy.Interrupt();
}
///
public override void CollectGarbage(bool exhaustive)
{
VerifyNotDisposed();
proxy.CollectGarbage(exhaustive);
}
#endregion
#region ScriptEngine overrides (internal members)
internal override IUniqueNameManager DocumentNameManager { get; }
internal override bool EnumerateInstanceMethods => base.EnumerateInstanceMethods && !SuppressInstanceMethodEnumeration;
internal override bool EnumerateExtensionMethods => base.EnumerateExtensionMethods && !SuppressExtensionMethodEnumeration;
internal override bool UseCaseInsensitiveMemberBinding => Flags.HasAllFlags(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding);
internal override void AddHostItem(string itemName, HostItemFlags flags, object item)
{
VerifyNotDisposed();
var globalMembers = flags.HasAllFlags(HostItemFlags.GlobalMembers);
if (globalMembers && Flags.HasAllFlags(V8ScriptEngineFlags.DisableGlobalMembers))
{
throw new InvalidOperationException("GlobalMembers support is disabled in this script engine");
}
MiscHelpers.VerifyNonNullArgument(itemName, nameof(itemName));
Debug.Assert(item is not null);
ScriptInvoke(
static ctx =>
{
var marshaledItem = ctx.self.MarshalToScript(ctx.item, ctx.flags);
if ((marshaledItem is not HostItem) && (marshaledItem is not V8FastHostItem))
{
throw new InvalidOperationException("Invalid host item");
}
ctx.self.proxy.AddGlobalItem(ctx.itemName, marshaledItem, ctx.globalMembers);
},
(self: this, itemName, flags, item, globalMembers)
);
}
internal override object MarshalToScript(object obj, HostItemFlags flags)
{
return MarshalToScriptInternal(obj, flags, null);
}
private object MarshalToScriptInternal(object obj, HostItemFlags flags, Dictionary marshaledArrayMap)
{
if (obj is null)
{
obj = NullExportValue;
}
if (obj is null)
{
return DBNull.Value;
}
if (obj is DBNull)
{
return obj;
}
if (obj is Undefined)
{
return null;
}
if (obj is Nonexistent)
{
return obj;
}
if (obj is INothingTag)
{
return null;
}
if (obj is BigInteger)
{
return obj;
}
if (obj is long longValue)
{
if (Flags.HasAllFlags(V8ScriptEngineFlags.MarshalAllInt64AsBigInt))
{
return new BigInteger(longValue);
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.MarshalUnsafeInt64AsBigInt) && ((longValue < -MiscHelpers.MaxInt64InDouble) || (longValue > MiscHelpers.MaxInt64InDouble)))
{
return new BigInteger(longValue);
}
}
if (obj is ulong ulongValue)
{
if (Flags.HasAllFlags(V8ScriptEngineFlags.MarshalAllInt64AsBigInt))
{
return new BigInteger(ulongValue);
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.MarshalUnsafeInt64AsBigInt) && (ulongValue > MiscHelpers.MaxInt64InDouble))
{
return new BigInteger(ulongValue);
}
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableDateTimeConversion) && (obj is DateTime))
{
return obj;
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableTaskPromiseConversion))
{
// .NET Core async functions return Task subclass instances that trigger result wrapping
var testObject = obj;
if (testObject is HostObject testHostObject)
{
testObject = testHostObject.Target;
}
if (testObject is not null)
{
if (testObject.GetType().IsAssignableToGenericType(typeof(Task<>), out var taskTypeArgs) && (taskTypeArgs.Length > 0) && taskTypeArgs[0].IsAccessible(this))
{
obj = typeof(TaskConverter<>).MakeSpecificType(taskTypeArgs).InvokeMember("ToPromise", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new[] { testObject, this });
}
else if (testObject is Task task)
{
obj = task.ToPromise(this);
}
else if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableValueTaskPromiseConversion))
{
if (obj.GetType().IsAssignableToGenericType(typeof(ValueTask<>), out var valueTaskTypeArgs))
{
obj = typeof(ValueTaskConverter<>).MakeSpecificType(valueTaskTypeArgs).InvokeMember("ToPromise", BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static, null, null, new[] { obj, this });
}
else if (obj is ValueTask valueTask)
{
obj = valueTask.ToPromise(this);
}
}
}
}
if (obj is HostItem hostItem)
{
if ((hostItem.Engine == this) && (hostItem.Flags == flags))
{
return obj;
}
obj = hostItem.Target;
}
var hostTarget = obj as HostTarget;
if ((hostTarget is not null) && (hostTarget is not IHostVariable))
{
obj = hostTarget.Target;
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableArrayConversion))
{
if ((obj is Array array) && ((hostTarget is null) || hostTarget.Type.IsArray) && (array.Rank == 1))
{
if (marshaledArrayMap?.TryGetValue(array, out var scriptArray) != true)
{
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
scriptArray = (V8ScriptItem)V8ScriptItem.Wrap(this, v8Internal.InvokeMethod(false, "createArray"));
(marshaledArrayMap ?? (marshaledArrayMap = new Dictionary())).Add(array, scriptArray);
var elementType = array.GetType().GetElementType();
var upperBound = array.GetUpperBound(0);
for (var index = array.GetLowerBound(0); index <= upperBound; index++)
{
var result = PrepareResult(array.GetValue(index), elementType, ScriptMemberFlags.None, false);
scriptArray.SetProperty(false, index, MarshalToScriptInternal(result, flags, marshaledArrayMap));
}
}
obj = scriptArray;
}
}
if (obj is ScriptItem scriptItem)
{
if ((scriptItem.Engine is V8ScriptEngine that) && (that.runtime == runtime))
{
return scriptItem.Unwrap();
}
if ((scriptItem is V8ScriptItem v8ScriptItem) && v8ScriptItem.IsShared)
{
return scriptItem.Unwrap();
}
}
if (obj is IV8FastHostObject fastObject)
{
return V8FastHostItem.Wrap(this, fastObject, flags);
}
return HostItem.Wrap(this, hostTarget ?? obj, flags);
}
internal override object MarshalToHost(object obj, bool preserveHostTarget)
{
return MarshalToHostInternal(obj, preserveHostTarget, null);
}
private object MarshalToHostInternal(object obj, bool preserveHostTarget, Dictionary marshaledArrayMap)
{
if (obj is null)
{
return UndefinedImportValue;
}
if (obj is DBNull)
{
return NullImportValue;
}
if (MiscHelpers.TryMarshalPrimitiveToHost(obj, DisableFloatNarrowing, out var result))
{
return result;
}
if (obj is V8FastHostItem fastHostItem)
{
return fastHostItem.Target;
}
if (obj is HostTarget hostTarget)
{
return preserveHostTarget ? hostTarget : hostTarget.Target;
}
if (obj is HostItem hostItem)
{
return preserveHostTarget ? hostItem.Target : hostItem.Unwrap();
}
if (obj is ScriptItem)
{
return obj;
}
var wrappedObject = V8ScriptItem.Wrap(this, obj);
if (obj is IV8Object v8Object)
{
var scriptItem = (V8ScriptItem)wrappedObject;
if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableTaskPromiseConversion) && v8Object.IsPromise)
{
return wrappedObject.ToTask();
}
if (Flags.HasAllFlags(V8ScriptEngineFlags.EnableArrayConversion) && v8Object.IsArray)
{
if (marshaledArrayMap?.TryGetValue(scriptItem, out var array) != true)
{
array = new object[((IList)scriptItem).Count];
(marshaledArrayMap ?? (marshaledArrayMap = new Dictionary())).Add(scriptItem, array);
var length = array.Length;
for (var index = 0; index < length; index++)
{
array[index] = MarshalToHostInternal(scriptItem.GetProperty(false, index), false, marshaledArrayMap);
}
}
return array;
}
}
return wrappedObject;
}
internal override object Execute(UniqueDocumentInfo documentInfo, string code, bool evaluate)
{
VerifyNotDisposed();
return ScriptInvoke(
static ctx =>
{
if ((ctx.self.documentNames is not null) && !ctx.documentInfo.Flags.GetValueOrDefault().HasAllFlags(DocumentFlags.IsTransient))
{
ctx.self.documentNames.Add(ctx.documentInfo.UniqueName);
}
if (ctx.self.inContinuationTimerScope || (ctx.self.ContinuationCallback is null))
{
if (ctx.self.ShouldAwaitDebuggerAndPause(ctx.documentInfo.Info))
{
ctx.self.proxy.AwaitDebuggerAndPause();
}
return ctx.self.ExecuteInternal(ctx.documentInfo, ctx.code, ctx.evaluate);
}
var state = new Timer[] { null };
using (state[0] = new Timer(_ => ctx.self.OnContinuationTimer(state[0]), null, Timeout.Infinite, Timeout.Infinite))
{
ctx.self.inContinuationTimerScope = true;
try
{
state[0].Change(continuationInterval, Timeout.Infinite);
if (ctx.self.ShouldAwaitDebuggerAndPause(ctx.documentInfo.Info))
{
ctx.self.proxy.AwaitDebuggerAndPause();
}
return ctx.self.ExecuteInternal(ctx.documentInfo, ctx.code, ctx.evaluate);
}
finally
{
ctx.self.inContinuationTimerScope = false;
}
}
},
(self: this, documentInfo, code, evaluate)
);
}
internal override object ExecuteRaw(UniqueDocumentInfo documentInfo, string code, bool evaluate)
{
return proxy.Execute(documentInfo, code, evaluate);
}
internal override HostItemCollateral HostItemCollateral { get; }
internal override void OnAccessSettingsChanged()
{
base.OnAccessSettingsChanged();
ScriptInvoke(static proxy => proxy.OnAccessSettingsChanged(), proxy);
}
#endregion
#region ScriptEngine overrides (script-side invocation)
internal override void ScriptInvoke(Action action)
{
VerifyNotDisposed();
using (CreateEngineScope())
{
proxy.InvokeWithLock(static ctx => ctx.self.ScriptInvokeInternal(ctx.action), (self: this, action));
}
}
internal override void ScriptInvoke(Action action, in TArg arg)
{
VerifyNotDisposed();
using (CreateEngineScope())
{
proxy.InvokeWithLock(static ctx => ctx.self.ScriptInvokeInternal(ctx.action, ctx.arg), (self: this, action, arg));
}
}
internal override TResult ScriptInvoke(Func func)
{
VerifyNotDisposed();
using (CreateEngineScope())
{
var ctx = (self: this, func, result: default(TResult));
proxy.InvokeWithLock(
static pCtx =>
{
ref var ctx = ref pCtx.AsRef();
ctx.result = ctx.self.ScriptInvokeInternal(ctx.func);
},
StructPtr.FromRef(ref ctx)
);
return ctx.result;
}
}
internal override TResult ScriptInvoke(Func func, in TArg arg)
{
VerifyNotDisposed();
using (CreateEngineScope())
{
var ctx = (self: this, func, arg, result: default(TResult));
proxy.InvokeWithLock(
static pCtx =>
{
ref var ctx = ref pCtx.AsRef();
ctx.result = ctx.self.ScriptInvokeInternal(ctx.func, ctx.arg);
},
StructPtr.FromRef(ref ctx)
);
return ctx.result;
}
}
#endregion
#region ScriptEngine overrides (disposal / finalization)
///
/// Releases the unmanaged resources used by the script engine and optionally releases the managed resources.
///
/// True to release both managed and unmanaged resources; false to release only unmanaged resources.
///
/// This method is called by the public method and the
/// Finalize method.
/// invokes the protected Dispose(Boolean)
/// method with the parameter set to true .
/// Finalize invokes Dispose(Boolean) with
/// set to false .
///
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (disposedFlag.Set())
{
base.Dispose(true);
((IDisposable)script).Dispose();
proxy.Dispose();
if (usingPrivateRuntime)
{
runtime.Dispose();
}
}
}
else
{
base.Dispose(false);
}
}
#endregion
#region fast host item cache
private readonly ConditionalWeakTable>> fastHostItemCache = new();
internal V8FastHostItem GetOrCreateFastHostItem(IV8FastHostObject target, HostItemFlags flags)
{
var cacheEntry = fastHostItemCache.GetOrCreateValue(target);
List> activeWeakRefs = null;
var staleWeakRefCount = 0;
foreach (var weakRef in cacheEntry)
{
if (!weakRef.TryGetTarget(out var hostItem))
{
staleWeakRefCount++;
}
else
{
if (hostItem.Flags == flags)
{
return hostItem;
}
if (activeWeakRefs is null)
{
activeWeakRefs = new List>(cacheEntry.Count);
}
activeWeakRefs.Add(weakRef);
}
}
if (staleWeakRefCount > 4)
{
cacheEntry.Clear();
if (activeWeakRefs is not null)
{
cacheEntry.Capacity = activeWeakRefs.Count + 1;
cacheEntry.AddRange(activeWeakRefs);
}
}
return CreateFastHostItem(target, flags, cacheEntry);
}
private V8FastHostItem CreateFastHostItem(IV8FastHostObject target, HostItemFlags flags, List> cacheEntry)
{
var newHostItem = new V8FastHostItem(this, target, flags);
if (cacheEntry is not null)
{
cacheEntry.Add(new WeakReference(newHostItem));
}
// ReSharper disable once SuspiciousTypeConversion.Global
if (target is IScriptableObject scriptableObject)
{
scriptableObject.OnExposedToScriptCode(this);
}
return newHostItem;
}
#endregion
#region IJavaScriptEngine implementation
uint IJavaScriptEngine.BaseLanguageVersion => 8;
CommonJSManager IJavaScriptEngine.CommonJSManager => CommonJSManager;
JsonModuleManager IJavaScriptEngine.JsonModuleManager => JsonModuleManager;
object IJavaScriptEngine.CreatePromiseForTask(Task task)
{
if (task.IsCompleted)
{
return CreateSettledPromise(task);
}
var scheduler = (Flags.HasAllFlags(V8ScriptEngineFlags.UseSynchronizationContexts) && MiscHelpers.Try(out var contextScheduler, static () => TaskScheduler.FromCurrentSynchronizationContext())) ? contextScheduler : TaskScheduler.Current;
return CreatePromise((resolve, reject) =>
{
task.ContinueWith(_ => CompletePromise(task, resolve, reject), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, scheduler);
});
}
object IJavaScriptEngine.CreatePromiseForTask(Task task)
{
if (task.IsCompleted)
{
return CreateSettledPromise(task);
}
var scheduler = (Flags.HasAllFlags(V8ScriptEngineFlags.UseSynchronizationContexts) && MiscHelpers.Try(out var contextScheduler, static () => TaskScheduler.FromCurrentSynchronizationContext())) ? contextScheduler : TaskScheduler.Current;
return CreatePromise((resolve, reject) =>
{
task.ContinueWith(_ => CompletePromise(task, resolve, reject), CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, scheduler);
});
}
object IJavaScriptEngine.CreatePromiseForValueTask(ValueTask valueTask)
{
if (valueTask.IsCompleted)
{
return CreateSettledPromise(valueTask);
}
return ((IJavaScriptEngine)this).CreatePromiseForTask(valueTask.AsTask());
}
object IJavaScriptEngine.CreatePromiseForValueTask(ValueTask valueTask)
{
if (valueTask.IsCompleted)
{
return CreateSettledPromise(valueTask);
}
return ((IJavaScriptEngine)this).CreatePromiseForTask(valueTask.AsTask());
}
Task IJavaScriptEngine.CreateTaskForPromise(ScriptObject promise)
{
if ((promise is not V8ScriptItem v8ScriptItem) || !v8ScriptItem.IsPromise)
{
throw new ArgumentException("The object is not a V8 promise", nameof(promise));
}
var source = new TaskCompletionSource();
var context = Flags.HasAllFlags(V8ScriptEngineFlags.UseSynchronizationContexts) ? SynchronizationContext.Current : null;
Action setResultWorker = result => source.SetResult(result);
var setResult = (context is null) ? setResultWorker : result => context.Post(_ => setResultWorker(result), null);
Action setExceptionWorker = exception => source.SetException(exception);
var setException = (context is null) ? setExceptionWorker : exception => context.Post(_ => setExceptionWorker(exception), null);
var v8Internal = (V8ScriptItem)script.GetProperty("EngineInternal");
Action onResolved = result =>
{
setResult(result);
};
Action onRejected = error =>
{
try
{
v8Internal.InvokeMethod("throwValue", error);
}
catch (Exception exception)
{
setException(exception);
}
};
var flags = v8ScriptItem.Flags;
v8Internal.InvokeMethod(false, "initializeTask", v8ScriptItem, flags.HasAllFlags(JavaScriptObjectFlags.Pending), flags.HasAllFlags(JavaScriptObjectFlags.Rejected), onResolved, onRejected);
return source.Task;
}
#endregion
#region unit test support
internal void EnableDocumentNameTracking()
{
documentNames = new List();
}
internal IEnumerable GetDocumentNames()
{
return documentNames;
}
#endregion
#region Nested type: Statistics
internal sealed class Statistics
{
public ulong ScriptCount;
public ulong ModuleCount;
public ulong ModuleCacheSize;
public int CommonJSModuleCacheSize;
}
#endregion
#region Nested type: TaskConverter
private static class TaskConverter
{
// ReSharper disable UnusedMember.Local
public static object ToPromise(Task task, V8ScriptEngine engine)
{
return task.ToPromise(engine);
}
// ReSharper restore UnusedMember.Local
}
#endregion
#region Nested type: ValueTaskConverter
private static class ValueTaskConverter
{
// ReSharper disable UnusedMember.Local
public static object ToPromise(ValueTask valueTask, V8ScriptEngine engine)
{
return valueTask.ToPromise(engine);
}
// ReSharper restore UnusedMember.Local
}
#endregion
#region Nested type: JsonHelper
///
public sealed class JsonHelper : JsonConverter
{
private readonly ScriptObject stringify;
private readonly HashSet cycleDetectionSet = new();
///
public JsonHelper(V8ScriptEngine engine)
{
var json = (ScriptObject)engine.script.GetProperty("JSON");
stringify = (ScriptObject)json.GetProperty("stringify");
}
///
public string ToJson(object key, object value)
{
key = key.ToString().ToNonBlank("[root]");
if (!cycleDetectionSet.Add(value))
{
throw new InvalidOperationException($"Cycle detected at key '{key}' during JSON serialization");
}
try
{
return JsonConvert.SerializeObject(value, this);
}
finally
{
cycleDetectionSet.Remove(value);
}
}
///
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var result = stringify.Invoke(false, value);
writer.WriteRawValue(result as string ?? "null");
}
///
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => throw new NotImplementedException();
///
public override bool CanConvert(Type objectType) => typeof(V8ScriptItem).IsAssignableFrom(objectType);
}
#endregion
}
}