// 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 } }