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