Skip to content
James edited this page Sep 4, 2019 · 15 revisions

Overview

It’s important to understand a few things before using V8.NET. The original premise to creating V8.NET was to create a wrapper that allows the user to control V8 from the managed side – at least to the degree that allows the user to easily integrate their own managed classes. The idea was not to wrap ALL V8 objects with managed ones, but only key ones, such as ObjectTemplate, FunctionTemplate, “Function”, “Object”, and the V8 handles. While many methods on the V8 objects also have managed side counterparts (with the same names), many new methods are added to support the managed-side specific implementation. All that said, the goal does include progressively exposing more and more of the native V8 side to the managed world over time.

The source code comments and this documentation will use the terms “Managed Object” to describe managed ‘IV8NativeObject’ objects, and “Native V8 Object” to describe the objects within the V8 engine.

Note: The source and documentation comments expect you to understand basic V8 concepts, so it is encouraged to read through the Google V8 Embedder’s Guide. Also, if you see curly brackets like {SomeType} it simply refers to an instance of the type.

Quick Start

To begin using V8.NET, you must do the following:

  1. First, if using Windows, you may need to unblock the downloaded file (right click, and select properties). If you don't, you will run into security issues trying to load and run the DLLs.
  2. Add a reference to the V8.Net.dll and V8.Net.SharedTypes.dll to your project, that's all. Make sure the other x86 and x64 interface and proxy CLR DLLs end up in the target output folder of your host project. They will be dynamically loaded depending on the OS architecture. The v8.Net.dll (compiled with "any CPU") will decide which one to load.
  3. In your source file, add “using V8.Net;”
  4. Done. 8)

To start using it, just create an instance of V8Engine and execute commands within a V8 scope. Here’s a simple “Console” application example.

1.  var V8Engine = new V8Engine(); // (note: you can pass in "false" to create your own context)
2.  v8Engine.Execute("function foo(s) { /* Some JavaScript Code Here */ return s; }", "My V8.NET Console");
3.  Handle result = v8Engine.DynamicGlobalObject.foo("bar!");
4.  Console.WriteLine(result.AsString); // (or cast using "(string)result")
5.  Console.WriteLine("Press any key to continue ...");
6.  Console.ReadKey();

Another quick start example can be found here: https://gist.github.com/rjamesnw/5ee5a0a2a769b321e1d0

Command line options for V8 are now supported. You can use '{V8Engine}.SetFlagsFromCommandLine(string flags)' to set one or more flags (space separated).

Definition of options: https://code.google.com/p/v8/source/browse/branches/bleeding_edge/src/flag-definitions.h

Example: {V8Engine}.SetFlagsFromCommandLine("--use_strict")

DLL Files Required For Your Projects

All of these files MUST exist in your "bin\Debug|Release" folders.

  • V8_Net_Proxy_x86.dll / V8_Net_Proxy_x64.dll Native libraries use used to support P/Invokes (contains native proxy wrappers). These are pure native wrappers only (no mixed CLR code).

  • V8.Net.dll This is the main V8.NET library, and is the only one you need to reference.

    Note: There were 4 CLR libraries, but using a new native loading method the shared library and 2 other interface libraries are no longer required. All code is now consolidated into a single assembly.

How the libraries load

When V8.NET.dll loads, it looks for the native libraries. It has a loader class that searches commonly known locations, such as the current working folder, the DLL location (bin and nuget cache), x64 and x86 sub-folders, and so on.

Tip: Set Loader.AlternateRootSubPath to a sub-directory (defaults to 'V8.NET') to act as an alternate sub-folder to check for. For instance, if using Visual Studio, you could create a "V8.NET" folder in your project, then dump the V8.NET libraries there, and set all files to "Copy if newer". This may help when deploying web solutions, as the IDE will know of the files. The IDE will create 'bin{Debug|Release}\V8.NET{optional:x86|x64}' folder paths in the target output. In this case, using the default 'V8.NET' value, the V8.Net native library loader will consider the nested folder to find the required libraries, if need be.

Thread Safety and Scopes

While the Google V8 engine is not thread safe, it does have "isolates" to block calls while another is in progress (so many threads can use it, but only one thread can call in at a time). The "scopes" have also been abstracted, which makes everything a bit easier on the CLR side.

The Global Object

The global object is the root executing environment where global properties are stored. This can be found on the managed side in two ways:

  • {V8Engine}.GlobalObject – This is a reference of type IV8NativeObject, which provides methods for editing properties. This is a simple wrapper around the native V8 context object.
  • {V8Engine}.DynamicGlobalObject – This is the same reference as the first one, EXCEPT it is type-cast to “dynamic” for you! You can simply store properties on it like “{V8Engine}.DynamicGlobalObject.x = 0”.
    Note: When accessing dynamic properties, they will usually be of type “InternalHandle”; however, if the handle represents a managed object, the managed object will be returned instead. If you know this in advance, you can simply type cast directly to the managed object type. If not, then no worries, since the manage types implemented by V8.NET implicitly convert to “InternalHandle” types anyhow.

