// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Numerics; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.ClearScript.JavaScript; using Microsoft.ClearScript.Util; 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 DocumentInfo(MiscHelpers.FormatInvariant("{0} [internal]", nameof(V8ScriptEngine))); private readonly V8Runtime runtime; private readonly bool usingPrivateRuntime; private readonly V8ScriptEngineFlags engineFlags; private readonly V8ContextProxy proxy; private readonly V8ScriptItem script; private readonly InterlockedOneWayFlag disposedFlag = new InterlockedOneWayFlag(); private const int continuationInterval = 2000; private bool inContinuationTimerScope; private bool awaitDebuggerAndPause; private List documentNames; private bool suppressInstanceMethodEnumeration; private bool suppressExtensionMethodEnumeration; private CommonJSManager commonJSManager; #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 != null) ? runtime.Name + ":" + name : name, "js") { if (runtime != null) { this.runtime = runtime; } else { this.runtime = runtime = new V8Runtime(name, constraints); usingPrivateRuntime = true; } DocumentNameManager = runtime.DocumentNameManager; HostItemCollateral = runtime.HostItemCollateral; engineFlags = flags; proxy = V8ContextProxy.Create(runtime.IsolateProxy, Name, flags, debugPort); script = (V8ScriptItem)GetRootItem(); if (flags.HasFlag(V8ScriptEngineFlags.EnableStringifyEnhancements)) { script.SetProperty("toJson", new Func(new JsonHelper(this).ToJson)); } Execute(initScriptInfo, initScript); if (flags.HasFlag(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(() => CompileInternal(documentInfo.MakeUnique(this), 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(); V8Script tempScript = null; cacheBytes = ScriptInvoke(() => { tempScript = CompileInternal(documentInfo.MakeUnique(this), code, cacheKind, out var tempCacheBytes); return tempCacheBytes; }); 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, 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. /// /// 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, 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. /// /// 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, 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. /// /// public V8Script Compile(DocumentInfo documentInfo, string code, V8CacheKind cacheKind, byte[] cacheBytes, out bool cacheAccepted) { VerifyNotDisposed(); V8Script tempScript = null; cacheAccepted = ScriptInvoke(() => { tempScript = CompileInternal(documentInfo.MakeUnique(this), code, cacheKind, cacheBytes, out var tempCacheAccepted); return tempCacheAccepted; }); 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, 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. /// 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, 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. /// 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, 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. /// 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); } // 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(() => proxy.WriteIsolateHeapSnapshot(stream)); } #endregion #region internal members internal V8Runtime.Statistics GetRuntimeStatistics() { VerifyNotDisposed(); return proxy.GetIsolateStatistics(); } internal Statistics GetStatistics() { VerifyNotDisposed(); return ScriptInvoke(() => { var statistics = proxy.GetStatistics(); if (commonJSManager != null) { statistics.CommonJSModuleCacheSize = CommonJSManager.ModuleCacheSize; } return statistics; }); } private CommonJSManager CommonJSManager => commonJSManager ?? (commonJSManager = new CommonJSManager(this)); private object GetRootItem() { return MarshalToHost(ScriptInvoke(() => proxy.GetRootItem()), false); } private void VerifyNotDisposed() { if (disposedFlag.IsSet) { throw new ObjectDisposedException(ToString()); } } // ReSharper disable ParameterHidesMember private object Execute(V8Script script, bool evaluate) { MiscHelpers.VerifyNonNullArgument(script, nameof(script)); VerifyNotDisposed(); return MarshalToHost(ScriptInvoke(() => { if (inContinuationTimerScope || (ContinuationCallback == null)) { if (MiscHelpers.Exchange(ref awaitDebuggerAndPause, false)) { proxy.AwaitDebuggerAndPause(); } return ExecuteInternal(script, evaluate); } var state = new Timer[] { null }; using (state[0] = new Timer(unused => OnContinuationTimer(state[0]), null, Timeout.Infinite, Timeout.Infinite)) { inContinuationTimerScope = true; try { state[0].Change(continuationInterval, Timeout.Infinite); if (MiscHelpers.Exchange(ref awaitDebuggerAndPause, false)) { proxy.AwaitDebuggerAndPause(); } return ExecuteInternal(script, evaluate); } finally { inContinuationTimerScope = false; } } }), 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); } // ReSharper disable once LocalVariableHidesMember var script = proxy.Compile(documentInfo, code); if (module != 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); } // ReSharper disable once LocalVariableHidesMember var script = proxy.Compile(documentInfo, code, cacheKind, out cacheBytes); if (module != 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); } // ReSharper disable once LocalVariableHidesMember var script = proxy.Compile(documentInfo, code, cacheKind, cacheBytes, out cacheAccepted); if (module != 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(); } 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 void OnContinuationTimer(Timer timer) { try { var callback = ContinuationCallback; if ((callback != 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 void CompletePromise(Task task, object resolve, object reject) { Func getResult = () => task.Result; Script.EngineInternal.completePromiseWithResult(getResult, resolve, reject); } private void CompletePromise(Task task, object resolve, object reject) { Action wait = task.Wait; Script.EngineInternal.completePromise(wait, resolve, reject); } partial void TryConvertValueTaskToPromise(object obj, Action setResult); #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"; /// /// Allows the host to access script resources dynamically. /// /// /// The value of this property is an object that is bound to the script engine's root /// namespace. It dynamically supports properties and methods that correspond to global /// script objects and functions. /// public override dynamic Script { get { VerifyNotDisposed(); return script; } } /// /// Allows the host to access script resources. /// /// /// The value of this property is an object that is bound to the script engine's root /// namespace. It allows you to access global script resources via the /// class interface. Doing so is likely to perform better than /// dynamic access via . /// 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(() => { Script.EngineInternal.commandHolder.command = command; return base.ExecuteCommand("EngineInternal.getCommandResult(eval(EngineInternal.commandHolder.command))"); }); } /// /// Gets a string representation of the script call stack. /// /// The script call stack formatted as a string. /// /// This method returns an empty string if the script engine is not executing script code. /// The stack trace text format is defined by the script engine. /// public override string GetStackTrace() { string stackTrace = Script.EngineInternal.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(); } /// /// Performs garbage collection. /// /// True to perform exhaustive garbage collection, false to favor speed over completeness. 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 => engineFlags.HasFlag(V8ScriptEngineFlags.UseCaseInsensitiveMemberBinding); internal override void AddHostItem(string itemName, HostItemFlags flags, object item) { VerifyNotDisposed(); var globalMembers = flags.HasFlag(HostItemFlags.GlobalMembers); if (globalMembers && engineFlags.HasFlag(V8ScriptEngineFlags.DisableGlobalMembers)) { throw new InvalidOperationException("GlobalMembers support is disabled in this script engine"); } MiscHelpers.VerifyNonNullArgument(itemName, nameof(itemName)); Debug.Assert(item != null); ScriptInvoke(() => { var marshaledItem = MarshalToScript(item, flags); if (!(marshaledItem is HostItem)) { throw new InvalidOperationException("Invalid host item"); } proxy.AddGlobalItem(itemName, marshaledItem, globalMembers); }); } internal override object MarshalToScript(object obj, HostItemFlags flags) { const long maxIntInDouble = (1L << 53) - 1; if (obj == null) { obj = NullExportValue; } if (obj == 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 (engineFlags.HasFlag(V8ScriptEngineFlags.MarshalAllLongAsBigInt)) { return new BigInteger(longValue); } if (engineFlags.HasFlag(V8ScriptEngineFlags.MarshalUnsafeLongAsBigInt) && (Math.Abs(longValue) > maxIntInDouble)) { return new BigInteger(longValue); } } if (obj is ulong ulongValue) { if (engineFlags.HasFlag(V8ScriptEngineFlags.MarshalAllLongAsBigInt)) { return new BigInteger(ulongValue); } if (engineFlags.HasFlag(V8ScriptEngineFlags.MarshalUnsafeLongAsBigInt) && (ulongValue > maxIntInDouble)) { return new BigInteger(ulongValue); } } if (engineFlags.HasFlag(V8ScriptEngineFlags.EnableDateTimeConversion) && (obj is DateTime)) { return obj; } if (engineFlags.HasFlag(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 != null) { if (testObject.GetType().IsAssignableToGenericType(typeof(Task<>), out var typeArgs)) { obj = typeof(TaskConverter<>).MakeSpecificType(typeArgs).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 (engineFlags.HasFlag(V8ScriptEngineFlags.EnableValueTaskPromiseConversion)) { TryConvertValueTaskToPromise(testObject, result => obj = result); } } } if (obj is HostItem hostItem) { if ((hostItem.Engine == this) && (hostItem.Flags == flags)) { return obj; } obj = hostItem.Target; } var hostTarget = obj as HostTarget; if ((hostTarget != null) && !(hostTarget is IHostVariable)) { obj = hostTarget.Target; } 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(); } } return HostItem.Wrap(this, hostTarget ?? obj, flags); } internal override object MarshalToHost(object obj, bool preserveHostTarget) { if (obj == null) { return UndefinedImportValue; } if (obj is DBNull) { return null; } if (MiscHelpers.TryMarshalPrimitiveToHost(obj, DisableFloatNarrowing, out var result)) { return result; } 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 scriptItem = V8ScriptItem.Wrap(this, obj); if (engineFlags.HasFlag(V8ScriptEngineFlags.EnableTaskPromiseConversion) && (obj is IV8Object v8Object) && v8Object.IsPromise) { return scriptItem.ToTask(); } return scriptItem; } internal override object Execute(UniqueDocumentInfo documentInfo, string code, bool evaluate) { VerifyNotDisposed(); return ScriptInvoke(() => { if ((documentNames != null) && !documentInfo.Flags.GetValueOrDefault().HasFlag(DocumentFlags.IsTransient)) { documentNames.Add(documentInfo.UniqueName); } if (inContinuationTimerScope || (ContinuationCallback == null)) { if (MiscHelpers.Exchange(ref awaitDebuggerAndPause, false)) { proxy.AwaitDebuggerAndPause(); } return ExecuteInternal(documentInfo, code, evaluate); } var state = new Timer[] { null }; using (state[0] = new Timer(unused => OnContinuationTimer(state[0]), null, Timeout.Infinite, Timeout.Infinite)) { inContinuationTimerScope = true; try { state[0].Change(continuationInterval, Timeout.Infinite); if (MiscHelpers.Exchange(ref awaitDebuggerAndPause, false)) { proxy.AwaitDebuggerAndPause(); } return ExecuteInternal(documentInfo, code, evaluate); } finally { inContinuationTimerScope = false; } } }); } 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(() => proxy.OnAccessSettingsChanged()); } #endregion #region ScriptEngine overrides (script-side invocation) internal override void ScriptInvoke(Action action) { VerifyNotDisposed(); using (CreateEngineScope()) { proxy.InvokeWithLock(() => ScriptInvokeInternal(action)); } } internal override T ScriptInvoke(Func func) { VerifyNotDisposed(); using (CreateEngineScope()) { var result = default(T); proxy.InvokeWithLock(() => result = ScriptInvokeInternal(func)); return 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 IJavaScriptEngine implementation uint IJavaScriptEngine.BaseLanguageVersion => 8; object IJavaScriptEngine.CreatePromiseForTask(Task task) { return CreatePromise((resolve, reject) => { task.ContinueWith(_ => CompletePromise(task, resolve, reject), TaskContinuationOptions.ExecuteSynchronously); }); } object IJavaScriptEngine.CreatePromiseForTask(Task task) { return CreatePromise((resolve, reject) => { task.ContinueWith(_ => CompletePromise(task, resolve, reject), TaskContinuationOptions.ExecuteSynchronously); }); } Task IJavaScriptEngine.CreateTaskForPromise(ScriptObject promise) { if (!(promise is V8ScriptItem v8ScriptItem) || !v8ScriptItem.IsPromise) { throw new ArgumentException("The object is not a V8 promise", nameof(promise)); } var source = new TaskCompletionSource(); Action onResolved = result => { source.SetResult(result); }; Action onRejected = error => { try { Script.EngineInternal.throwValue(error); } catch (Exception exception) { source.SetException(exception); } }; v8ScriptItem.InvokeMethod(false, "then", 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: JsonHelper /// public sealed class JsonHelper : JsonConverter { private readonly ScriptObject stringify; private readonly HashSet cycleDetectionSet = new HashSet(); /// public JsonHelper(ScriptEngine engine) { stringify = engine.Script.JSON.stringify; } /// public string ToJson(object key, object value) { key = MiscHelpers.EnsureNonBlank(key.ToString(), "[root]"); if (cycleDetectionSet.Contains(value)) { throw new InvalidOperationException($"Cycle detected at key '{key}' during JSON serialization"); } cycleDetectionSet.Add(value); 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 } }