-
Notifications
You must be signed in to change notification settings - Fork 38
Home
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 (after much experience I now realize I need to include a context type as well, which will come soon). 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.
To begin using V8.NET, you must do the following:
- 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.
- 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.
- In your source file, add “using V8.Net;”
- 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 = newV8Engine();
2. Handle result = v8Engine.Execute("/* Some JavaScript Code Here */", "My V8.NET Console");
3. Console.WriteLine(result.AsString); // (or "(string)result")
4. Console.WriteLine("Press any key to continue ...");
5. 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")
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.Proxy.Interface.x86.dll / V8.Net.Proxy.Interface.x64.dll Managed libraries that contain only interface methods for calling into the native proxy libraries. V8.NET.dll will “decide” which one to load based on the platform bit depth it is running in.
- V8.Net.SharedTypes.dll Managed library that contains only types needed between the various managed libraries. This is needed if you access certain enum and struct types (such as property type enums).
- V8.Net.dll This is the main V8.NET library, and is usually the only one you need to reference (though it will be a good idea to reference “V8.Net.SharedTypes.dll” as well).
When V8.NET.dll loads, it is designed to expect “V8.Net.Proxy.Interface.dll”, which will never exist because a property on the reference is set to not copy to local output. This forces a hook into the assembly resolver to locate the file, at which time the correct interface for the platform is loaded. The system also looks for x86\
and x64\
sub-folders, so you can also organize files that way as well (by putting only the respective DLLs into those folders of course).
Tip: Set 'V8Engine.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 assemblies 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{x86|x64}' folder paths in the target output. In this case, using the default 'V8.NET' value, the V8.Net interface resolver will consider the nested folder to find the required libraries.
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. Also, in effort to make things easier, a single context is used per engine. In the future multiple contexts per engine will be supported - you'll just have to activate the one you need beforehand.
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.
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 (thought 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. While this provides a huge performance boost, it comes at the cost of requiring its users to ensure “Dispose()” is called on it in order to reduce reference counts. It is safe to use the “using(){}” statement, or wrap code in “try..finally” blocks. For most cases, where speed is not an issue, it is recommended to use the “
Handle
” type.Warning: If using this type, you should never use the “=” operator to set a value. Since C# doesn’t support copy constructors, you have to call “{InternalHandle}.Set()” instead. If you use the “=” operator to copy this handle type, the system will not be aware of the copy, and the native handle may become disposed before it gets used. As well, when calling “Set()” to set a handle value, failing to call “Dispose()” on the old handle, if not needed, will result in memory leaks.
Exception to the rules: Internal handles used in callback parameters are disposed AUTOMATICALLY upon return from the callback method. Because of this, you can simply and safely initially use "=" to set either Handle or ObjectHandle (only) with an internal one.
Note: If you pass a "first" internal handle (for example, the first one returned by calling “
{V8Engine}.CreateInteger()
”) directly as an argument to another V8Engine method, it will be detected and dispose automatically, so upon return that handle will no longer be valid. To prevent this, simply make a copy of the handle value using “{InternalHandle}.Set({InternalHandle})
” or “{InternalHandle}.Clone()
” as needed, or pass into the constructor. -
Handle / ObjectHandle
This is an InternalHandle wrapper (“InternalHandle” is a value type), 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). This allows developers to use the garbage collector to clean up handles automatically when no longer needed.Tip: You don’t have to keep creating these objects – instead, feel free to create them only once and call “Set()” instead (same rule of thumb for “InternalHandle” values).
Warning: If using this type, you should never use the “=” operator to make copies of the handle, unless you know what you are doing. Since C# doesn’t support copy constructors, you have to call “{Handle}.Set()” instead. If you use the “=” operator to copy this handle type you will be copying only the reference to the handle object, and V8.NET will not be aware of it. If many references point to the same handle instance, calling “Dispose()” on ANY one of them will dispose all of them.
Callback functions support using handles as parameters to prevent the system trying to convert them to CLR types (and are required if you wish to receive JS-specific types, such as JS function references). In this special case, “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.
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. That said, if speed is important, or there’s a need to create an enormous number of handles, you can use the stack created “InternalHandle” which will prevent creating objects for the garbage collector to deal with; however, you have to make a copy of the handles, and they have to be manually disposed.
1. var handle = v8Engine.CreateInteger(0);
In the case above, the handle is of type “InternalHandle”. There are two main things to keep in mind: 1. The handle must be manually disposed, and 2. The handle is marked as a “first” handle, which means it will be destroyed automatically if passed directly into another V8Engine method that accepts those handle types. There are three main ways to 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(); }
If a copy of a handle is needed, then you must “set” it, and not “assign” it.
1. internalHandle.Set(anotherHandle);
2. // ... OR ...
3. var internalHandle = anotherHandle.Clone();
4. // ^Note: 'Clone()' is only valid when initializing a variable,
5. // and NEVER to replace an existing handle that is not empty; thus:
6. internalHandle = anotherHandle2.Clone(); // WRONG - 'internalHandle' is now a memory leak.
In both these cases, a proper copy of the handle is made. A counter on the native side keeps track of the number of handles, and is only released when all handles on the managed side are disposed. As long as one handle copy exists, disposing one will not release the native handle. Again, to make things easier, just use the Handle
type so it can dispose the internal handles using the GC (unless you are confident to use the internal ones).
Note: On line 1, when “Set()
” is called, if “anotherHandle
” is an InternalHandle
value directly returned from a V8Engine method (such as “CreateInteger()”), making a copy of it will immediately dispose the given handle. If you recall previously mentioned further above, all InternalHandles returned by the engine are marked as “first” handle. As such, in this case, anotherHandle
would be disposed, and internalHandle
will now contain it. You are also then responsible to dispose of it when no longer needed.
The following is an example of what NOT to do:
1. var handle = v8Engine.CreateInteger(0);
2. var handle2 = handle;
This example makes a copy of the handle (an “InternalHandle” type); however, because “Set()” was not used, disposing one will immediately invalidate the other as well, and the native handle will no longer be valid. As a rule of thumb, never use the “=” operator on ANY handle type, unless this behaviour is desired. The same issue will occur for “Handle” types as well for the same reason.
In regards to “InternalHandle” types, here’s another example of what NOT to do:
1. handle.Set(anotherHandle.Clone());
Clone()
creates a new copy of anotherHandle
(incrementing an internal handle counter in the process) and passes the new copy to the “Set()” method (not the original handle). At this point there are now two handles (the original and the copy). The “Set()” method will first dispose of any existing handle reference in handle
, and then make another copy of the copy passed in, effectively resulting in 2 copies (besides the original one). The first copy of the original is never disposed, causing a memory leak. It’s important to use either one or the other as needed, but not both.
Garbage collection is a challenge to deal with, as normally you try never touch the 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 manage side. These are the usual rules/steps:
-
All active handles have an internal proxy field 'Disposed' set to 0. The
{V8Engine}.Handles_Active
property will return a list of these handles (no need to dispose the handles returned from this list). -
An 'IV8Disposable' CLR object that loses all references to it will call 'this.Finalizing();' to have itself added to a disposal queue (only if necessary, otherwise nothing happens [eg. 'Handle' objects always finalize immediately, and add an InternalHandle value to the disposal queue instead]). This is required, as calling into V8 from the finalizer thread to dispose native side V8 handles can cause deadlocks (Google V8 does not support multiple threads at the same time). Once in the disposal queue, an internal proxy field 'Disposed' is set to 1, which indicates that the managed side is now ready to dispose of the managed object. These handles can be inspected through
{V8Engine}.Handles_ManagedSideDisposeReady
. -
The worker thread reads from the disposal queue and checks the '{IV8Disposable}.CanDispose' property. If true, it will call '{IV8Disposable}.Dispose()' on the object (in essence, acting like a finalizer). If false, then it is assumed that the object is still needed by the native V8 side. In such case it is assumed that the object was "abandoned" on the manage side (no more CLR references), and is then added to an "abandoned" queue. If this is found to be a 'V8NativeObject', then the native V8 handle is marked as "weak", and the proxy ''Disposed' value is set to 2, which means that the native V8 engine will call back to dispose the object later on when no other V8 handle exists (note: just because there's no JavaScript reference doesn't mean V8 or a native side proxy object doesn't still have a reference for some reason). These handles can be inspected through '{V8Engine}.Handles_NativeSideWeak'. Items that dispose successfully (usually InternalHandle values) can be inspected through
{V8Engine}.Handles_DisposedAndCached
. -
The queue of abandoned objects are scanned periodically (in timed short stages) to see if they can be disposed by checking the '{IV8Disposable}.CanDispose' property. Most objects that make it here are 'ObjectTemplate' and 'FunctionTemplate' objects (though others are possible). Those template objects must remain as long as any objects created from them still exist, otherwise those objects will no longer be valid. As template created objects are disposed, a reference counter is updated on the template object. When this is 0, the template object will then dispose successfully.
-
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.
-
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.
-
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.SharedTypes.dll (if referenced)
- bin\V8.Net*.dll V8.Net.dll will look for the "V8.NET" folder in the 'bin' folder for the other DLLs, and if found, will use that instead to locate 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.
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.