If needed, you can create a custom global object using an ObjectTemplate. You must first pass false to the engine constructor, create and configure the object template, then call {V8Engine}.CreateContext() with the template. Once you have a context you can assign it as the current context using {V8Engine}.SetContext().

Handles

Handles wrap native V8 handles and are used to keep track of them. V8 handles are never disposed (cached) until they are no longer in use, so it is important to make sure to dispose of handles when they are no longer needed. Fortunately, you can use the “Handle” type and let the garbage collector take care of it for you, or call “Dispose()” yourself if you’d like to release it back more quickly. There are two types of handles (though they both function nearly the same):

  • InternalHandle

    This is a value type that is used internally for wrapping marshalled handles on the native side. This is done so a native handle can quickly be wrapped by a stack-bound value instead of creating objects on the heap for the GC to have to deal with. Stack-bound handles have great speed advantages when V8 interceptors call back into the managed side via large loops executed in script. Outside of call-back methods, it is safe to use the “using(){}” statement, or wrap code in “try..finally” blocks if desired (to force disposal instead of waiting for the GC to do it later). For most cases, where speed is not an issue, it is recommended to use the “Handle” type.

    Exception to the rules: Internal handles used in callback parameters are disposed AUTOMATICALLY upon return from the callback method. Because of this, you should not dispose those yourself.

  • Handle / ObjectHandle
    This is an InternalHandle wrapper (InternalHandle is a value type, while Handle is a class), and is the type most developers should be using, unless speed is an issue. If at any time you get an InternalHandle type returned, you should either type-cast it to Handle or ObjectHandle to create a variable of type Handle instead (use "ObjectHandle" to represent script objects - it has object-related methods).

    Note: Handle is also the based type for all managed side objects that represent native V8 objects.

Handles also come with many useful functions, such as calling functions by name given as a string. It would be wise to review all the features available to you, since most everything revolves around handles.

Handles in Callback Functions

Callback functions are passed InternalHandle values. InternalHandle parameter values don’t need to be disposed. Anytime a callback is invoked, the internal handle arguments passed to the method are automatically disposed when the method returns. If you need to persist the value, make a copy of it using .Set() or .KeepTrack().

Examples:

The following are two examples of how to use handles.

1.  Handle handle = v8Engine.CreateInteger(0);
2.  var handle = (Handle)v8Engine.CreateInteger(0);

In both cases, the InternalHandle value returned is converted to a handle object so that the native handles can be disposed when they are no longer needed. This is the recommended way to use handles. If you keep the InternalHandle value returned by one of the Create????() engine methods (excluding those that create managed objects) you are then responsible to dispose of them, otherwise you'll have memory leaks. Calling .KeepTrack() on internal handles adds a "tracker" to the internal handle so the GC can claim it later. If you keep track and dispose the values yourself you can save the GC some work. ;) Note that .KeepTrack() and Handle h = {some internal handle) are much the same thing, so this would not be much different

1.  Handle handle = v8Engine.CreateInteger(0);
2.  InternalHandle handle = v8Engine.CreateInteger(0).KeepTrack();

There are three main ways to forcibly dispose handles (of any type):

1.  var internalHandle = v8Engine.CreateInteger(0);
2.  // (... do something with it [the next 2 below is better in case of errors] ...)
3.  internalHandle.Dispose();
4.
5.  // ... OR ...
6.
7.  using (var internalHandle = v8Engine.CreateInteger(0))
8.  {
9.      // (... do something with it ...)
10. }
11.
12.  // ... OR ...
13.
14.  var internalHandle = InternalHandle.Empty; (optional to initialize)
15.  try
16.  {
17.      internalHandle = v8Engine.CreateInteger(0);
18.      // (... do something with it ...)
19.  }
20.  finally { internalHandle.Dispose(); }

Implicit Type Binding

V8.Net supports binding CLR types to the V8 engine. A "Type Binder" is a wrapper the reflects over and caches type information. An "Object Binder" is a CLR object created FROM a "Type Binder" that gets a V8 Native Object assigned to it for your custom type. When JavaScript access properties on the native object, callbacks are executed from V8 to the CLR side to read the CLR object members. The object binder uses the type binder assigned to it to create "getters" and "setters" for the instance (among other things; as needed, and then cached also).

Note: It would not make sense to inherit from V8NativeObject (or other derived type) and then register your type with the type binder. A V8NativeObject already gets a native object assigned to it (if you create it via engine.CreateObject<T>()), so you'd actually end up with two native objects (and odd behavior). Also, calling new V8NativeObject_ORDerivedType() is not supported (you must use the engine to create an instance).

Typically, binding follows this pattern:

  1. When you call {V8Engine}.RegisterType(typeof(MyClassType)), a TypeBinder instance is created to wrap the type by analyzing all data members and inherited based types. This information is cached internally for quick lookup later.
  2. When you assign a type (i.e. engine.GlobalObject.SetProperty(typeof(MyClassType)), a property named 'MyClassType' gets created/set with a special function (from TypeBinder) that represents the STATIC side of the type. That function can be used to create instances of the CLR type.

Example: https://gist.github.com/rjamesnw/6a0b107c22a4b702d8de45944959be62

You don't need to register types first. Calling engine.GlobalObject.SetProperty(typeof(MyClassType)) will do so automatically; however, recursive type inclusion is not enabled by default, which will protect against exposing all nested types. RegisterType() has a flag that allows including all nested types automatically.

The type binder does not iterate over all nested types when a type becomes registered. Types are only registered (reflected over and cached) as they are seen/accessed, unless you manually registered expected types in advance.

Garbage Collection

Garbage collection is a challenge to deal with, as normally you try never touch the Garbage Collector (GC) and work around it. However, exposing managed objects to the native side means the managed side GC has no idea about native side V8 handles. V8 does have a GC itself, but as well, it also has no idea about managed objects. To make this work it is necessary to hook into the GC finalization process on both sides in order to coordinate the disposal of objects. One rule of thumb is that the managed side owns ALL wrappers to native proxy data, so disposal must start on the managed side. These are the usual rules/steps:

  1. InternalHandle tracks a native side handle. It is not an object, so the GC does not get involved. This also means you will cause memory leaks if you do not dispose of it. An InternalHandle value implicitly converts to a Handle value for tracking purposes (to prevent memory leaks in case you forget, but that's up to you).

  2. Once you dispose an object it is immediately released back into a native queue/cache for reuse. It may also be queued for disposal instead, if a script calls the managed side that then calls back to dispose handles (as a safety precaution).

  3. Managed objects that are associated with native objects will dispose the native side automatically when the managed object is collected by the GC. This means that YOU are normally responsible to maintain a rooted reference to an object so the GC doesn't claim it. That said, there is a trick to keep a managed side object alive without needing to keep track yourself. Calling .KeepAlive() will root an object and make the native handle weak. As soon as the V8 engine no longer has any other references, a registered native GC callback will call back into the managed side and clear the rooted reference, allowing the managed object to be claimed by the GC. This neat feature allows you to create objects on the fly for the engine without needing to store references (which is how the auto-type-binding system works). Potentially you could also use .KeepAlive() to create static singletons that scripts can access, and by using V8PropertyAttributes.Locked you could prevent it from being removed/overwritten in script.

Integration into ASP.NET:

  1. Create a folder specifically named "V8.NET", or change the name using 'Loader.AlternateRootSubPath'.

    Note: There is a test project in the source with the source which uses a PRE-Build event that copies files to "$(ProjectDir)V8.NET" - this must match 'Loader.AlternateRootSubPath'.

    Note: Loader.AlternateRootSubPath is set to "V8.NET" by default, but you could change it to "lib"/"libs" for example, if you have other libraries all in one folder.

  2. For all DLLS in the "$(ProjectDir)V8.NET" folder, change "Copy to Output Directory" to "Copy if newer". This should also tell Visual Studio that this content is required for the application.

  3. Set any "$(ProjectDir)V8.NET*.DLL" files that you have REFERENCED to "Do not copy" - the build action will copy referenced DLLs by default.

    When setup correctly, Visual Studio will replicate the folder structure for the "V8.NET" folder into the 'bin' folder, and the referenced DLLs will end up in the ROOT of the 'bin' folder. Thus, you will have this folder structure:

    • bin\V8.Net.dll
    • bin\V8.Net*.dll The V8.Net.dll Loader class will look for the "V8.NET" folder (or other if specified) in the 'bin' folder for the other DLLs, and if found, will use that instead to locate the dependent libraries.

    Note: ASP.NET may shadow copy some DLLs in the 'bin' folder (http://goo.gl/vXbwGp). This means that those DLLs may end up elsewhere in a temporary folder during runtime.

More Help

There is much more documentation on all classes and methods throughout the assemblies (and source). As well, please open the program.cs file of the Console project and review it for examples. There are some very neat examples of integration with the .Net side. I encourage you to also run the console app and watch what it outputs to the screen, then take a look at the source behind the outputs. It will show examples, and allow you to play around with the environment to get a "handle" on it. ;) If you still need more help, feel free to start a discussion.

In addition, there are example projects under a solution folder called "Test Projects" (note: due to recent changes some may be missing or not be working as of March 6, 2019):

  • V8.Net-Console: A demo console I use mainly for unit-testing, but contains may examples as well.
  • V8.NET-Console-ObjectExamples: Examples on how to integrate your own types.
  • ASPNetTest: Example ASP.NET project.
  • WCFServiceTest: Example WCF service project.