v8i: C++ Schnittstelle/Wrapper-Template für die JavaScript-Engine V8
v8i: C++ interface / wrapper class template for the V8 JavaScript engine
Eine Template-basierte (Wrapper und Proxy-) Schnittstelle zur Integration
der Google V8 JavaScript-Engine in C++ Anwendungen: (namespace) v8i
.
(GIT: git clone http://atwillys.de/git/v8i.git
).
Der Fokus dieser Implementation liegt auf der Einbindung von Scripting-Features
in Software, deren Funktionalität von C++-Code dominiert wird (siehe auch v8juice).
Da V8 ein kontinuierlich weiterentwickeltes Projekt ist, gehen mit Optimierungen
und neuen Features häufig auch Änderungen der API einher - was für Programme,
bei denen die Scripting-Engine ein untergeordnetes Featureset darstellt, zu hohem
Wartungsaufwand in der Schnittstelle führt. Um diesen zu reduzieren bietet die hier
beschriebene "Ein-Datei-Bibliothek" eine Umsetzung zwischen der "duck typed,
garbage collected world of JavaScript" und strikt typisiertem c++. D.h. zum
Definieren und Evaluieren von Variablen, Konstanten, Objekten, Funktionen
(auch "native" Callbacks) in der Skript-Runtime müssen keine V8-spezifische
Blöcke impementiert werden. Durch die Pallette von Features, die mit c++11
eingeführt wurden, konnte die Schnittstelle auf wenige Objekte und mehrfach
überladene Methoden begrenzt werden.
Beispiel:
#include <v8i.hh> /* core */
#include <v8istd.hh> /* some standard functions like console.log */
#include <iostream>
#include <vector>
using namespace std;
//
// Callback for "myNs.func"
//
void my_function_callback(v8i::function_callback_info&& cbi)
{
cerr << "[c++] myNs.func num of callback arguments: " << cbi.args.length() << endl;
if(cbi.args.length() != 2) {
// That will be translated into a JS runtime exception ("Error")
throw v8i::callback_error("Need two numeric arguemnts");
}
double a = cbi.args.get<double>(0); // get arg 0 or throw if not convertible to double
double b = cbi.args.get<double>(1); // get arg 1 or throw if not convertible to double
cbi.return_value(a+b); // return converted from double to Number
// You also have access to the engine where the call comes from, e.g.
cbi.engine().define("myNs.callack_called", true);
}
//
// Instance used for our native object
//
struct my_class
{
int counter = 0; // An instance variable
// A method callback that has access to the my_class instance:
static void count(v8i::method_callback_info<my_class>&& cbi)
{
// That is a shared pointer, the cbi object throws an exception if that pointer is not valid.
auto inst = cbi.instance();
cerr << "[c++] count is: " << inst->counter << endl;
cbi.return_value(++(inst->counter)); // That is the counter variable above.
return;
cbi.return_this(); // You could also return the JS object reference ...
}
};
//
// Manages a V8 instance with own isolate and global context. Used in main() below.
//
v8i::engine js;
//
// MAIN
//
int main(int argc, char** argv)
{
js.initialize(); // Every engine must be initialised before using it.
v8i::standard_functions::define_console(js); // `console` from header v8istd.hh, saves work here.
// By default all defines are immutable/constant, not deletable, and not enumerable.
js.define("myNs"); // Define a empty plain object in the runtime, here used as "namespace".
js.define("myNs.version", "1.0"); // Constant string, e.g. "console.log(myNs.version);"
js.define("i", 0, v8i::engine::defopt_mutable); // That is equivalent to "var i=0;"
js.define("myNs.pi", 3.1415); // ... same with floating point - well, quite inaccurate PI.
js.define("myNs.coeffs", vector<float> {1,2,3,4,5} ); // ... or Arrays ...
js.define("myNs.func", my_function_callback); // Define a "native" function
// Define a constructor MyClass that wrapps c++ instances of my_class, and specify
// the callbacks we export as methods:
js.define<my_class>("MyClass", { /*Method list*/ { "count", my_class::count } });
// Eval, include, call
js.eval("i=10;"); // Evaluate JS code: The above defined `i` is now 10.
js.include("intro.js"); // Let V8 compile and run a script from a file
js.call("nativeCounter"); // call the JS function that uses the MyClass object a bit
double r = js.call<double>("square", 100); // Call a runtime function, fetch result as double
cout << "[c++] fetched square(100) == " << r << endl;
try {
js.call("throwingFunction");
} catch(const v8i::js_exception& e) {
cout << "[c++] v8i::js_exception (see next line)" << endl << e.what() << endl;
}
return 0;
}
// Included JavaScript file (intro.js)
//
function square(num) { // Function called from c++
console.log("[js ] square(",num,") called");
return num * num;
}
function nativeCounter() { // Deal a bit with a "native object" instance
var obj = new MyClass();
console.log("[js ] obj.count() ==", obj.count());
console.log("[js ] obj.count() ==", obj.count());
console.log("[js ] obj.count() ==", obj.count());
}
// We provoke an exception by calling myNs.func with a value that it cannot convert to double:
function throwingFunction() {
myNs.func(10,{x:11});
}
console.log("[js ] i ==", i); // Print the defined i
console.log("[js ] myNs.version ==", myNs.version); // Print the defined string
console.log("[js ] myNs.pi ==", myNs.pi); // Print the defined float/double
console.log("[js ] myNs.coeffs ==", JSON.stringify(myNs.coeffs)); // Print the defined Array
console.log("[js ] Result of myNs.func(10,11) ==", myNs.func(10,11)); // Call c++ function
Ausgabe:
[js ] i == 10
[js ] myNs.version == 1.0
[js ] myNs.pi == 3.1415
[js ] myNs.coeffs == [1,2,3,4,5]
[c++] myNs.func num of callback arguments: 2
[js ] Result of myNs.func(10,11) == 21
[c++] count is: 0
[js ] obj.count() == 1
[c++] count is: 1
[js ] obj.count() == 2
[c++] count is: 2
[js ] obj.count() == 3
[js ] square( 100 ) called
[c++] fetched square(100) == 10000
[c++] myNs.func num of callback arguments: 2
[c++] v8i::js_exception (see next line)
Javascript exception: "Error: 'Object' not convertible to double" in file "intro.js" line 17
|
| 17: myNs.func(10,{x:11});
| _______^
Stack trace:
Error: 'Object' not convertible to double
at Error (native)
at throwingFunction (intro.js:17:8)
... aber ...
Es gibt immer ein "aber"; Obwohl die Verwendung von Templates und Inlining
Compilern eine gute Optimierung ermöglicht, kommt diese Schnittstelle mit
einem Overhead und damit einem Leistungsverlust, auf den ich hinweisen möchte.
Er tritt dann in Erscheinung, wenn Callbacks mit wenig Code oft aufgerufen
werden, z.B. for(var i=0; i<1000000; i++) { myNativeFunction(); }
, oder
aus c++ for(int i=0; i<1000000; ++i) { js.call("something"); }
.
Ein Teil davon ist unumgänglich durch die V8 bestimmt, d.h. es müssen
HandleScope
s angelegt werden, Information von V8-Datenformaten in c++
umgewandelt werden, TryCatch
-Vorbereitungen getroffen werden, das Isolate
muss evtl. abgesperrt werden, usw. Ein anderer Teil "verfliegt" durch
Compileroptimierungen - aber es gibt einen Teil, der bleibt: Zwischen den
c++-Callbacks und der JS Runtime ist eine Proxyfunktion geschaltet, die das
cbi
-Objekt vorbereitet, immer einen try-catch-Block enthält, und die c++
Callbackadresse nachschlagen muss. Normalerweise ist dieser Aufwand
vernachlässigbar, nicht jedoch beim Durchiterieren. Daher empfiehlt es sich,
native Funktionen zu exportieren, die größere Datenmengen mit einem Aufruf
verarbeiten. z.B. für ein natives Objekt, hinter dem sich ein std::vector<double>
versteckt, Methoden wie MyVector.sum()
, MyVector.mean()
, MyVector.min()
usw.
Was ist es, was nicht ...
Bei dieser Implementation geht es darum, die Flexibilität eines c++ Programms zu erhöhen, nicht ein Framework für JavaScript-Programme zu erstellen - dieses Rad ist schon mit NodeJS und Co. erfunden und gut implementiert. In eigenen Anwendungen ziehle ich mehr darauf ab, dynamische bzw. "freiprogrammierbare" "Hooks" in JavaScript zur Verfügung zu stellen, d.h. Konfigurieren, auf Plattformen abgestimmem, Sequenzen festgelegen, Sachverhalte nochmals gegengeprüfen, usw. Auch bei kleinen sequenziell ablaufenden Programmen (für das auf Asynchronizität ausgelegte Javascript eigentlich ungewöhnlich) ist dieses Interface recht hilfreich. Dabei sollte erwähnt sein, dass bei statischem Linken der V8 die kleinen Programme nicht mehr so klein sind (>=18MB).
Mit anderen Worten: Prüfe, ob Deine Anwendung nicht ein Plugin/Extension für NodeJS ist.
Abhängigkeiten
Um diese V8 Schnittstelle zu verwenden wird benötigt:
- Die Headerdatei
v8i.hh
. - Der Compiler mit
std=c++11
kompilieren. - Die Google V8 Engine muss mindestens
Version >= 3.29
sein.
Tests wurden durchgeführt mit:
g++
,clang++
- Linux, FreeBSD
- 32bit, 64bit Maschinen
- Kompiliert auch mit VS2013-Express (nach einigen Adaptionen),
leider waren Tests damit jedoch nicht möglich, da zum Linken
auch
v8.lib/v8.dll
benötigt wird - lassen Sie es mich wissen ;).
"@see also" ...
Andere Implementierungen, "embedded" Scriptsprachen, und "useful links":
- V8 Project page Google V8 JavaScript Engine
- V8 Developers page V8 Einführung
- v8juice / V8Convert / cvv8 Ähnliche Implementierung wie
v8i
. - NodeJS V8 basierte JS platform.
- SpiderMonkey Mozilla JavaScript Engine
- JSC (Webkit Command-Line-Tool )
- jsish JavaScript Interpreter.
- SEE JavaScript Interpreter (32bit, 2009)
- LUA Leightgewichtige alternative Skriptsprache für C++-Embedding.
Dokumentation:
(Die Dokumentation belasse ich mal in Englisch):
Template based single-header-file-interface to the V8 JavaScript engine with strong
API separation: (namespace) v8i
. (GIT: git clone http://atwillys.de/git/v8i.git
).
Purpose of it is allowing to embed scripting functionality to a software dominated by c++ code. As V8 is a project continuously pushed forward, interfacing to c++ code and testing is maintenance intensive. To overcome these interfacing issues, this class template provides a basic glue layer functionality to strongly separate the V8 engine. Hence, changes in V8 have major effect only in this header file and not (or only minor) in the native interfacing code.
Example:
#include <v8i.hh> /* core */
#include <v8istd.hh> /* some standard functions like console.log */
#include <iostream>
#include <vector>
using namespace std;
//
// Callback for "myNs.func"
//
void my_function_callback(v8i::function_callback_info&& cbi)
{
cerr << "[c++] myNs.func num of callback arguments: " << cbi.args.length() << endl;
if(cbi.args.length() != 2) {
// That will be translated into a JS runtime exception ("Error")
throw v8i::callback_error("Need two numeric arguemnts");
}
double a = cbi.args.get<double>(0); // get arg 0 or throw if not convertible to double
double b = cbi.args.get<double>(1); // get arg 1 or throw if not convertible to double
cbi.return_value(a+b); // return converted from double to Number
// You also have access to the engine where the call comes from, e.g.
cbi.engine().define("myNs.callack_called", true);
}
//
// Instance used for our native object
//
struct my_class
{
int counter = 0; // An instance variable
// A method callback that has access to the my_class instance:
static void count(v8i::method_callback_info<my_class>&& cbi)
{
// That is a shared pointer, the cbi object throws an exception if that pointer is not valid.
auto inst = cbi.instance();
cerr << "[c++] count is: " << inst->counter << endl;
cbi.return_value(++(inst->counter)); // That is the counter variable above.
return;
cbi.return_this(); // You could also return the JS object reference ...
}
};
//
// Manages a V8 instance with own isolate and global context. Used in main() below.
//
v8i::engine js;
//
// MAIN
//
int main(int argc, char** argv)
{
js.initialize(); // Every engine must be initialised before using it.
v8i::standard_functions::define_console(js); // `console` from header v8istd.hh, saves work here.
// By default all defines are immutable/constant, not deletable, and not enumerable.
js.define("myNs"); // Define a empty plain object in the runtime, here used as "namespace".
js.define("myNs.version", "1.0"); // Constant string, e.g. "console.log(myNs.version);"
js.define("i", 0, v8i::engine::defopt_mutable); // That is equivalent to "var i=0;"
js.define("myNs.pi", 3.1415); // ... same with floating point - well, quite inaccurate PI.
js.define("myNs.coeffs", vector<float> {1,2,3,4,5} ); // ... or Arrays ...
js.define("myNs.func", my_function_callback); // Define a "native" function
// Define a constructor MyClass that wrapps c++ instances of my_class, and specify
// the callbacks we export as methods:
js.define<my_class>("MyClass", { /*Method list*/ { "count", my_class::count } });
// Eval, include, call
js.eval("i=10;"); // Evaluate JS code: The above defined `i` is now 10.
js.include("intro.js"); // Let V8 compile and run a script from a file
js.call("nativeCounter"); // call the JS function that uses the MyClass object a bit
double r = js.call<double>("square", 100); // Call a runtime function, fetch result as double
cout << "[c++] fetched square(100) == " << r << endl;
try {
js.call("throwingFunction");
} catch(const v8i::js_exception& e) {
cout << "[c++] v8i::js_exception (see next line)" << endl << e.what() << endl;
}
return 0;
}
// Included JavaScript file (intro.js)
//
function square(num) { // Function called from c++
console.log("[js ] square(",num,") called");
return num * num;
}
function nativeCounter() { // Deal a bit with a "native object" instance
var obj = new MyClass();
console.log("[js ] obj.count() ==", obj.count());
console.log("[js ] obj.count() ==", obj.count());
console.log("[js ] obj.count() ==", obj.count());
}
// We provoke an exception by calling myNs.func with a value that it cannot convert to double:
function throwingFunction() {
myNs.func(10,{x:11});
}
console.log("[js ] i ==", i); // Print the defined i
console.log("[js ] myNs.version ==", myNs.version); // Print the defined string
console.log("[js ] myNs.pi ==", myNs.pi); // Print the defined float/double
console.log("[js ] myNs.coeffs ==", JSON.stringify(myNs.coeffs)); // Print the defined Array
console.log("[js ] Result of myNs.func(10,11) ==", myNs.func(10,11)); // Call c++ function
Program output:
[js ] i == 10
[js ] myNs.version == 1.0
[js ] myNs.pi == 3.1415
[js ] myNs.coeffs == [1,2,3,4,5]
[c++] myNs.func num of callback arguments: 2
[js ] Result of myNs.func(10,11) == 21
[c++] count is: 0
[js ] obj.count() == 1
[c++] count is: 1
[js ] obj.count() == 2
[c++] count is: 2
[js ] obj.count() == 3
[js ] square( 100 ) called
[c++] fetched square(100) == 10000
[c++] myNs.func num of callback arguments: 2
[c++] v8i::js_exception (see next line)
Javascript exception: "Error: 'Object' not convertible to double" in file "intro.js" line 17
|
| 17: myNs.func(10,{x:11});
| _______^
Stack trace:
Error: 'Object' not convertible to double
at Error (native)
at throwingFunction (intro.js:17:8)
... but ...
There is always a "but": Although the template nature of the implemented classes
allows good optimisations, this interface comes with a slight overhead - callback proxies
- which means that each JS call to a "native" function or method implies function
pointer lookup to the callback that you implement. Hence, in applications where
small calls (e.g. getters/setters) are extensively used in JS runtime loops, a
performance loss from this overhead can be measured compared to direct V8 callbacks.
For this reason it is possible to define V8 callbacks in this engine wrapper, too -
but the necessity of such JavaScript implementations may point out that a native function
for a task is missing. E.g. you have a std::vectorvar sum = mvvector.sum()
.
What is it, what not
The essence: In this implementation the purpose of JavaScript that is aimed for is to provide flexibility for a c++ program, not to provide a native framework for JavaScript software - these wheels are already invented and well implemented, e.g. in NodeJS. So, please check if your application is more a plugin/extension for NodeJS rather than a standalone program.
Dependencies
To use this interface, you need:
- The header file
v8i.hh
. - Compilation with
std=c++11
. - Die Google V8
version >= 3.29
.
Tested with:
g++
,clang++
- Linux, FreeBSD
- 32bit, 64bit machines
- After applying some adaptions the template compiles with VS2013
Express, too. However, as linking requires
v8.lib/v8.dll
, which obviously requires the non-free VS version, tests were unfortunately not possible - let me know ;)
See also
Similar implementations, embedded scripting languages, useful links:
- V8 Project page Google V8 JavaScript engine
- V8 Developers page V8 Introduction
- NodeJS V8 based server side JS platform.
- v8juice / V8Convert / cvv8,
like
v8i
a V8 wrapping for c++ - also check that one out, if you prefer it. - SpiderMonkey JavaScript engine
- JSC (Webkit command line tool )
- jsish JavaScript interpreter.
- SEE JavaScript interpreter (32bit, last updated approx. 2009)
- LUA Lightweight embedded scripting language.
Usage
All exported elements related to this implementation are accessible via
the namespace ::v8i
. Main class is v8i::engine, of which you can instantiate
one or more objects. Each v8i::engine instance works with its own V8 isolate,
context and global object. Having an initialised instance of v8i::engine, you
can define constants/variables, functions and objects for this instance, evaluate
JS code, call functions and methods, and include '.js' files.
+++ For everything below assume that js
is a c++ object defined as ::v8i::engine js;
. +++
The principle of operation assumed/proposed for using the engine is:
Initialisation and runtime setup
You define one or more v8i::engine instances,
You initialise them using initialize(),
You define constants, variables, functions and objects in the runtime(s), which are bound to native c++,
You
include()
oreval()
initialising JavaScript code
Run / use
- You include the main script or
call()
functions / methods in the script. Calling accesses to the runtime are interlocked (using V8 Locker feature).
- You include the main script or
Termination
You can call the
terminate()
method to stop execution. This method will return after the execution is finished, the V8 isolate not locked by anyone and the isolate disposed. The destructor calls terminate() implicitly.The v8i::engine disposes the V8 isolate in the destructor.
Initialisation and configuration
When a v8i::engine
is instantiated, only very basic and quick initialisations
are mare that shall guarantee stability. You initialise the runtime explicitly
using the method js.initialize()
. When the first engine object is initialised,
global ("static run-once") initialisations that V8 requires take place. This
way you can define your js instance e.g. as global or static object without
worrying about timing or object construction order race conditions. You can
invoke initialize() multiple times, only the first will have effect. If something
goes wrong, you will have to catch a v8i::engine_error exception. You can check
if the engine is initialised with the method js.initialized()
.
As an additional setting, you can set a value that describes the maximum
level of recursive js.eval()
calls (description of this method later).
getter and setter are static v8i::engine::max_eval_recursion_depth(<int>)
However, you can leave the default, too.
Defining native elements in the JavaScript runtime
Definitions are always made using the js.define(..)
method, where the overload
signature decides what you define. The first argument is always a string specifying
the name (or better the "selector path", because the dot '.' is detected as separator
for convenience). As V8 provides, definitions can be writable or readonly, enumerable
or not, and deletable or not.
Defining constants:
Second argument is numeric, std::string
or a STL container like vector
,
deque
, list
(will be Array), map
, unordered_map
(will be plain Object). E.g.:
js.define("i", 1); // i = (Number) 1 in the global object
js.define("program.name", "my program"); // program is an object, program.name a String
js.define("program.args", string_vector); // program.args is an Array
All these values and objects (also program
) are by default readonly, not enumerable and
not deletable. To change this and make them writable, deletable, etc, v8::engine
has an
enum v8::engine::define_options
, that can be specified as last argument of all define()
overrides. These options are basically flags and can be bit-or'ed: Namely, there is:
defopt_overwritable
, defopt_deletable
, defopt_enumerable
, defopt_mutable
,
where defopt_shadowed
is 0 (default) and defopt_mutable
a short for all. So, a "normal
variable" defined in the JS runtime would be:
js.define("i", 1, v8i::engine::defopt_mutable); // like JavaScript: `i=1`;
Defining functions:
Second argument of define()
is a function with defined address at compile time
(global function definition or static class function, no instance methods or lambdas).
This function has the signature:
[static] void my_function(v8i::function_callback_info&& cbi);
Proxy'ed, RECOMMENDED, cbi is an object providing type conversion, Isolate, context
and handle scope management, and allows you to throw v8i::callback_error
exceptions
to the JavaScript runtime. You have no contact with V8 specific data types. E.g:
void my_array_returning_function(v8i::function_callback_info&& cbi)
{
if(cbi.args.length() != 1) {
// Callback errors will be transformed to JS runtime `Error`s by the proxy.
throw v8i::callback_error("Need to know how long the Array shall be");
}
// Get argument 0 as unsigned or throw JavaScript exception if it is no number
// or not in the value range of `unsigned int`.
unsigned length = cbi.args.get<unsigned>(0);
// Return an automatically converted value, here an Array.
std::vector<int> vec(length, 0);
ci.return_value(vec);
}
v8i::engine js;
int main(.....)
{
[...]
js.define("myFunction", my_array_returning_function); // in JS the function is myFunction
[...]
}
- Alternatively, if you absolutely need it, you can use the V8 callback directly:
[static] void my_function(const v8::FunctionCallbackInfo<v8::Value> info);
Defining object constructors:
You define "classes" using js.define("MyNamespace.ClassName", METHODS, PROPERTIES )
,
where METHODS is an initialiser list containing initialiser lists (again) with the
pattern { "methodName", cpp_method_callback }. PROPERTIES are similar initialiser lists
containing initialiser lists with the pattern { "propertyName", cpp_getter, cpp_setter }.
E.g.
js.define("MyMath.Vector", {
// Methods { NAME, CALLBACK}
{ "cross", MyVector::cross }
{ "add", MyVector::add }
}, {
// Properties { NAME, GET, SET }
{ "norm", MyVector::get_norm, nullptr } // has no setter
{ "dims", MyVector::get_length, nullptr }
});
Similar to v8i::function_callback_info&&
for functions you get a callback argument
v8i::method_callback_info<T>&&
. It's pretty much the same, except that you have
additional methods, e.g. to return this
in the script runtime (for chaining: var vec
= new MyMath.Vector(); vec.add(vec2).cross(vec3).norm
. You also get a direct access to
the native instance with a shared pointer: autp me = cbi.instance();
. Because of this,
v8i::method_callback_info is a template, and you must say what instance type you are
aiming for. E.g.
class MyVector
{
public:
void get_norm(v8i::method_callback_info<MyVector>&& cbi)
{
auto me = cbi.instance(); // Pointer is checked, will throw if not ok.
auto& vec = me->vect_; // We can access private variables in here.
double sqs = 0;
for(auto d : vec) sqs += d*d;
cbi.return_value(std::sqrt(sqs));
}
private:
std::vector<double> vect_;
};
No matter if method or property, the argument type v8i::method_callback_info
{ "get[]", MyVector::get_by_index } // var d = myvect[i];
{ "set[]", MyVector::set_by_index } // myvect[i] = d;
{ "delete[]", MyVector::del_by_index } // delete myvect[i];
{ "call()", MyVector::call_object } // var someResult = myvect();
Using eval(), call(), include()
With js.eval(CODE)
you simply compile and run code. You can also fetch a return value of
the script. If so, you must specify that type you want back:
js.eval("console.log('evaluated from c++')"); // implicit void return
int i = js.eval<int>("100"); // implicit int conversion
int i = js.eval<int>("abc"); // throws conversion_error because cannot be converted to int
auto d = js.eval<double>("Math.random();");
js.call()
simply invokes a function or method in the runtime and returns the result.
In the same manner as when using eval()
, you tell call()
which return type you like to have.
The arguments for the runtime functions are variadic arguments of call()
itself, and the
arguments will be implicitly converted to the appropriate JS types:
js.call("console.log", 1,2,3, "4", std::vector<int>{5,6,7} );
js.call<double>("Math.sqrt", 9.0);
js.call<string>("myObject.method", arg1, arg2, ...);
Not more to say about that. js.include(FILE)
reads a file, runs it, caches the script
return and returns the result of the script run. From c++ you include a file only once to
prevent recursion in the first place. As with eval
, you can fetch convertible return values:
js.include("initscript.js");
bool success = js.include<bool>("config.js");
double d = js.include<double>("calculate_once.js");
By default, include is disabled in the JS runtime because it depends on your application if includes shall be allowed or not. To define the include function in the runtime, invoke
js.define_include("include/base/path");
then in JS, you can write e.g. var MyClass = include("MyClass.js");
Important to notice
is that this is NOT like require()
, there is no module system. However, it is possible
on this base to implement require()
if needed. This include
really returns only the result
of a script run. Include can be invoked multiple times, but the script actually runs only
the first time. All subsequent call will directly return a cached value. Hence, similar to
require()
, it makes sense to implement functions, constructors, "namespaces" or constant
values in script files that will be included. This principle is not only to boost,
performance bit also to prevent unwanted include recursion.
Exceptions
The exception model of v8i is to forward errors to the caller - if possible. As the caller can be in both c++ or JS runtime, exceptions are translated. In short:
You throw
v8i::callback_error
's from callbacks.You get
v8i::js_exception
's from the runtime when invoking include, eval or call.You get
v8i::conversion_error
's if argument or return value conversion failed.You get
v8i::engine_error
's if something went wrong that cannot or should not be handled in the runtime.v8i::callback_error
's are forwarded to the V8 runtime, where they can be caught. If they are not caught there, they come back to your c++ code, but as js_exception's, because they are uncaught runtime errors. If you don't catch that it is rethrown in the runtime and my come back to you. That ping-pong game is done up to the last caller, which is always in your c++ code (either include(), eval() or call()). If you don't catch it there your c++ program will abort because of an unhandled exception.The method/function proxy explicitly catches only js_exceptions and
callback_error
s. All other exceptions are nothing the runtime shall see. In callbacks this will cause a program abort.
Conversions
An important part of the API separation is that no V8 data types have to be handled. There is always the trouble interfacing duck-typed and a strong-typed languages, and the solution of this implementation is to complain directly and not trying to cast and convert until the type might be correct. Hence, if a conversion fails, there are two possibilities:
Strict conversion: you get a v8i::conversion_error if a conversion fails.
Non-strict conversion: you get a default value if the conversion fails. Non-strict conversion can make sense and shorten your code. To convert non-strict, specify the second template argument, that is by default
Strict=true
, asfalse
, e.g.cbi.args.get<string,false>(0)
orjs.call<int,false>("functionName");
. The result of a failed non-strict conversion is always an "empty" value, means 0 for integer types, "" for string, empty containers. Only double, long double and float return NAN - because they can.
The situations where conversions take place are:
- js.define("some.name", value) , from c++ to js
- js.eval("code") , from js to c++ if not eval() = eval<void>(...)
- js.include("file") , from js to c++ if not include<void>(...)
- js.call("what", a1, a2, a3 ...) , args to js, return to c++ if not void
- Callback: cbi.args.get<T>( index ) , from js to c++
- Callback: cbi.return_value(value) , from c++ to js
Notes:
std::map
andstd::unordered_map
have conversions to js, but not from js to c++.
Runtime interruption
V8 has the feature to request interrupting the current runtime execution, e.g. to
abort in case of long running or even hanging scripts. These abort requests are invoked
by another thread (one that is not currently executing) and callback based. v8i has an
interface for this feature, based on a stack allocated instance of class
v8i::engine::interruption
. In the constructor, a mutex and the interrupt request
is set and an internal callback specified, which then allows the constructor to
resume. Hence, if you declare an instance of engine::interruption
in a block, then
you know that the V8 runtime is interrupted until you exit that scope. E.g.
v8i::engine js;
void interrupt_thread()
{
this_thread::sleep_for(chrono::milliseconds(250));
{
v8i::engine::interruption intr(js);
// Engine interrupted from here ....
js.abort();
// ...to here
}
}
int main()
{
thread ithread(interrupt_thread);
js.initialize().include("hanging.js");
ithread.join();
}
The interruption constructor has one mandatory and one optional argument, the fist is always the v8::engine you want to interrupt, the second a timeout in milliseconds. The timeout is for the interruption mutex - that is the lock for concurrent interruptions, because only one interruption can take place at a time. If the timeout is not specified and the engine is already interrupted by another thread, you get an engine_error. Otherwise you get an engine_error if the interruption constructor did not get this lock after timeout milliseconds. Normally this lock is no issue, and the only latency comes from the runtime itself. Note that the runtime thread-lock is NOT set by the interruption. If you try to lock the V8 isolate your program have a deadlock because the executing thread has the lock and does not release it until the execution has finished.
The essence: Use it only to abort() or some readonly tasks that does not require the use of v8::Locker.
Annotations:
Typed array feature is prepared but not released because the V8 implementation of TypedArray is not considered stable enough.
Microtasks are not supported yet because the feature is experimental.
The possibility to implement this functionality in one file is based on the miracle of modern c++ and IDEs. The line wrap is unconventionally set to 100 columns, and the code formatting might not exactly look as many programmers would expect it. For reviews, debugging and extending I recommend IDEs like NetBeans or Eclipse (Netbeans allows custom code folding comments, which I used here).
Dateien
Files
- GIT link ("all-in-one"):
git clone http://atwillys.de/git/v8i.git
- Class template source: v8i.hh
Standard functions source:: v8istd.hh
Examples/microtests: 001-define-value.cc 001-define-value.js 002-define-function.cc 002-define-function.js 003-call.cc 003-call.js 004-eval.cc 004-eval.js 005-include.cc 005-include.js 009-eval-recursion.cc 009-eval-recursion.js 010-define-constructor.cc 010-define-constructor.js 020-std-functions.cc 020-std-functions.js 021-std-functions-min-fs.cc 021-std-functions-min-fs.js 050-self-terminate.cc 050-self-terminate.js 100-val_array.cc 100-val_array.js 998-interrupt.cc 998-interrupt.js 999-multithread-terminate.cc 999-multithread-terminate.js Makefile microtest.hh test.hh test.js
Quelltext
Source code
/**
* @package de.atwillys.cc.swl.lang.js
* @license BSD (simplified)
* @author Stefan Wilhelm (stfwi)
* @file v8i.hh
* @version 1.0rc1
* @ccflags
* @ldflags -lrt -lv8
* @platform linux, bsd
* @standard >= c++11
* @requires V8 >= 3.29
*
* -------------------------------------------------------------------------------------------------
*
* Template based single-header-file-interface to the V8 JavaScript engine with strong
* API separation.
*
* Purpose of it is to allow embedding scripting functionality into a software
* dominated by c++ code (see also v8juce). As V8 is a project continuously pushed
* forward, interfacing to c++ code and testing is maintenance intensive. To
* overcome these interfacing issues, this file provides basic glue layer functionality
* to strongly separate the V8 engine. Hence, changes in V8 have major effect
* only in this header file and no or minor in the native code.
*
* Although the template nature of the implemented classes allows good optimisations,
* this interface comes with a slight overhead - callback proxies - which means that
* each JS call to a "native" function or method implies function pointer lookup to
* the callback that you implement. Hence, in applications where small calls (e.g.
* getters/setters) are extensively used in JS runtime loops, a performance loss from
* this overhead can be measured compared to direct V8 callbacks. For this reason it
* is possible to define V8 callbacks in this engine wrapper, too - but the necessity
* of such JavaScript implementations may point out that a native function for a task
* is missing. E.g. you have a std::vector<double> with 5 million elements. Iterating
* and accumulating in Javascript screams for a native method that can be called as
* `var sum = mvvector.sum()`.
*
* The essence: In this implementation the purpose of JavaScript that is aimed for
* is to provide flexibility for a c++ program, not to provide a native framework for
* JavaScript software - these wheels are already invented and well implemented, e.g.
* in nodejs or jsc.
*
* -------------------------------------------------------------------------------------------------
* USAGE
* -------------------------------------------------------------------------------------------------
*
* All exported elements related to this implementation are accessible via
* the namespace `::v8i`. Main class is v8i::engine, of which you can instantiate
* one or more objects. Each v8i::engine instance works with its own V8 isolate,
* context and global object. Having an initialised instance of v8i::engine, you
* can define constants/variables, functions and objects for this instance, evaluate
* JS code, call functions and methods, and include '.js' files.
*
* +++ For everything below assume that `js` is a c++ object defined as `::v8i::engine js;`. +++
*
* --------------------------
*
* The principle of operation assumed/proposed for using the engine is:
*
* 1) Initialisation and runtime setup
*
* 1.1) You define one or more v8i::engine instances,
*
* 1.2) You initialise them using initialize(),
*
* 1.3) You define constants, variables, functions and objects in the runtime(s),
* which are bound to native c++,
*
* 1.4) You `include()` or `eval()` initialising JavaScript code
*
* 2) Run / use
*
* 2.1) You include the main script or `call()` functions / methods in the script.
* Calling accesses to the runtime are interlocked (using V8 Locker feature).
*
* 3) Termination
*
* 3.1 You can call the `terminate()` method to stop execution. This method will
* return after the execution is finished, the V8 isolate not locked by anyone
* and the isolate disposed. The destructor calls terminate() implicitly.
*
* 3.2 The v8i::engine disposes the V8 isolate in the destructor.
*
*
* --------------------------
*
* - Initialisation and configuration
*
* When a v8i::engine is instantiated, only very basic and quick initialisations
* are mare that shall guarantee stability. You initialise the runtime explicitly
* using the method `js.initialize()`. When the first engine object is initialised,
* global ("static run-once") initialisations that V8 requires take place. This
* way you can define your js instance e.g. as global or static object without
* worrying about timing or object construction order race conditions. You can
* invoke initialize() multiple times, only the first will have effect. If something
* goes wrong, you will have to catch a v8i::engine_error exception. You can check
* if the engine is initialised with the method `js.initialized()`.
*
* As an additional setting, you can set a value that describes the maximum
* level of recursive `js.eval()` calls (description of this method later).
* getter and setter are static v8i::engine::max_eval_recursion_depth() and
* static v8i::engine::max_eval_recursion_depth(int). However, you can leave the
* default, too.
*
* --------------------------
*
* - Defining native elements in the JavaScript runtime
*
* Definitions are always made using the `js.define(..)` method, where the overload
* signature decides what you define. The first argument is always a string specifying
* the name (or better the "selector path", because the dot '.' is detected as separator
* for convenience). As V8 provides, definitions can be writable or readonly, enumerable
* or not, and deletable or not.
*
* - Defining constants:
*
* Second argument is numeric, std::string or a STL container like vector,
* deque, list (will be Array), map, unordered_map (will be plain Object). E.g.:
*
* - js.define("i", 1); // i = (Number) 1 in the global object
* - js.define("program.name", "my program"); // program is an object, program.name a String
* - js.define("program.args", string_vector); // program.args is an Array
*
* All these values and objects (also `program`) are by default readonly, not enumerable and
* not deletable. To change this and make them writable, deletable, etc, v8::engine has an
* `enum v8::engine::define_options`, that can be specified as last argument of all `define()`
* overrides. These options are basically flags and can be bit-or'ed: Namely, there is:
* defopt_shadowed, defopt_overwritable, defopt_deletable, defopt_enumerable, defopt_mutable,
* where defopt_shadowed is 0 (default) and defopt_mutable a short for all. So, a "normal
* variable" defined in the JS runtime would be:
*
* - js.define("i", 1, v8i::engine::defopt_mutable); // like JavaScript: `i=1`;
*
* --------------------------
*
* - Defining functions:
*
* Second argument of `define()` is a function with defined address at compile time
* (global function definition or static class function, no instance methods or lambdas).
* This function has the signature:
*
* [static] void my_function(v8i::function_callback_info&& cbi);
*
* Proxy'ed, RECOMMENDED, cbi is an object providing type conversion, Isolate, context
* and handle scope management, and allows you to throw `v8i::callback_error` exceptions
* to the JavaScript runtime. You have no contact with V8 specific data types. E.g:
*
* void my_array_returning_function(v8i::function_callback_info&& cbi)
* {
* if(cbi.args.length() != 1) {
* // Callback errors will be transformed to JS runtime `Error`s by the proxy.
* throw v8i::callback_error("Need to know how long the Array shall be");
* }
*
* // Get argument 0 as unsigned or throw JavaScript exception if it is no number
* // or not in the value range of `unsigned int`.
* unsigned length = cbi.args.get<unsigned>(0);
*
* // Return an automatically converted value, here an Array.
* std::vector<int> vec(length, 0);
* ci.return_value(vec);
* }
*
* v8i::engine js;
* int main(.....)
* { js.define("myFunction", my_array_returning_function); [...] }
*
* Alternatively, if you absolutely need it, you can use the V8 callback directly:
*
* [static] void my_function(const v8::FunctionCallbackInfo<v8::Value> info);
*
* --------------------------
*
* - Defining object constructors:
*
* You define "classes" using `js.define("MyNamespace.ClassName", METHODS, PROPERTIES )`,
* where METHODS is an initialiser list containing initialiser lists (again) with the
* pattern { "methodName", cpp_method_callback }. PROPERTIES are similar initialiser lists
* containing initialiser lists with the pattern { "propertyName", cpp_getter, cpp_setter }.
* E.g.
*
* js.define("MyMath.Vector", {
* // Methods { NAME, CALLBACK}
* { "cross", MyVector::cross }
* { "add", MyVector::add }
* }, {
* // Properties { NAME, GET, SET }
* { "norm", MyVector::get_norm, nullptr } // has no setter
* { "dims", MyVector::get_length, nullptr }
* });
*
* Similar to `v8i::function_callback_info&&` for functions you get a callback argument
* `v8i::method_callback_info<T>&&`. It's pretty much the same, except that you have
* additional methods, e.g. to return `this` in the script runtime (for chaining: `var vec
* = new MyMath.Vector(); vec.add(vec2).cross(vec3).norm`. You also get a direct access to
* the native instance with a shared pointer: `autp me = cbi.instance();`. Because of this,
* v8i::method_callback_info is a template, and you must say what instance type you are
* aiming for. E.g.
*
* class MyVector
* {
* public:
* void get_norm(v8i::method_callback_info<MyVector>&& cbi)
* {
* auto me = cbi.instance(); // Pointer is checked, will throw if not ok.
* auto& vec = me->vect_; // We can access private variables in here.
* double sqs = 0;
* for(auto d : vec) sqs += d*d;
* cbi.return_value(std::sqrt(sqs));
* }
* private:
* std::vector<double> vect_;
* };
*
* No matter if method or property, the argument type v8i::method_callback_info<T> is
* always the same. There are also special method names for array index access and
* call-object-as-function. Note that you must do range checks yourself. The engine
* cannot know e.g. how many elements your std::vector has.
*
* { "get[]", MyVector::get_by_index } // var d = myvect[i];
* { "set[]", MyVector::set_by_index } // myvect[i] = d;
* { "delete[]", MyVector::del_by_index } // delete myvect[i];
* { "call()", MyVector::call_object } // var someResult = myvect();
*
* --------------------------
*
* - Using eval(), call(), include()
*
* With js.eval(CODE) you simply compile and run code. You can also fetch a return value of
* the script. If so, you must specify that type you want back:
*
* js.eval("console.log('evaluated from c++')"); // implicit void return
* int i = js.eval<int>("100"); // implicit int conversion
* int i = js.eval<int>("abc"); // throws conversion_error because cannot be converted to int
* auto d = js.eval<double>("Math.random();");
*
* `js.call()` simply invokes a function or method in the runtime and returns the result.
* In the same manner as when using eval(), you tell call() which return type you like to have.
* The arguments for the runtime functions are variadic arguments of call() itself, and the
* arguments will be implicitly converted to the appropriate JS types:
*
* js.call("console.log", 1,2,3, "4", std::vector<int>{5,6,7} );
* js.call<double>("Math.sqrt", 9.0);
* js.call<string>("myObject.method", arg1, arg2, ...);
*
* Not more to say about that. `js.include(FILE)` reads a file, runs it, caches the script
* return and returns the result of the script run. From c++ you include a file only once to
* prevent recursion in the first place. As with eval, you can fetch convertible return values:
*
* js.include("initscript.js");
* bool success = js.include<bool>("config.js");
* double d = js.include<double>("calculate_once.js");
*
* By default, include is disabled in the JS runtime because it depends on your application
* if includes shall be allowed or not. To define the include function in the runtime, invoke
*
* js.define_include("include/base/path");
*
* then in JS, you can write e.g. `var MyClass = include("MyClass.js");` Important to notice
* is that this is NOT like `require()`, there is no module system. However, it is possible
* on this base to implement require() if needed. This include really returns only the result
* of a script run. Include can be invoked multiple times, but the script actually runs only
* the first time. All subsequent call will directly return a cached value. Hence, similar to
* require(), it makes sense to implement functions, constructors, "namespaces" or constant
* values in script files that will be included. This principle is not only to boost,
* performance bit also to prevent unwanted include recursion.
*
* --------------------------
*
* - Exceptions
*
* The exception model of v8i is to forward errors to the caller - if possible. As the caller
* can be in both c++ or JS runtime, exceptions are translated. In short:
*
* - You throw v8i::callback_error's from callbacks.
*
* - You get v8i::js_exception's from the runtime when invoking include, eval or call.
*
* - You get v8i::conversion_error's if argument or return value conversion failed.
*
* - You get v8i::engine_error's if something went wrong that cannot or should not be
* handled in the runtime.
*
* - v8i::callback_error's are forwarded to the V8 runtime, where they can be caught.
* If they are not caught there, they come back to your c++ code, but as js_exception's,
* because they are uncaught runtime errors. If you don't catch that it is rethrown in
* the runtime and my come back to you. That ping-pong game is done up to the last
* caller, which is always in your c++ code (either include(), eval() or call()). If
* you don't catch it there your c++ program will abort because of an unhandled exception.
*
* - The method/function proxy explicitly catches only js_exceptions and `callback_error`s.
* All other exceptions are nothing the runtime shall see. In callbacks this will cause
* a program abort.
* --------------------------
*
* - Conversions
*
* An important part of the API separation is that no V8 data types have to be handled.
* There is always the trouble interfacing duck-typed and a strong-typed languages, and
* the solution of this implementation is to complain directly and not trying to cast
* and convert until the type might be correct. Hence, if a conversion fails, there are
* two possibilities:
*
* 1) Strict conversion: you get a v8i::conversion_error if a conversion fails.
*
* 2) Non-strict conversion: you get a default value if the conversion fails. Non-strict
* conversion can make sense and shorten your code. To convert non-strict, specify
* the second template argument, that is by default `Strict=true`, as `false`, e.g.
* `cbi.args.get<string,false>(0)` or `js.call<int,false>("functionName");`. The
* result of a failed non-strict conversion is always an "empty" value, means 0
* for integer types, "" for string, empty containers. Only double, long double and
* float return NAN - because they can.
*
* The situations where conversions take place are:
*
* - js.define("some.name", value) , from c++ to js
* - js.eval("code") , from js to c++ if not eval() = eval<void>(...)
* - js.include("file") , from js to c++ if not include<void>(...)
* - js.call("what", a1, a2, a3 ...) , args to js, return to c++ if not void
*
* - Callback: cbi.args.get<T>( index ) , from js to c++
* - Callback: cbi.return_value(value) , from c++ to js
*
* Notes:
*
* - std::map and std::unordered_map have conversions to js, but not from js to c++.
*
* --------------------------
*
* - Runtime interruption
*
* V8 has the feature to request interrupting the current runtime execution, e.g. to
* abort in case of long running or even hanging scripts. These abort requests are invoked
* by another thread (one that is not currently executing) and callback based. v8i has an
* interface for this feature, based on a stack allocated instance of class
* `v8i::engine::interruption`. In the constructor, a mutex and the interrupt request
* is set and an internal callback specified, which then allows the constructor to
* resume. Hence, if you declare an instance of `engine::interruption` in a block, then
* you know that the V8 runtime is interrupted until you exit that scope. E.g.
*
* v8i::engine js;
* void interrupt_thread()
* {
* this_thread::sleep_for(chrono::milliseconds(250));
* {
* v8i::engine::interruption intr(js);
* // Engine interrupted from here ....
* js.abort();
* // ...to here
* }
* }
*
* int main()
* {
* thread ithread(interrupt_thread);
* js.initialize().include("hanging.js");
* ithread.join();
* }
*
* The interruption constructor has one mandatory and one optional argument,
* the fist is always the v8::engine you want to interrupt, the second a timeout
* in milliseconds. The timeout is for the interruption mutex - that is the lock
* for concurrent interruptions, because only one interruption can take place at
* a time. If the timeout is not specified and the engine is already interrupted
* by another thread, you get an engine_error. Otherwise you get an engine_error
* if the interruption constructor did not get this lock after timeout milliseconds.
* Normally this lock is no issue, and the only latency comes from the runtime
* itself. Note that the runtime thread-lock is NOT set by the interruption. If
* you try to lock the V8 isolate your program have a deadlock because the executing
* thread has the lock and does not release it until the execution has finished.
*
* The essence: Use it only to abort() or some readonly tasks that does not
* require the use of v8::Locker.
*
* -------------------------------------------------------------------------------------------------
*
* Annotations:
*
* - Typed array feature is prepared but not released because the V8 implementation of
* TypedArray is not considered stable enough.
*
* - Microtasks are not supported yet because the feature is experimental.
*
* - The possibility to implement this functionality in one file is based on
* the miracle of modern c++ and IDEs. The line wrap is unconventionally set
* to 100 columns, and the code formatting might not exactly look as many
* programmers would expect it. For reviews, debugging and extending I recommend
* IDEs like NetBeans or Eclipse (Netbeans allows custom code folding comments,
* which I used here).
*
* -------------------------------------------------------------------------------------------------
* +++ BSD license header +++
* Copyright (c) 2012-2014, Stefan Wilhelm (stfwi, <cerbero s@atwilly s.de>)
* All rights reserved.
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met: (1) Redistributions
* of source code must retain the above copyright notice, this list of conditions
* and the following disclaimer. (2) Redistributions in binary form must reproduce
* the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* (3) Neither the name of atwillys.de nor the names of its contributors may be
* used to endorse or promote products derived from this software without specific
* prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS
* AND CONTRIBUTORS "AS IS" AND VAR EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
* BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
* OR CONTRIBUTORS BE LIABLE FOR VAR DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON VAR THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN VAR
* WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
* DAMAGE.
* -------------------------------------------------------------------------------------------------
*/
#ifndef V8I_HH
#define V8I_HH
// <editor-fold desc="preprocessor" defaultstate="collapsed">
#include <v8.h>
#include <string>
#include <sstream>
#include <iostream>
#include <fstream>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <limits>
#include <functional>
#include <algorithm>
#include <cctype>
#include <vector>
#include <deque>
#include <locale>
#include <clocale>
#include <atomic>
#include <memory>
#include <unordered_map>
#include <map>
#include <mutex>
#include <condition_variable>
#include <locale>
#include <chrono>
#include <cassert>
#include <cstdlib>
#include <thread>
#include <chrono>
#ifndef V8_VERSION
#define V8_VERSION 3027999L
#endif
#if defined(_MSC_VER) /* && _MSC_VER < 0x?????L --> VS2013 still missing that */
#define noexcept throw()
#define constexpr
#endif
// </editor-fold>
namespace v8i {
// <editor-fold desc="forward decls" defaultstate="collapsed">
/**
* The main engine class.
*/
namespace templates { template <int N=0> class engine; }
typedef templates::engine<> engine;
// </editor-fold>
// <editor-fold desc="internal auxiliaries" defaultstate="collapsed">
namespace {
//
// V8 string auxiliaries. Note: If no isolate is specified,
// GetCurrent is assumed, means you must be in a isolate scope / entered isolate.
// Explicitly not defined as single universal ref template.
//
template <typename R=v8::Local<v8::String>>
inline R v8str(const char* s)
{ return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), s); }
template <typename R=v8::Local<v8::String>>
inline R v8str(const std::string& s)
{ return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), s.data(),
v8::String::kNormalString, s.length()); }
template <typename R=v8::Local<v8::String>>
inline R v8str(std::string&& s)
{ return v8::String::NewFromUtf8(v8::Isolate::GetCurrent(), s.data(),
v8::String::kNormalString, s.length()); }
template <typename R=v8::Local<v8::String>>
inline R v8str(const char* s, v8::Isolate* isl)
{ return v8::String::NewFromUtf8(isl, s); }
template <typename R=v8::Local<v8::String>>
inline R v8str(const std::string& s, v8::Isolate* isl)
{ return v8::String::NewFromUtf8(isl, s.data(), v8::String::kNormalString, s.length()); }
template <typename R=v8::Local<v8::String>>
inline R v8str(std::string&& s, v8::Isolate* isl)
{ return v8::String::NewFromUtf8(isl, s.data(), v8::String::kNormalString, s.length()); }
}
// </editor-fold>
// <editor-fold desc="exceptions" defaultstate="collapsed">
namespace templates {
/**
* Thrown on fatal errors that might make the script engine unusable
*/
template <typename Select=void>
class basic_engine_error : public std::runtime_error
{
public:
explicit basic_engine_error(const std::string& msg) noexcept : std::runtime_error(msg) {;}
};
/**
* Thrown when a javascript type is not convertible into a specified c++ type
*/
template <typename Select=void>
class basic_not_convertible_error : public std::runtime_error
{
public:
explicit basic_not_convertible_error(const std::string& msg) noexcept : std::runtime_error(msg) {;}
};
/**
* You throw this exception in function callbacks, and it is converted into a JavaScript
* exception (means in the V8 runtime).
*/
template <typename Select=void>
class basic_callback_error : public std::runtime_error
{
public:
explicit basic_callback_error(const std::string& msg) noexcept : std::runtime_error(msg) {;}
};
/**
* v8::Exception thrown from exceptions in the javascript code.
*/
template <typename Select=void>
class basic_js_exception : public std::exception
{
public:
explicit basic_js_exception(std::string whatmsg, std::string ex, std::string file="",
int line=0, int col=0, std::string code_line="", std::string stacktr="") noexcept :
line_(line), column_(col), except_(ex), file_(file), code_line_(code_line),
stack_trace_(stacktr), what_(whatmsg)
{ ; }
//virtual ~basic_js_exception() noexcept
//{ ; }
/**
* Complete message for std::exception::what()
*/
const char *what() const noexcept { return what_.c_str(); }
/**
* Details specific to js exception
*/
const char *message() const noexcept { return except_.c_str(); }
const char *file() const noexcept { return file_.c_str(); }
const char *code_line() const noexcept { return code_line_.c_str(); }
const char *stack_trace() const noexcept { return stack_trace_.c_str(); }
int line() const noexcept { return line_; }
int column() const noexcept { return column_; }
public:
static void trigger(std::string msg)
{ v8::Isolate::GetCurrent()->ThrowException(v8::Exception::Error(v8str(msg))); }
private:
int line_, column_;
std::string except_, file_, code_line_, stack_trace_, what_;
};
}
/**
* Drived from std::runtime_error, thrown on engine errors that are hard or not
* to recover.
*/
typedef templates::basic_engine_error<> engine_error;
/**
* Derived from std::exception, thrown when exceptions in executed JavaScript code occur
* (not c++ exceptions, but exceptions in the script runtime). T.m. engine methods like
* `eval()` or `call()` throw it.
*/
typedef templates::basic_js_exception<> js_exception;
/**
* A runtime error thrown when a script value is not convertible into the desired c++ type.
* E.G. casting traits in namespace v8i::type throw this exception.
*/
typedef templates::basic_not_convertible_error<> not_convertible_error;
/**
* You throw this exception in function callbacks, and it is converted into a JavaScript
* exception (means in the V8 runtime).
*/
typedef templates::basic_callback_error<> callback_error;
// </editor-fold>
namespace type {
// <editor-fold desc="js_typename (for debugging use)" defaultstate="collapsed">
/**
* Javascript type name query. Note: This is a runtime type query and expensive.
* Use it only for debugging or in exceptional situations.
*
* @param const HandleType& val
* @return const char*
*/
template <typename HandleType=v8::Handle<v8::Value>>
const char* js_typename(const HandleType& val)
{
if(val.IsEmpty()) return "(empty value)";
if(val->IsUndefined()) return "undefined";
if(val->IsNull()) return "null";
#if 1
if(val->IsFalse()) return "false";
if(val->IsTrue()) return "true";
if(val->IsInt32()) return "Int32";
if(val->IsUint32()) return "Uint32";
if(val->IsBoolean()) return "Boolean";
if(val->IsString()) return "String";
if(val->IsNumber()) return "Number";
if(val->IsNativeError()) return "NativeError";
if(val->IsFunction()) return "Function";
if(val->IsArrayBuffer()) return "ArrayBuffer";
if(val->IsFloat32Array()) return "Float32Array";
if(val->IsFloat64Array()) return "Float64Array";
if(val->IsInt16Array()) return "Int16Array";
if(val->IsInt32Array()) return "Int32Array";
if(val->IsInt8Array()) return "Int8Array";
if(val->IsUint16Array()) return "Uint16Array";
if(val->IsUint32Array()) return "Uint32Array";
if(val->IsUint8Array()) return "Uint8Array";
if(val->IsUint8ClampedArray()) return "Uint8ClampedArray";
if(val->IsTypedArray()) return "TypedArray";
if(val->IsDataView()) return "DataView";
if(val->IsArray()) return "Array";
if(val->IsBooleanObject()) return "BooleanObject";
if(val->IsSymbolObject()) return "SymbolObject";
if(val->IsDate()) return "Date";
if(val->IsExternal()) return "External";
if(val->IsNumberObject()) return "NumberObject";
if(val->IsPromise()) return "Promise";
if(val->IsRegExp()) return "RegExp";
if(val->IsStringObject()) return "StringObject";
if(val->IsSymbol()) return "Symbol";
if(val->IsObject()) return "Object";
#else
if(val->IsBoolean()) return "Boolean";
if(val->IsNumber()) return "Number";
if(val->IsString()) return "String";
if(val->IsFunction()) return "Function";
if(val->IsTypedArray()) return "TypedArray";
if(val->IsArray()) return "Array";
if(val->IsDate()) return "Date";
if(val->IsRegExp()) return "RegExp";
if(val->IsObject()) return "Object";
#endif
return "(unrecognized javascript type)";
}
// </editor-fold>
// <editor-fold desc="conversion traits - native type names" defaultstate="collapsed">
/**
* c++ "type name" traits for common/used types. Hidden namespace, mainly
* used to specify the type name in conversion exceptions. Not `value`, but `name`
* is used to get the type name for extensibility (where `value` or `type` etc might
* be required).
*/
namespace {
template <typename T>
struct nattype {
static constexpr const char* name() { return "(unregistered type)"; }
static constexpr const char* js_name() { return "(unregistered type)"; }
};
template <> struct nattype<void> {
static constexpr const char* name() { return "void"; }
static constexpr const char* js_name() { return "undefined"; }
};
template <> struct nattype<bool> {
static constexpr const char* name() { return "bool"; }
static constexpr const char* js_name() { return "Boolean"; }
};
// Numbers
#define declnat_type_trait(TYPE) template <> struct nattype<TYPE> { \
static constexpr const char* name() { return #TYPE; } \
static constexpr const char* js_name() { return "Number"; } \
}
declnat_type_trait(short);
declnat_type_trait(int);
declnat_type_trait(long);
declnat_type_trait(long long);
declnat_type_trait(unsigned char);
declnat_type_trait(unsigned short);
declnat_type_trait(unsigned int);
declnat_type_trait(unsigned long);
declnat_type_trait(unsigned long long);
declnat_type_trait(float);
declnat_type_trait(double);
declnat_type_trait(long double);
#undef declnat_type_trait
template <> struct nattype<char> { // char conversion == first character of v8::String
static constexpr const char* name() { return "char"; }
static constexpr const char* js_name() { return "String"; }
};
template <> struct nattype<std::string> {
static constexpr const char* name() { return "std::string"; }
static constexpr const char* js_name() { return "String"; }
};
template <typename T> struct nattype<std::vector<T>> {
static constexpr const char* name() { return "std::vector"; }
static constexpr const char* js_name() { return "Array"; }
};
template <typename T> struct nattype<std::deque<T>> {
static constexpr const char* name() { return "std::deque"; }
static constexpr const char* js_name() { return "Array"; }
};
}
// </editor-fold>
// <editor-fold desc="conversion traits - declaration / void" defaultstate="collapsed">
/**
* Main template for conversion traits. The typename Enable=void is only
* for std::enable_if use. In all specialized versions only
* `template <> struct conv<Specialised Type> { ... ... ...}` is used.
*/
template <typename T, typename Enable=void>
struct conv;
/**
* void conversion (for void return)
*/
template <> struct conv<void>
{
typedef void type;
constexpr static const char* type_name() { return "void"; }
template <bool Strict=false>
static void from_js(const v8::Handle<v8::Value>& val)
{ return void(); }
static void from_js_strict(const v8::Handle<v8::Value>& val)
{ return void(); }
static v8::Handle<v8::Value> to_js(void)
{ return v8::Undefined(v8::Isolate::GetCurrent()); }
};
// </editor-fold>
// <editor-fold desc="conversion traits - hanle passthrough" defaultstate="collapsed">
/**
* passthrough conversion (v8::Handle)
*/
template <> struct conv<v8::Handle<v8::Value>>
{
typedef void type;
constexpr static const char* type_name() { return "v8::Handle<v8::Value>"; }
static v8::Handle<v8::Value> from_js(const v8::Handle<v8::Value>& val)
{ return v8::Handle<v8::Value>(val); }
static v8::Handle<v8::Value> from_js(v8::Handle<v8::Value>&& val)
{ return val; }
static v8::Handle<v8::Value> from_js_strict(const v8::Handle<v8::Value>& val)
{ return v8::Handle<v8::Value>(val); }
static v8::Handle<v8::Value> from_js_strict(v8::Handle<v8::Value>&& val)
{ return val; }
static v8::Handle<v8::Value> to_js(const v8::Handle<v8::Value>& val)
{ return v8::Handle<v8::Value>(val); }
static v8::Handle<v8::Value> to_js(v8::Handle<v8::Value>&& val)
{ return val; }
};
// </editor-fold>
// <editor-fold desc="conversion traits - numeric types" defaultstate="collapsed">
/**
* This conversion trait applies to all numeric / arithmetic types in c++. Conversion is
* from and to js v8::Number.
*/
template <typename T>
struct conv<T, typename std::enable_if< std::is_arithmetic<T>::value>::type>
{
typedef T type;
constexpr static const char* type_name() { return nattype<T>::name(); }
static type from_js_strict(const v8::Handle<v8::Value>& val)
{ return from_js<true>(val); }
template <bool Strict=false>
static type from_js(const v8::Handle<v8::Value>& val)
{
if(val.IsEmpty() || (!val->IsNumber() && !val->IsBoolean())) {
if(Strict) {
throw not_convertible_error(std::string("'") + js_typename(val)
+ "' not convertible to " + type_name());
} else if(std::numeric_limits<T>::has_quiet_NaN) {
return std::numeric_limits<T>::quiet_NaN();
} else {
return T(0);
}
}
if(val->IsBoolean()) {
return T(val->BooleanValue() ? 1 : 0);
} else if(std::numeric_limits<T>::has_quiet_NaN) { // Optimised type based conversions
return T(val->NumberValue());
} else if(std::is_signed<T>::value && sizeof(T) == 4 && val->IsInt32()) {
return val->Int32Value();
} else if(!std::is_signed<T>::value && sizeof(T) == 4 && val->IsUint32()) {
return val->Uint32Value();
} else {
double d = val->NumberValue();
if(d > std::numeric_limits<T>::max()) {
if(Strict) throw not_convertible_error(std::string("Number greater than maximum " \
"allowed value of " + std::to_string(std::numeric_limits<T>::max())));
return T(0);
} else if(d < std::numeric_limits<T>::min()) {
if(Strict) throw not_convertible_error(std::string("Number less than minimum allowed " \
"value of " + std::to_string(std::numeric_limits<T>::min())));
return T(0);
} else {
return T(d);
}
}
}
static v8::Handle<v8::Value> to_js(type val)
{ return v8::Number::New(v8::Isolate::GetCurrent(), (double) val); } // anyway all v8::Number
};
// </editor-fold>
// <editor-fold desc="conversion traits - char / string / const char*" defaultstate="collapsed">
/**
* Conversion for char interpreted as first character in a string or js v8::String it the data
* type is v8::String, otherwise a v8::Number or (0 / 1) if Boolean.
*/
template <> struct conv<char>
{
typedef char type;
constexpr static const char* type_name() { return "char"; }
static type from_js_strict(const v8::Handle<v8::Value>& val)
{ return from_js<true>(val); }
template <bool Strict=false>
static type from_js(const v8::Handle<v8::Value>& val)
{
if(val.IsEmpty() || val->IsUndefined() || val->IsNull()) {
if(Strict) {
throw not_convertible_error("'undefined/null' not convertible to char");
} else {
return type(0);
}
} else if(val->IsBoolean()) {
return val->IsTrue() ? 1 : 0;
} else if(val->IsNumber()) {
double d = val->NumberValue();
if(d < 0 || d > 127) {
if(Strict) {
throw not_convertible_error("Number out of range for char");
} else {
return d < 0 ? 0 : 127;
}
}
return static_cast<char>(d);
} else {
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
v8::String::Utf8Value v(val->ToString());
if(!(*v)) {
if(Strict) {
throw not_convertible_error(std::string("'") + js_typename(val)
+ "' cannot be converted to string");
} else {
return type(0);
}
}
// If the string is empty the return value will be '\0'
return type(reinterpret_cast<const char*>(*v)[0]);
}
}
static v8::Handle<v8::Value> to_js(const type val)
{
char s[2] = { val, '\0' };
return v8str(s);
}
};
/**
* Conversion FROM c-v8::String, not from js. There is explicitly no from_js() conversion.
*/
template <> struct conv<char const*>
{
typedef char const* type;
constexpr static const char* type_name () { return "c-string"; }
static type from_js(const v8::Handle<v8::Value>&);
static type from_js_strict(const v8::Handle<v8::Value>&);
static v8::Handle<v8::Value> to_js(type val)
{
if(!val) throw engine_error("A null pointer cannot converted to a javascript String!");
return v8str(val);
}
};
/**
* Conversion for std::string. Note: string data interpreted as UTF-8.
*/
template <typename T> struct conv<std::basic_string<T>>
{
typedef std::basic_string<T> type;
static_assert(sizeof(typename type::value_type) == 1,
"JS_ENGINE: wstring not supported because its character width is platform dependent." \
"Use std::codecvt to convert to UTF-8."
);
static type from_js_strict(const v8::Handle<v8::Value>& val)
{ return from_js<true>(val); }
template <bool Strict=false>
static type from_js(const v8::Handle<v8::Value>& val)
{
if(val.IsEmpty() || val->IsUndefined()) {
if(Strict) {
throw not_convertible_error("'undefined' not convertible to string");
} else {
return "#undefined";
}
} else if(val->IsNull()) {
if(Strict) {
throw not_convertible_error("'null' not convertible to string");
} else {
return "#null";
}
}
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
v8::String::Utf8Value v(val->ToString());
if(!(*v)) {
if(Strict) {
throw not_convertible_error(std::string("'") + js_typename(val)
+ "' cannot be converted to string");
} else {
return "#nostring";
}
}
return reinterpret_cast<const typename type::value_type*>(*v);
}
static v8::Handle<v8::Value> to_js(const type& val)
{ return v8str(val); }
static v8::Handle<v8::Value> to_js(type&& val)
{ return v8str(std::move(val)); }
};
// </editor-fold>
// <editor-fold desc="conversion traits - array containers" defaultstate="collapsed">
namespace {
/**
* Conversion to containers that have emplace_back()
*/
template <typename ArrayContainer>
struct conv_array
{
typedef ArrayContainer type;
typedef typename ArrayContainer::value_type value_type;
// >>>> Only for vector.reserve(size) optimisation, used in from_js()
template <typename T=value_type, typename ContainerType=ArrayContainer>
static void container_reserve(ContainerType&, unsigned) { }
template <typename T=value_type>
static void container_reserve(std::vector<T>& c, unsigned n) { c.reserve(n); }
// <<<<
static type from_js_strict(const v8::Handle<v8::Value>& val)
{ return from_js<true>(val); }
template <bool Strict=false>
static type from_js(const v8::Handle<v8::Value>& val)
{
type r;
if(val.IsEmpty() || !val->IsArray()) {
if(Strict) {
throw not_convertible_error(std::string("'") + js_typename(val)
+ "' cannot be converted to "
+ nattype<type>::name()
+ "<" + nattype<value_type>::name() + ">");
} else {
return r;
}
}
v8::HandleScope handle_scope(v8::Isolate::GetCurrent());
v8::Handle<v8::Array> a = v8::Handle<v8::Array>::Cast(val);
unsigned length = a->Length();
if(length == 0) return r;
container_reserve<>(r, length);
#ifdef V8I_TYPED_ARRAY_HH
if(a->IsTypedArray()) {
if(!typed_array<>::from_js<type>(a, r)) {
// Error here intentionally throws even in non-strict mode, it's always
// a "real" error.
throw engine_error(std::string("'") + js_typename(val)
+ "' could not be converted to "
+ nattype<type>::name()
+ "<" + nattype<value_type>::name() + ">");
}
return r;
}
#endif
if(!Strict) {
for(uint32_t i=0; i<length; ++i) {
r.emplace_back(conv<value_type>::from_js(a->Get(i)));
}
} else {
uint32_t i;
try {
for(i=0; i<length; ++i) {
r.emplace_back(conv<value_type>::from_js_strict(a->Get(i)));
}
} catch(const not_convertible_error& e) {
std::stringstream ss;
ss << "'Array' cannot be converted to " << nattype<type>::name()
<< "<" << nattype<value_type>::name()
<< "> at element array[" << i << "]";
throw not_convertible_error(ss.str());
}
}
return r;
}
static v8::Handle<v8::Value> to_js(const type& val)
{
unsigned length = val.size();
#ifdef V8I_TYPED_ARRAY_HH
if(std::is_arithmetic<value_type>::value && length > typed_array<>::auto_generation_threshold()) {
v8::Handle<v8::Value> a;
if(!typed_array<>::to_js<type>(val, a)) {
throw v8i::engine_error("Failed to create typed array");
}
return a;
}
#endif
{
v8::Handle<v8::Array> r(v8::Array::New(v8::Isolate::GetCurrent(), static_cast<int>(length)));
for(unsigned i=0; i<length; ++i) {
r->Set(i, conv<typename type::value_type>::to_js(val[i]));
}
return v8::Handle<v8::Value>::Cast(r);
}
}
};
}
/**
* Conversions for std::vector<T>
*/
template <typename T> struct conv<std::vector<T>>
{
typedef std::vector<T> type;
static inline type from_js(const v8::Handle<v8::Value>& val)
{ return conv_array<type>::from_js(val); }
static inline type from_js_strict(const v8::Handle<v8::Value>& val)
{ return conv_array<type>::from_js_strict(val); }
static inline v8::Handle<v8::Value> to_js(const type& val)
{ return conv_array<type>::to_js(val); }
};
/**
* Conversions for std::deque<T>
*/
template <typename T> struct conv<std::deque<T>>
{
typedef std::deque<T> type;
static inline type from_js(const v8::Handle<v8::Value>& val)
{ return conv_array<type>::from_js(val); }
static inline type from_js_strict(const v8::Handle<v8::Value>& val)
{ return conv_array<type>::from_js_strict(val); }
static inline v8::Handle<v8::Value> to_js(const type& val)
{ return conv_array<type>::to_js(val); }
};
// </editor-fold>
// <editor-fold desc="conversion traits - object containers" defaultstate="collapsed">
namespace {
/**
* Conversion to containers that operator [...] = ...
*/
template <typename MapContainer>
struct conv_map
{
typedef MapContainer type;
// +++ From JS later because required recursion checks +++
static v8::Handle<v8::Value> to_js(const type& val)
{
v8::Local<v8::Object> r(v8::Object::New(v8::Isolate::GetCurrent()));
if(r.IsEmpty()) throw v8i::not_convertible_error("Creating object failed");
for(typename type::const_iterator it = val.begin(); it != val.end(); ++it) {
r->Set(
conv<std::string>::to_js(it->first),
conv<decltype(it->second)>::to_js(it->second)
);
}
return v8::Handle<v8::Value>::Cast(r);
}
};
}
/**
* Conversions for std::map<T>
*/
template <typename T> struct conv<std::map<std::string, T>>
{
typedef std::map<std::string, T> type;
static inline v8::Handle<v8::Value> to_js(const type& val)
{ return conv_map<type>::to_js(val); }
static inline v8::Handle<v8::Value> to_js(type&& val)
{ return conv_map<type>::to_js(val); }
};
/**
* Conversions for std::unordered_map<T>
*/
template <typename T> struct conv<std::unordered_map<std::string, T>>
{
typedef std::unordered_map<std::string, T> type;
static inline v8::Handle<v8::Value> to_js(const type& val)
{ return conv_map<type>::to_js(val); }
};
// </editor-fold>
// <editor-fold desc="unpack_convert" defaultstate="collapsed">
namespace {
inline std::vector<v8::Handle<v8::Value>> unpack_convert()
{ return std::vector<v8::Handle<v8::Value>>(); }
template <typename ...Args>
inline std::vector<v8::Handle<v8::Value>> unpack_convert(Args ...args)
{ return std::vector<v8::Handle<v8::Value>> {conv<Args>::to_js(std::forward<Args>(args))...};}
}
// </editor-fold>
}
// <editor-fold desc="function_callback_info" defaultstate="collapsed">
/**
* Equivalent to v8::FunctionCallbackInfo<v8::Value>.
* Used as type separation for callback definitions in engine::define(...), where the selected
* callback type is not the proxied version `void function(v8i::function_callback_info&& ci)`,
* but the direct V8 API version. Using this callback type is not recommended because changes
* in V8 may affect your c++ callback implementation.
*/
typedef v8::FunctionCallbackInfo<v8::Value> function_callback_info_v8;
namespace templates {
/**
* object forward.
*/
template <typename InstanceType> class object;
/**
* Stack instantiated object in the callback functions, used to access and implicitly
* convert the v8 engines type `FunctionCallbackInfo<v8::Value>`.
* This slim wrapper implicitly sets the isolate scope, locks the isolate, and sets
* a handle scope. Unlock, handle scope exit etc happens when the object is destructed.
*
* Note: v8::Function callbacks using this function_callback_info class are NOT intended to be
* constructors. Therefore it will throw if `var o = new MyFunction(.....)` is
* invoked.
*
* Note: Re-entering almost no overhead in V8::Isolate->Enter(), so a stack allocated
* Isolate::Scope is ok, but likely not required. So a double check is needed
* if `iscope_` can be omitted.
*/
template <typename Select=void>
class function_callback_info
{
public:
function_callback_info() = delete;
/**
* Contructor from v8 arguments
* @param const js_args_t& args
*/
function_callback_info(const function_callback_info_v8& jsargs, bool construct=false) noexcept
: jsargs_(jsargs), lck_(jsargs.GetIsolate()), iscope_(jsargs.GetIsolate()),
hscope_(jsargs.GetIsolate()), noreturn_(construct), args(jsargs_)
{ }
~function_callback_info()
{
if(!noreturn_ && jsargs_.IsConstructCall()) {
return_exception("That native function cannot be called as constructor.");
}
}
/**
* Set the return value.
* @param typename T val
*/
template <typename T, typename std::enable_if<std::is_floating_point<T>::value ||
(std::is_integral<T>::value && (sizeof(T) > 4)), int>::type=0>
void return_value(T val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set((double) val); }
/**
* Set the return value.
* @param typename (uint8 to uint32) val
*/
template <typename T, typename std::enable_if<std::is_integral<T>::value &&
std::is_unsigned<T>::value && sizeof(T) <= 4, int>::type=0>
void return_value(T val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set((uint32_t) val); }
/**
* Set the return value.
* @param typename (int16 to int32) val
*/
template <typename T, typename std::enable_if<std::is_integral<T>::value &&
!std::is_unsigned<T>::value && sizeof(T) <= 4 && !std::is_same<T, char>::value,
int>::type=0>
void return_value(T val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set((int32_t) val); }
/**
* Set the return value.
* @param char|typed pointer|const char* val
*/
template <typename T, typename std::enable_if<std::is_same<T, char>::value ||
(std::is_pointer<T>::value && !std::is_same<T, void*>::value), int>::type = 0>
void return_value(T val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(type::conv<T>::to_js(val)); }
/**
* Set the return value.
* @param typename T&& val
*/
template <typename T, typename std::enable_if<!std::is_lvalue_reference<T>::value &&
!std::is_arithmetic<T>::value && !std::is_pointer<T>::value, int>::type = 0>
void return_value(T&& val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(type::conv<T>::to_js(std::move(val))); }
/**
* Set the return value.
*/
template <typename T, typename std::enable_if<!std::is_reference<T>::value &&
!std::is_fundamental<T>::value && !std::is_pointer<T>::value &&
!std::is_same<T,char>::value && !std::is_array<T>::value, int>::type = 0>
void return_value(const T& val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(type::conv<T>::to_js(val)); }
/**
* Set the return value (bool)
*/
void return_value(bool val) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(val); }
/**
* Set the return value (std::atomic)
*/
template <typename T>
void return_value(std::atomic<T>& val) const
{ T v = val; if(!noreturn_) jsargs_.GetReturnValue().Set(type::conv<T>::to_js(v)); }
/**
* Returns a plain object, where even argument indices are keys (starting at 0) and odd
* indices are values. Keys must be string, values convertible. The number of arguments
* must be even (each key must have a value).
* @param typename Args ...args
*/
template <typename ...Args>
void return_object(Args ...args) const
{
if(noreturn_) return;
const unsigned len = std::tuple_size<decltype(std::make_tuple(args...))>::value;
static_assert(len % 2 == 0, "Wrong number of arguments");
v8::Handle<v8::Object> o = v8::Object::New(v8::Isolate::GetCurrent());
return_object_unpack(o, args...);
jsargs_.GetReturnValue().Set(o);
}
/**
* Instead of returning a value, throw an exception in the Javascript runtime.
* The method is called return because after throwing you must return.
*/
void return_exception(std::string&& message)
{
if(noreturn_) return;
noreturn_ = true;
v8i::js_exception::trigger(message);
}
/**
* Fast-set return value to undefined
*/
void return_undefined() const
{ if(!noreturn_) jsargs_.GetReturnValue().SetUndefined(); }
/**
* Fast-set return value to null
*/
void return_null() const
{ if(!noreturn_) jsargs_.GetReturnValue().SetNull(); }
/**
* Fast-set return value to ""
*/
void return_empty_string() const
{ if(!noreturn_) jsargs_.GetReturnValue().SetEmptyString(); }
/**
* Fast-set return value to ""
*/
void return_handle(const v8::Handle<v8::Value>& hnd) const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(hnd); }
/**
* Returns true if the call from javascript is acceptable - currently that means
* not a constructor call. You can return immediately if this method returns false.
* @return bool
*/
bool is_valid_call() const
{ return !jsargs_.IsConstructCall(); }
/**
*
* @return v8i::engine&
*/
v8i::engine& engine() const
{
v8::Local<v8::Context> ctx = jsargs_.GetIsolate()->GetCurrentContext();
if(ctx.IsEmpty() || ctx->Global().IsEmpty()) throw callback_error("Failed to get engine (global)");
v8::Local<v8::Value> obj = ctx->Global()->GetHiddenValue(v8str("v8i::engine"));
if(obj.IsEmpty() || !obj->IsObject()) throw callback_error("Failed to get engine (object)");
void* p = v8::Handle<v8::Object>::Cast(obj)->GetAlignedPointerFromInternalField(0);
if(!p) throw callback_error("Failed to get engine (engine no set)");
return *reinterpret_cast<v8i::engine*>(p);
}
/**
* Auxiliary type for (*this).args.length(), (*this).args.get(), etc.
*/
struct args_t
{
args_t() = delete;
args_t(const function_callback_info_v8& args) : args_(args) {}
/**
* Length / size of the argument array
* @return int
*/
inline int length() const noexcept
{ return args_.Length(); }
/**
* Get an argument, convert non-strict from javascript to native type
* @param int index
* @return typename T
*/
template <typename T, bool Strict=true>
inline T get(int index) const
{
if(Strict) {
if(index < 0 || index > args_.Length()) {
throw not_convertible_error("Wrong number of arguments.");
}
return type::conv<T>::from_js_strict(args_[index]);
} else {
if(index < 0 || index > args_.Length()) {
return !std::is_floating_point<T>::value ? T() : std::numeric_limits<T>::quiet_NaN();
}
return type::conv<T>::from_js(args_[index]);
}
}
private:
const function_callback_info_v8& args_;
};
private:
/**
* Variadic argument unpack for return_object
*/
static void return_object_unpack(v8::Handle<v8::Object>& o)
{ }
/**
* Variadic argument unpack for return_object
*/
template <typename K, typename V, typename ...Args>
static void return_object_unpack(v8::Handle<v8::Object>& o, K key, V val, Args ...args)
{
o->Set(v8str(key), type::conv<V>::to_js(val));
return_object_unpack(o, args...);
}
protected:
// Do not change the positions of these instance variables (see constructor
// initialisation list).
const function_callback_info_v8& jsargs_;
v8::Locker lck_;
v8::Isolate::Scope iscope_; // Re-entering almost no overhead in V8::Isolate->Enter()
v8::HandleScope hscope_;
bool noreturn_;
public:
args_t args;
};
}
typedef templates::function_callback_info<> function_callback_info;
// </editor-fold>
// <editor-fold desc="method_callback_info" defaultstate="collapsed">
/**
* The arguments type for JS method callbacks.
*/
template <typename InstanceType>
class method_callback_info final : public function_callback_info
{
public:
typedef std::shared_ptr<InstanceType> instance_pointer;
typedef InstanceType& instance_reference;
typedef std::shared_ptr<templates::object<InstanceType>> object_pointer;
public:
method_callback_info() = delete;
method_callback_info(const function_callback_info_v8& jsargs, bool construct=false)
: function_callback_info<>(jsargs, construct),
object_(nullptr)
{
// Because of function_callback_info ctor, we are in a handle and isolate scope
if(jsargs_.Holder().IsEmpty() || !jsargs_.Holder()->IsObject()) {
base_t::return_exception("Reference Error: Method call without 'this' specification");
} else {
v8::Local<v8::Value> v = base_t::jsargs_.Holder()->GetHiddenValue(v8str("oid"));
if(!v.IsEmpty()) {
unsigned long id = v->NumberValue();
std::unique_lock<std::mutex> lck(object_t::map_lock_);
auto it = object_t::objects_.find(id);
if(it != object_t::objects_.end()) {
object_ = it->second;
}
}
if(object_ == nullptr) {
base_t::return_exception("Reference Error: Native object already deleted");
}
}
}
/**
* Returns a shared pointer to your instance (InstanceType) related to the Javascript
* object of which the method that you defined was called.
* Returns nullptr if the instance is already disposed.
* @return instance_pointer
*/
instance_pointer instance() const
{
auto inst = (object_ == nullptr || object_->disposed()) ? nullptr : object_->instance();
if(inst == nullptr) throw v8i::callback_error("Object already disposed");
return inst;
}
/**
* Disposes your instance, so that the memory is freed. The Garbage collector may
* collect the related Javascript object later.
*/
void dispose_instance() const
{ if(object_ != nullptr) object_->dispose(); }
/**
* Returns true if your instance bound to this object is already disposed (by you).
* If so, the method instance() will throw.
*/
bool instance_disposed() const noexcept
{ return object_ == nullptr || object_->disposed(); }
/**
* Fast-set return this.
*/
void return_this() const
{ if(!noreturn_) jsargs_.GetReturnValue().Set(jsargs_.Holder()); }
/**
* Returns a new Javascript object bound to an instance given by the pointer.
* @param std::shared_ptr<InstanceType>&& inst_ptr
*/
void return_new(std::shared_ptr<InstanceType>&& inst_ptr) const
{
if(noreturn_) return;
v8::Local<v8::Function> ctor = v8::Local<v8::Function>::Cast(jsargs_.Holder()->Get(v8str("constructor")));
if(ctor.IsEmpty() || !ctor->IsCallable()) {
throw callback_error("return_new() failed because JS constructor is not callable");
}
v8::Local<v8::Value> argv[1] = { v8::Handle<v8::Value>() };
v8::Local<v8::Object> o = v8::Local<v8::Object>::Cast(ctor->CallAsConstructor(0, argv));
if(o.IsEmpty() || !o->IsObject()) return; // external try-catch shall take care of that
v8::Local<v8::Value> hoid = o->GetHiddenValue(v8str("oid"));
if(hoid.IsEmpty() || !hoid->IsNumber()) throw callback_error("return_new() Missing object id");
unsigned long id = hoid->NumberValue();
std::unique_lock<std::mutex> lck(object_t::map_lock_);
auto it = object_t::objects_.find(id);
if(it != object_t::objects_.end()) {
object_pointer obj = it->second;
if(obj != nullptr && !obj->disposed()) {
obj->instance_ = inst_ptr;
jsargs_.GetReturnValue().Set(o);
return;
}
}
throw callback_error("return_new() Failed to copy create object.");
}
private:
typedef function_callback_info<> base_t;
typedef templates::object<InstanceType> object_t;
object_pointer object_; // pointer to the related object<T> instance ("this")
};
// </editor-fold>
// <editor-fold desc="object" defaultstate="collapsed">
namespace templates {
template <typename InstanceType>
class object final
{
// <editor-fold desc="friends" defaultstate="collapsed">
friend class method_callback_info<InstanceType>;
template <int N> friend class engine;
// </editor-fold>
public:
// <editor-fold desc="types" defaultstate="collapsed">
typedef std::shared_ptr<InstanceType> instance_pointer;
typedef void (*method_callback_t)(method_callback_info<InstanceType>&& info);
typedef struct { std::string name; method_callback_t function; } method_decl_t;
typedef struct { std::string name; method_callback_t get; method_callback_t set; } property_decl_t;
typedef std::vector<method_decl_t> methods_decl_t;
typedef std::vector<property_decl_t> properties_decl_t;
// </editor-fold>
// <editor-fold desc="ctor/dtor" defaultstate="collapsed">
object() : id_(0), jsDisposed_(false), related_isolate_(nullptr), instance_(nullptr)
{ }
object(const object& o) = delete;
~object() // intentionally non-virtual
{
try {
if(!jsDisposed_ && !jsobject_.IsEmpty()) jsobject_.Reset();
} catch(...) {
assert(false);
}
}
// </editor-fold>
public:
// <editor-fold desc="getters/setters/public operations" defaultstate="collapsed">
/**
* Returns a shared pointer to your InstanceType instance OR throws a callback_error
* if it is already disposed.
* @return instance_pointer
*/
instance_pointer instance() const
{
if(disposed()) throw callback_error("Native instance is already disposed");
return instance_pointer(instance_);
}
/**
* Returns true if the script engine has garbage collected the javascript object related to
* this wrapper object.
* @return bool
*/
bool collected() const noexcept
{ return jsDisposed_; }
/**
* Returns true if your InstanceType object contained in this wrapper container is deleted
* or better if the shared pointer of this wrapper is nullptr.
* @return bool
*/
bool disposed() const noexcept
{ return instance_ == nullptr; }
/**
* Deletes the contained native instance (removes this object's shared_ptr reference).
*/
void dispose()
{
std::unique_lock<std::mutex> lck(map_lock_);
#ifdef V8I_DOUBLE_CHECKS
if(jsDisposed_) {
// Double check that first
auto it = objects_.find(id_);
if(it != objects_.end()) objects_.erase(it);
}
#endif
instance_ = nullptr;
}
/**
* Clears the whole static container of this InstanceType class specialisation.
* Note: Don't do this if the related isolates are running.
*/
static void reset(const v8::Isolate* isolate=nullptr)
{
std::unique_lock<std::mutex> lck(map_lock_);
if(isolate == nullptr) {
objects_.clear();
curr_id_ = 0;
} else {
auto it = objects_.begin();
while(it != objects_.end()) {
if(it->second->related_isolate_ == isolate) {
it = objects_.erase(it);
} else {
++it;
}
}
}
}
// </editor-fold>
private:
// <editor-fold desc="instantiate (contained instance)" defaultstate="collapsed">
/**
* Contained native instance instantiation, where the class has NO constructor
* MyClass(v8i::method_callback_info<MyClass>&& cbi)
* @param v8i::method_callback_info<T>&& cbi
*/
template <typename T>
typename std::enable_if<
!std::is_constructible<T, v8i::method_callback_info<T>&&>::value &&
std::is_same<T, InstanceType>::value
>
::type instantiate(v8i::method_callback_info<T>&& cbi)
{
if(cbi.args.length() == 0) {
instance_ = std::make_shared<T>();
} else {
throw v8i::callback_error("Constructor does not accept arguments.");
}
}
/**
* Contained native instance instantiation, where the class HAS a constructor
* MyClass(v8i::method_callback_info<MyClass>&& cbi)
* @param v8i::method_callback_info<T>&& cbi
*/
template <typename T>
typename std::enable_if<
std::is_constructible<T, v8i::method_callback_info<T>&&>::value &&
std::is_same<T, InstanceType>::value
>
::type instantiate(v8i::method_callback_info<T>&& cbi)
{
if(cbi.args.length() == 0) {
instance_ = std::make_shared<T>();
} else {
instance_ = std::make_shared<T>(std::move(cbi));
}
}
// </editor-fold>
// <editor-fold desc="js constructor template / construction callback" defaultstate="collapsed">
/**
* Template of the object constructor. Must be called in the appropriate isolate and handle
* scope.
* @param const std::string & name
* @return v8::Local<v8::Function>
*/
static v8::Local<v8::Function> constructor_function(const std::string & name,
methods_decl_t&& methods, properties_decl_t&& properties)
{
v8::Isolate* isl = v8::Isolate::GetCurrent();
v8::Local<v8::FunctionTemplate> ft = v8::FunctionTemplate::New(isl, constructor_callback);
ft->SetClassName(v8str(name));
ft->InstanceTemplate()->Set(v8str("class"), v8str(name),
(v8::PropertyAttribute) (v8::ReadOnly|v8::DontDelete|v8::DontEnum)
);
ft->InstanceTemplate()->Set(
v8str("dispose"),
v8::FunctionTemplate::New(isl, dispose_callback)->GetFunction(),
(v8::PropertyAttribute) (v8::DontDelete | v8::ReadOnly | v8::DontEnum)
);
{
ft->InstanceTemplate()->SetAccessorProperty(
v8str("disposed"),
v8::FunctionTemplate::New(isl, disposed_callback),
v8::FunctionTemplate::New(isl, nop_callback),
(v8::PropertyAttribute) (v8::DontDelete | v8::ReadOnly | v8::DontEnum),
(v8::AccessControl) (v8::ALL_CAN_READ)
);
}
std::unique_lock<std::mutex> lck(map_lock_);
// Methods
bool has_array_get = false, has_array_set = false, has_array_query = false,
has_array_delete = false, has_array_enum = false;
for(auto &e : methods) {
if(e.function == nullptr) {
throw engine_error(std::string("Callback pointer is nullptr for method ") + e.name);
}
defined_methods_.push_back(reinterpret_cast<void(*)()>(e.function));
v8::Local<v8::Value> data = v8::Uint32::New(isl, defined_methods_.size()-1);
v8::Local<v8::Value> fn = v8::FunctionTemplate::New(isl, method_proxy, data)->GetFunction();
if(e.name == "call()") {
// Special method: Call object like a function
ft->InstanceTemplate()->SetCallAsFunctionHandler(method_proxy, data);
} else {
// Normal method
ft->InstanceTemplate()->Set(v8str(e.name), fn,
(v8::PropertyAttribute) (v8::DontDelete | v8::ReadOnly | v8::DontEnum)
);
}
// Special methods: array access
if (e.name == "get[]") has_array_get = true;
else if(e.name == "set[]") has_array_set = true;
else if(e.name == "query[]") has_array_query = true;
else if(e.name == "delete[]") has_array_delete = true;
else if(e.name == "enum[]") has_array_enum = true;
}
// Array index methods
if(has_array_get) {
ft->InstanceTemplate()->SetIndexedPropertyHandler(
array_index_forwards::getter,
has_array_set ? array_index_forwards::setter : 0,
has_array_query ? array_index_forwards::query : 0,
has_array_delete ? array_index_forwards::eraser : 0,
has_array_enum ? array_index_forwards::enumerator : 0
);
}
// Properties
for(auto &e : properties) {
if(e.get == nullptr && e.set == nullptr) {
throw engine_error(std::string("Both getter and setter are nullptr for ") + e.name);
}
defined_methods_.push_back(e.get != nullptr ? reinterpret_cast<void(*)()>(e.get) :
reinterpret_cast<void(*)()>(nop_callback));
defined_methods_.push_back(e.set != nullptr ? reinterpret_cast<void(*)()>(e.set) :
reinterpret_cast<void(*)()>(nop_callback));
v8::Local<v8::FunctionTemplate> fn_get = v8::FunctionTemplate::New(isl, method_proxy,
v8::Uint32::New(isl, defined_methods_.size()-2));
v8::Local<v8::FunctionTemplate> fn_set = v8::FunctionTemplate::New(isl, method_proxy,
v8::Uint32::New(isl, defined_methods_.size()-1));
ft->InstanceTemplate()->SetAccessorProperty(
v8str(e.name), fn_get, fn_set,
(v8::PropertyAttribute) (v8::DontDelete | (e.set==nullptr ? v8::ReadOnly : 0) ), // v8::DontEnum ?
(v8::AccessControl) (
(e.get==nullptr ? 0 : v8::ALL_CAN_READ) |
(e.set==nullptr ? 0 : v8::ALL_CAN_WRITE)
)
);
}
ft->SetHiddenPrototype(true);
ft->ReadOnlyPrototype();
return ft->GetFunction();
}
/**
* v8::Object construction callback
* @param const FunctionCallbackInfo<v8::Value>& args
*/
static void constructor_callback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
v8::Isolate::Scope iscope(args.GetIsolate()); // stfwi: check if this can be omitted
if(!args.IsConstructCall()) {
return js_exception::trigger("This is a constructor, not a function.");
}
std::shared_ptr<object> wo;
unsigned long id;
try {
wo = std::make_shared<object>();
std::unique_lock<std::mutex> lck(map_lock_);
id = curr_id_ += 1;
if(objects_.find(id) != objects_.end()) {
// That should be a really rare case
if(objects_.size() >= objects_.max_size()) {
return js_exception::trigger("Too many native objects.");
}
id = curr_id_ += 1;
while(objects_.find(id) != objects_.end()) id=curr_id_ += 1;
assert(objects_.find(id) == objects_.end());
}
objects_[id] = wo;
} catch(const std::exception& e) {
return js_exception::trigger(std::string("Object construction failed: ") + e.what());
} catch(...) {
return js_exception::trigger("Memory allocation failed.");
}
try {
v8::Isolate * isl = args.GetIsolate();
v8::HandleScope handle_scope(isl);
v8::Handle<v8::Object> o = args.Holder();
if(o.IsEmpty() || !o->IsObject()) {
return js_exception::trigger(
"v8i::object::constructor_callback: Bad instance template (internal error)."
);
}
o->SetHiddenValue(v8str("oid"), v8::Number::New(isl, id));
{
std::unique_lock<std::mutex> lck(map_lock_);
wo->related_isolate_ = isl;
wo->id_ = id;
wo->jsobject_.Reset(isl, o);
wo->jsobject_.SetWeak(const_cast<char*>(&gc_check_address_), gc_callback);
}
{
v8i::method_callback_info<InstanceType> cbi(args, true);
wo->instantiate(std::move(cbi));
}
args.GetReturnValue().Set(wo->jsobject_);
} catch(const std::exception& e) {
wo->jsobject_.Reset();
std::unique_lock<std::mutex> lck(map_lock_);
auto it = objects_.find(id);
if(it != objects_.end()) objects_.erase(it);
return js_exception::trigger(std::string("Object instantiation failed: ") + e.what());
} catch(...) {
wo->jsobject_.Reset();
std::unique_lock<std::mutex> lck(map_lock_);
auto it = objects_.find(id);
if(it != objects_.end()) objects_.erase(it);
return js_exception::trigger("Object instantiation failed");
}
}
// </editor-fold>
// <editor-fold desc="callbacks (gc, builtin default functions)" defaultstate="collapsed">
/**
* "Weak callback" --> GC callback
* @param const v8::WeakCallbackData<v8::Object, T>& data
*/
static void gc_callback(const v8::WeakCallbackData<v8::Object, char>& data)
{
assert((data.GetParameter() == &gc_check_address_));
if(!data.GetValue().IsEmpty()) {
unsigned long id = data.GetValue()->GetHiddenValue(
v8str("oid", data.GetIsolate()))->NumberValue();
{
std::unique_lock<std::mutex> lck(map_lock_);
auto it = objects_.find(id);
if(it != objects_.end()) {
std::shared_ptr<object> p = it->second;
objects_.erase(it);
lck.unlock();
if(p.use_count() > 0 && p != nullptr) {
p->jsDisposed_ = true;
p->jsobject_.Reset();
}
}
}
}
data.GetValue().Clear();
}
/**
* Callback for js: myobject.dispose();
* @param const FunctionCallbackInfo<v8::Value>& args
*/
static void dispose_callback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
if(args.This().IsEmpty()) return;
v8::HandleScope handle_scope(args.GetIsolate());
v8::Local<v8::Value> oid(args.This()->GetHiddenValue(v8str("oid")));
if(oid.IsEmpty()) return;
unsigned long id = oid->NumberValue();
std::unique_lock<std::mutex> lck(map_lock_);
auto it = objects_.find(id);
if(it != objects_.end()) {
std::shared_ptr<object> p = it->second;
if(p != nullptr && p->instance_ != nullptr) {
p->instance_ = nullptr;
}
}
}
/**
* Callback for js: myobject.disposed();
* @param const FunctionCallbackInfo<v8::Value>& args
*/
static void disposed_callback(const v8::FunctionCallbackInfo<v8::Value>& args)
{
if(args.This().IsEmpty()) return;
v8::HandleScope handle_scope(args.GetIsolate());
v8::Local<v8::Value> oid(args.This()->GetHiddenValue(v8str("oid")));
if(oid.IsEmpty()) return;
unsigned long id = oid->NumberValue();
std::unique_lock<std::mutex> lck(map_lock_);
auto it = objects_.find(id);
if(it != objects_.end()) {
std::shared_ptr<object> p = it->second;
args.GetReturnValue().Set(p == nullptr || p->instance_ == nullptr);
} else {
args.GetReturnValue().Set(true);
}
}
/**
* Callback for NOP / no operation.
* @param const v8::FunctionCallbackInfo<v8::Value>& args
*/
static void nop_callback(const v8::FunctionCallbackInfo<v8::Value>& args)
{ args.GetReturnValue().SetUndefined(); }
// </editor-fold>
// <editor-fold desc="method_proxy" defaultstate="collapsed">
/**
* Determines native instance callback pointer from the given arguments.
* @param const v8::FunctionCallbackInfo<v8::Value>& args
*/
template <typename CallbackType>
static CallbackType get_function(const v8::FunctionCallbackInfo<v8::Value>& args)
{
if(args.Data().IsEmpty() || !args.Data()->IsUint32()) {
throw engine_error("method_proxy(): arguments data attachment is not valid (FATAL)");
}
unsigned fid = args.Data()->Uint32Value();
if(fid < defined_methods_.size()) {
auto fptr = reinterpret_cast<CallbackType>(defined_methods_[fid]);
if(fptr) return fptr;
}
throw engine_error("method_proxy(): Native function missing (FATAL)");
}
/**
* Redirects V8 calls to native functions
* @param const v8::FunctionCallbackInfo<v8::Value>& args
*/
static void method_proxy(const v8::FunctionCallbackInfo<v8::Value>& args)
{
v8::Isolate::Scope isope(args.GetIsolate()); // stfwi: check if this can be omitted
v8::HandleScope handle_scope(args.GetIsolate());
auto fun = get_function<method_callback_t>(args);
v8::TryCatch try_catch;
try {
v8i::method_callback_info<InstanceType> fci(args);
fun(std::move(fci));
} catch(const v8i::not_convertible_error & e) {
v8i::js_exception::trigger(e.what());
} catch(const v8i::callback_error & e) {
v8i::js_exception::trigger(e.what());
} catch(const v8i::js_exception& e) {
if(try_catch.HasCaught()) {
try_catch.ReThrow();
} else {
v8i::js_exception::trigger(e.message());
}
}
}
// </editor-fold>
// <editor-fold desc="array_index_forwards" defaultstate="collapsed">
struct array_index_forwards
{
/**
* V8 doc: Returns the value of the property if the getter intercepts the
* request. Otherwise, returns an empty handle.
*/
static void getter(uint32_t index, const v8::PropertyCallbackInfo<v8::Value>& info)
{
v8::HandleScope handle_scope(info.GetIsolate());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(
info.Holder()->GetRealNamedProperty(v8str("get[]")));
if(fn.IsEmpty()) {
info.GetReturnValue().Set(v8::Handle<v8::Value>());
} else {
v8::Handle<v8::Value> args[1] { v8::Integer::New(info.GetIsolate(), index) };
v8::Handle<v8::Value> r = fn->Call(info.Holder(), 1, args);
info.GetReturnValue().Set(r); // Can set empty handle.
}
}
/**
* V8 doc: Returns the value if the setter intercepts the request.
* Otherwise, returns an empty handle.
*/
static void setter(uint32_t index, v8::Local<v8::Value> value,
const v8::PropertyCallbackInfo<v8::Value>& info)
{
v8::HandleScope handle_scope(info.GetIsolate());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(
info.Holder()->GetRealNamedProperty(v8str("set[]")));
if(fn.IsEmpty()) {
info.GetReturnValue().Set(v8::Local<v8::Value>());
} else {
v8::Local<v8::Value> args[2] {
v8::Integer::New(info.GetIsolate(), index),
value
};
v8::Local<v8::Value> r = fn->Call(info.Holder(), 2, args);
info.GetReturnValue().Set(r);
}
}
/**
* V8 doc Returns a non-empty handle if the interceptor intercepts the request.
* The result is an integer encoding property attributes.
*/
static void query(uint32_t index, const v8::PropertyCallbackInfo<v8::Integer>& info)
{
v8::HandleScope handle_scope(info.GetIsolate());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(
info.Holder()->GetRealNamedProperty(v8str("query[]")));
if(fn.IsEmpty()) {
info.GetReturnValue().Set(v8::Handle<v8::Integer>());
} else {
v8::Local<v8::Value> args[1] { v8::Integer::New(info.GetIsolate(), index) };
v8::Local<v8::Integer> r = v8::Local<v8::Integer>::Cast(fn->Call(info.Holder(), 1, args));
info.GetReturnValue().Set(r);
}
}
/**
* V8 doc: Returns a non-empty handle if the deleter intercepts the request.
* The return value is true if the property could be deleted and false
* otherwise.
*/
static void eraser(uint32_t index, const v8::PropertyCallbackInfo<v8::Boolean>& info)
{
v8::HandleScope handle_scope(info.GetIsolate());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(
info.Holder()->GetRealNamedProperty(v8str("delete[]")));
if(fn.IsEmpty()) {
info.GetReturnValue().Set(v8::Handle<v8::Boolean>());
} else {
v8::Local<v8::Value> args[1] { v8::Integer::New(info.GetIsolate(), index) };
fn->Call(info.Holder(), 1, args);
// If the callback does not throw it's ok
info.GetReturnValue().Set(v8::True(info.GetIsolate()));
}
}
/**
* V8 doc: Returns an array containing the indices of the properties the
* indexed property getter intercepts.
*/
static void enumerator(const v8::PropertyCallbackInfo<v8::Array>& info)
{
v8::HandleScope handle_scope(info.GetIsolate());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(
info.Holder()->GetRealNamedProperty(v8str("enum[]")));
if(fn.IsEmpty()) {
info.GetReturnValue().Set(v8::Local<v8::Array>());
} else {
v8::Local<v8::Value> args[1] { v8::Handle<v8::Value>() };
v8::Local<v8::Array> r = v8::Handle<v8::Array>::Cast(fn->Call(info.Holder(), 0, args));
info.GetReturnValue().Set(r);
}
}
};
// </editor-fold>
private:
// <editor-fold desc="instance/static variables" defaultstate="collapsed">
unsigned long id_; /// Id of this object, retrieved from curr_id_
std::atomic<bool> jsDisposed_; /// Describes if instance_ is supposed to be nullptr
v8::Persistent<v8::Object> jsobject_; /// V8 JavaScript object
v8::Isolate* related_isolate_; /// V8 Isolate of this object
instance_pointer instance_; /// The actual native instance contained in this object
static std::atomic<unsigned long> curr_id_; /// Instance id counter
static std::unordered_map<unsigned long, std::shared_ptr<object>> objects_; /// The used objects
static std::mutex map_lock_; /// Interlock for `objects_`
static std::vector<void(*)()> defined_methods_; /// function-pointer container
static const char gc_check_address_; /// "magic" char to have a pointer for the gc_callback
// </editor-fold>
};
// <editor-fold desc="statics init" defaultstate="collapsed">
template <typename T>
std::atomic<unsigned long> object<T>::curr_id_;
template <typename T>
std::unordered_map<unsigned long, std::shared_ptr<object<T>>> object<T>::objects_;
template <typename T>
std::mutex object<T>::map_lock_;
template <typename T>
const char object<T>::gc_check_address_ = '#';
template <typename T>
std::vector<void(*)()> object<T>::defined_methods_;
// </editor-fold>
}
// </editor-fold>
// <editor-fold desc="typed_array (only skeleton)" defaultstate="collapsed">
namespace templates {
/**
* Buffer allocation override. NOTE: will be modified to non-C allocation to prevent
* leaks. Typed arrays are not seen safe enough yet to be really used.
*/
template <typename=void>
class array_allocator : public v8::ArrayBuffer::Allocator
{
template<int N> friend class engine;
public:
virtual void* Allocate(size_t length) { return ::calloc(length, 1); }
virtual void* AllocateUninitialized(size_t length) { return ::malloc(length); }
virtual void Free(void* data, size_t) { if(data) ::free(data); }
private:
static void initialize() { v8::V8::SetArrayBufferAllocator(&instance_); }
static array_allocator instance_;
};
template <typename T> array_allocator<T> array_allocator<T>::instance_;
/**
* Typed array class skeleton.
*/
template <typename ValueType=unsigned char>
class typed_array
{
};
}
// </editor-fold>
// <editor-fold desc="engine" defaultstate="collapsed">
/**
* Use the specialisation `v8i::engine`, NOT this template because this template has to be
* unique - otherwise some features of V8 will be accidentally initialised twice!
*/
namespace templates {
/**
* engine template
*/
template <int Reserved>
class engine final
{
public:
// <editor-fold desc="types/enums" defaultstate="collapsed">
/**
* Alias for FunctionCallback in case this type changes in v8.
*/
typedef v8::FunctionCallback v8_callback_function_t;
/**
* Callback type for proxied native functions
*/
typedef void (*callback_function_t)(v8i::function_callback_info&& info);
/**
* Enum specifying how to define functions, objects and values in the global object (nested).
* Default is "shadowed", which means "hard-sealed-frozen" and not visible when enumerating
* the global object (or nested) - practically :
* - "static, persistent, undeletable/constant" functions and values that can be accessed
* when knowing where they are.
* - Builtin commands like print, console.log, console.write ...
*
* Other enum values, or bit-or'ed combinations loose this strict constrain.
*/
enum define_options { defopt_shadowed=0, defopt_overwritable=1, defopt_deletable=2,
defopt_enumerable=4, defopt_mutable=7 };
// </editor-fold>
public:
// <editor-fold desc="ctor/dtor" defaultstate="collapsed">
engine() noexcept : isolate_(nullptr), global_context_(), num_running_evals_(0),
interruption_mtx_()
{ ; }
engine(const engine&) = delete;
engine(engine&&) = delete;
~engine() noexcept // intentionally non-virtual
{
try {
terminate();
} catch(...) {
#ifdef DEBUG
std::cerr "~engine(): terminate failed." << endl;
#endif
}
}
// </editor-fold>
// <editor-fold desc="initialize/terminate" defaultstate="collapsed">
/**
* Initialises the engine instance, which is NOT done in the constructor.
* The method may be called multiple times, only the first initialisation has effect.
* On the first initialisation since program startup, global v8 initialisation takes
* place. The engine is not usable if it is not initialised.
* @return engine&
*/
engine& initialize()
{
if(!class_initialized_) {
class_initialized_ = true;
v8::V8::InitializeICU();
array_allocator<>::initialize();
}
if(initialized()) {
return *this;
}
isolate_ = v8::Isolate::New();
if(!isolate()) {
throw engine_error("v8i::engine: Failed to create v8::Isolate instance (engine unusable!).");
}
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> ctx = v8::Context::New(isolate(), NULL);
v8::Context::Scope context_scope(ctx);
{
global_context_.Reset(isolate(), ctx);
v8::Local<v8::ObjectTemplate> tpl = v8::ObjectTemplate::New(isolate());
tpl->SetInternalFieldCount(1);
v8::Local<v8::Object> obj = tpl->NewInstance();
obj->SetAlignedPointerInInternalField(0, this);
ctx->Global()->SetHiddenValue(v8str("v8i::engine"), obj);
}
define("system"); // ro, noenum, nodel
define("system.engine", defopt_enumerable); // ro, enum, nodel
define("system.engine.includes"); // included files, ro, enum, nodel, hidden internal data
define("system.engine.name", "V8/v8i::engine", defopt_enumerable);
define("system.engine.version", v8::V8::GetVersion(), defopt_enumerable);
{
// List of exported included files
v8::Local<v8::Array> arr = v8::Array::New(isolate(), 0);
find<v8::Object>("system.engine")->ForceSet(
v8str("included"), arr, (v8::PropertyAttribute) (v8::DontDelete | v8::ReadOnly)
);
}
return *this;
}
/**
* Terminate the runtime context of this engine.
* NOTE: To terminate the current script execution, use the abort() method,
* this is to terminate the engine for good.
* @return engine&
*/
engine& terminate()
{
if(isolate()) {
auto isl = isolate();
v8::V8::TerminateExecution(isolate());
{
// Race condition checked: ilck and lck are independent
std::unique_lock<decltype(interruption_mtx_)> ilck(interruption_mtx_);
v8::Locker lck(isolate());
isolate_ = nullptr; // --> state not initialised
global_context_.Reset();
num_running_evals_ = 0; // is already, but ensure
defined_functions_.clear();
include_base_path_.clear();
}
isl->Dispose();
}
return *this;
}
// </editor-fold>
public:
// <editor-fold desc="getters/setters" defaultstate="collapsed">
/**
* Returns the V8 context (persistent) of this instance.
* @return v8::Persistent<v8::Context>&
*/
v8::Persistent<v8::Context>& context() noexcept
{ return global_context_; }
/**
* Returns the pointer to the V8 isolate in which this engine instance lives.
* @return v8::Isolate*
*/
inline v8::Isolate* isolate() noexcept
{ return isolate_; }
/**
* Returns true if this instance is initialised and ready to run or define functions and
* objects.
* @return bool
*/
bool initialized() const noexcept
{ return (isolate_ != nullptr) && (!global_context_.IsEmpty()); }
/**
* Returns the source code of an included file
* @param std::string origin
* @return std::vector<std::string>
*/
std::string included_file_source(std::string origin)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
v8::Local<v8::Object> reg_obj = find<v8::Object>("system.engine.includes");
if(reg_obj.IsEmpty() || !reg_obj->IsObject()) throw engine_error("system.engine.includes missing");
v8::Local<v8::Value> o = reg_obj->GetHiddenValue(v8str(origin));
if(o.IsEmpty() || !o->IsObject()) return std::string();
v8::Local<v8::Value> vs = v8::Local<v8::Object>::Cast(o)->Get(v8str("code"));
if(vs.IsEmpty() || !vs->IsString()) return std::string();
v8::String::Utf8Value s(vs);
return (*s) ? (*s) : "";
}
/**
* Returns the maximum recursion depth for engine::compile_run().
* @return int
*/
inline static int max_eval_recursion_depth() noexcept
{ return max_running_evals_; }
/**
* Sets the maximum recursion depth for engine::compile_run(). Minimum is 1.
* Main purpose of this setting is to force stopping potential recursion in
* callbacks that call engine::eval().
* @return int
*/
inline static void max_eval_recursion_depth(int n) noexcept
{ max_running_evals_ = n > 0 ? n : 1; }
// </editor-fold>
// <editor-fold desc="control methods" defaultstate="collapsed">
/**
* Stop the current runtime context execution of this engine
* @return engine&
*/
engine& abort()
{
if(isolate()) {
v8::V8::TerminateExecution(isolate());
}
return *this;
}
// </editor-fold>
// <editor-fold desc="define (v8_callback_function_t function)" defaultstate="collapsed">
/**
* Define a function in the isolates main context global scope.
*
* Note: The function name / object path must be correct, the name is not error checked
* (as it is supposed to be defined directly in the c++ source code, not from file
* or the like).
*
* @param std::string name
* @param v8_callback_function_t fn
* @param enum define_options define_opts = defopt_shadowed
* @return engine&
*/
engine& define(std::string name, v8_callback_function_t fn, enum define_options opts = defopt_shadowed)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
if(name.empty()) throw engine_error("define(): function name empty");
if(!std::isalpha(name[0])) throw engine_error("define(): function name invalid");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
{
v8::Local<v8::Object> obj;
std::string fnname = name;
auto p = fnname.find_last_of('.');
if(p == fnname.npos) {
obj = cnt->Global();
} else if(p >= fnname.length()-1) {
throw engine_error(std::string("define(): Invalid selector '") + name + "'");
} else {
std::string obj_struct = fnname.substr(0, p);
obj = implicit_define(std::move(obj_struct), opts);
fnname = fnname.substr(p+1);
}
v8::Local<v8::Value> key = v8str(fnname);
v8::Local<v8::Value> val = v8::FunctionTemplate::New(isolate(), fn)->GetFunction();
obj->ForceSet(key, val, (v8::PropertyAttribute) (
((opts & defopt_overwritable) ? 0 : v8::ReadOnly) |
((opts & defopt_deletable) ? 0 : v8::DontDelete) |
((opts & defopt_enumerable) ? 0 : v8::DontEnum)
));
}
{
v8::Handle<v8::Value> chk = find<v8::Value>(name);
if(chk.IsEmpty() || !chk->IsFunction()) {
throw engine_error(std::string("Failed to define '") + name + "'");
}
}
return *this;
}
// </editor-fold>
// <editor-fold desc="define (callback_function_t function)" defaultstate="collapsed">
/**
* Define a function in the isolates main context global scope.
*
* Note: The function name / object path must be correct, the name is not error checked
* (as it is supposed to be defined directly in the c++ source code, not from file
* or the like).
*
* @param std::string name
* @param callback_function_t fn
* @param enum define_options define_opts = defopt_shadowed
* @return engine&
*/
engine& define(std::string name, callback_function_t fun, enum define_options opts = defopt_shadowed)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
if(name.empty()) throw engine_error("define(): function name empty");
if(!std::isalpha(name[0])) throw engine_error("define(): function name not starting with a letter");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
{
std::string fnname = name;
v8::Local<v8::Object> obj;
auto p = fnname.find_last_of('.');
if(p == fnname.npos) {
obj = cnt->Global();
} else if(p >= fnname.length()-1) {
throw engine_error(std::string("define(): Invalid selector '") + name + "'");
} else {
std::string obj_struct = fnname.substr(0, p);
obj = implicit_define(std::move(obj_struct), opts);
fnname = fnname.substr(p+1);
}
defined_functions_.push_back(reinterpret_cast<void(*)()>(fun));
unsigned long fid = defined_functions_.size()-1;
v8::Local<v8::Value> key = v8str(fnname);
v8::Local<v8::Value> data = v8::Uint32::New(isolate(), fid);
v8::Local<v8::Value> fn = v8::FunctionTemplate::New(isolate(), function_proxy, data)->GetFunction();
obj->ForceSet(key, fn, (v8::PropertyAttribute) (
((opts & defopt_overwritable) ? 0 : v8::ReadOnly) |
((opts & defopt_deletable) ? 0 : v8::DontDelete) |
((opts & defopt_enumerable) ? 0 : v8::DontEnum)
));
}
{
v8::Handle<v8::Value> chk = find<v8::Value>(name);
if(chk.IsEmpty() || !chk->IsFunction()) {
throw engine_error(std::string("Failed to define '") + name + "'");
}
}
return *this;
}
// </editor-fold>
// <editor-fold desc="define (empty object)" defaultstate="collapsed">
/**
* Defines an empty object by name ("path" / selector),
* e.g. "sys.my.detail" --> global['sys'] = { my: { detail: { } } }
* @param std::string name
* @param define_options define_opts = defopt_shadowed
* @return engine&
*/
engine& define(std::string name, enum define_options opts = defopt_shadowed)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
implicit_define(std::move(name), opts);
return *this;
}
// </editor-fold>
// <editor-fold desc="define (constant value)" defaultstate="collapsed">
/**
* Define a constant in the isolates main context global scope.
*
* @param std::string name
* @param T value
* @param enum define_options define_opts = defopt_shadowed
* @return engine&
*/
template <typename T>
engine& define(std::string name, T value, enum define_options opts = defopt_shadowed)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
if(name.empty()) throw engine_error("define(): constant/value name is empty");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
{
std::string vname = name;
v8::Local<v8::Object> obj;
auto p = vname.find_last_of('.');
if(p == vname.npos) {
obj = cnt->Global();
} else if(p >= vname.length()-1) {
throw engine_error(std::string("define(): Invalid selector '") + name + "'");
} else {
std::string obj_struct = vname.substr(0, p);
obj = implicit_define(std::move(obj_struct), opts);
vname = vname.substr(p+1);
}
obj->ForceSet(
v8str(vname),
type::conv<T>::to_js(std::move(value)),
(v8::PropertyAttribute) (
((opts & defopt_overwritable) ? 0 : v8::ReadOnly) |
((opts & defopt_deletable) ? 0 : v8::DontDelete) |
((opts & defopt_enumerable) ? 0 : v8::DontEnum)
)
);
}
{
v8::Handle<v8::Value> chk = find<v8::Value>(name);
if(chk.IsEmpty()) {
throw engine_error(std::string("Failed to define '") + name + "'");
}
}
return *this;
}
// </editor-fold>
// <editor-fold desc="define (native object)" defaultstate="collapsed">
/**
* Define a weak object in the isolates main context global scope.
*
* Note: The function name / object path must be correct, the name is not error checked
* (as it is supposed to be defined directly in the c++ source code, not from file
* or the like).
*
* @param std::string name
* @param v8_callback_function_t fn
* @param enum define_options define_opts = defopt_shadowed
* @return engine&
*/
template <typename InstanceType>
engine& define(std::string name,
typename v8i::templates::object<InstanceType>::methods_decl_t&& methods = {},
typename v8i::templates::object<InstanceType>::properties_decl_t&& properties = {},
enum define_options opts = defopt_shadowed)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
if(name.empty()) throw engine_error("define(): object name empty");
if(!std::isalpha(name[0])) throw engine_error("define(): object name not starting with a letter");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
{
std::string oname = name;
v8::Local<v8::Object> obj;
auto p = oname.find_last_of('.');
if(p == oname.npos) {
obj = cnt->Global();
} else {
std::string obj_struct = oname.substr(0, p);
obj = implicit_define(std::move(obj_struct), opts);
oname = oname.substr(p+1);
}
v8::Local<v8::Function> fn = v8i::templates::object<InstanceType>::constructor_function(
oname, std::move(methods), std::move(properties)
);
v8::Local<v8::Value> key = v8str(oname);
obj->ForceSet(key, fn, (v8::PropertyAttribute) (
((opts & defopt_overwritable) ? 0 : v8::ReadOnly) |
((opts & defopt_deletable) ? 0 : v8::DontDelete) |
((opts & defopt_enumerable) ? 0 : v8::DontEnum)
));
}
{
v8::Handle<v8::Value> chk = find<v8::Value>(name);
if(chk.IsEmpty() || !chk->IsObject()) {
throw engine_error(std::string("Failed to define '") + name + "'");
}
}
return *this;
}
// </editor-fold>
// <editor-fold desc="define (include)" defaultstate="collapsed">
engine& define_include(std::string base_path=".")
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
if(!include_base_path_.empty()) throw engine_error("include() already enabled.");
if(base_path.empty()) {
throw engine_error("define_include(...): You MUST specify a base path, even if \".\"");
}
include_base_path_ = base_path;
define("include", include_callback);
// require() is deletable and overwritable
eval("if(typeof require==='undefined') require=function(){ "\
"throw 'Use var result = include(file)';};");
return *this;
}
// </editor-fold>
// <editor-fold desc="include (javascript file)" defaultstate="collapsed">
/**
* Compiles a file and runs it in global scope of this engine's isolate/context.
* Does not accept a source to be included twice.
* @param std::string filename
* @return engine&
*/
engine& include(std::string filename, bool hide_file_in_js_runtime=true)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> ctx = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(ctx);
try {
include_file(filename, true, hide_file_in_js_runtime);
} catch(const callback_error& e) {
throw engine_error(e.what());
}
return *this;
}
// </editor-fold>
// <editor-fold desc="eval (string source in global scope)" defaultstate="collapsed">
/**
* Evaluate (compile and run) a string script source code in the isolate global scope.
* @param std::string code
* @return return_type
*/
template <typename Return_Type=void, bool StrictConversion=true>
Return_Type eval(std::string code)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> ctx = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(ctx);
v8::Local<v8::Value> h = compile_run(std::move(code), "");
if(StrictConversion) {
return type::conv<Return_Type>::from_js_strict(h);
} else {
return type::conv<Return_Type>::from_js(h);
}
}
// </editor-fold>
// <editor-fold desc="call (js function, fetch result)" defaultstate="collapsed">
template <typename Return_Type=void,
bool StrictReturnConversion=true,
bool ImplicitThisCall=true,
typename ...Arguments
>
Return_Type call(std::string fun, Arguments&&... args)
{
if(!isolate()) throw engine_error("v8i::engine not initialized");
std::vector<std::string> chunks = split_selector(std::string(fun));
if(chunks.empty()) throw engine_error("No function specified to call (empty string)");
v8::Locker lck(isolate());
v8::Isolate::Scope isolate_scope(isolate());
v8::HandleScope handle_scope(isolate());
v8::Local<v8::Context> ctx = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(ctx);
v8::Local<v8::Object> h_next = ctx->Global();
while(chunks.size() > 1) {
v8::Local<v8::String> h_key = v8str(chunks.back());
v8::Local<v8::Value> v = h_next->Get(h_key);
if(v.IsEmpty() || !v->IsObject()) {
throw engine_error(std::string("v8::Function not found: '")+ fun + "' (" + chunks.back()
+ " is not an object)");
}
chunks.pop_back();
h_next = v8::Local<v8::Object>::Cast<v8::Value>(v);
}
v8::Local<v8::String> h_key = v8str(chunks.back());
v8::Local<v8::Function> fn = v8::Local<v8::Function>::Cast(h_next->Get(h_key));
if(fn.IsEmpty() || !fn->IsCallable()) {
throw engine_error(std::string("'") + fun + "' is not callable");
}
std::vector<v8::Handle<v8::Value>> argv = type::unpack_convert((std::forward<Arguments>(args))...);
v8::TryCatch try_catch;
v8::Local<v8::Value> r = fn->Call(ImplicitThisCall ? h_next : ctx->Global(),
argv.size(), argv.data());
if(try_catch.HasCaught()) {
throw_js_exception(try_catch);
} else if(r.IsEmpty()) {
#if V8_VERSION >= 3028026L
std::string origin = type::conv<std::string>::from_js(fn->GetScriptOrigin().ResourceName());
int line = type::conv<int>::from_js(fn->GetScriptOrigin().ResourceLineOffset());
throw js_exception("v8::Function call error", "v8::Function call error", origin, line);
#else
std::string origin;
throw js_exception("v8::Function call error", "v8::Function call error", "", 0);
#endif
}
if(StrictReturnConversion) {
return type::conv<Return_Type>::from_js_strict(r);
} else {
return type::conv<Return_Type>::from_js(r);
}
}
// </editor-fold>
public:
// <editor-fold desc="interruption" defaultstate="collapsed">
/**
* Stack allocated object to request a V8 execution interrupt.
* Note: Don't call this from any JavaScript callbacks, because the executing
* thread cannot interrupt itself. You will get an exception if you
* try, and the function callback proxy explicitly does not catch it for
* you. The program will abort because V8 does catch it neither.
*
* v8i::engine js;
* [...]
* void other_thread_function() {
* do_thinge();
* {
* v8i::engine interruption intr(js);
* [ V8 Interrupted ]
* }
* [ V8 continues running because intr is out of scope ]
* do_thinge();
* }
*/
class interruption
{
public:
interruption(const interruption&) = delete;
interruption(interruption&&) = delete;
interruption(engine& en, unsigned timeout_ms=0) : en_(en), interrupted_(false),
lck_(en.interruption_mtx_, std::defer_lock)
{
if((!timeout_ms && !lck_.try_lock()) || (timeout_ms > 0
&& !lck_.try_lock_for(std::chrono::milliseconds(timeout_ms)))) {
throw v8i::engine_error("Interruption failed");
}
en.isolate()->AddCallCompletedCallback(call_completed_callback);
en.isolate()->RequestInterrupt(interruption_callback, this);
auto end = std::chrono::high_resolution_clock::now() + std::chrono::milliseconds(timeout_ms);
while(!interrupted_) {
if(en.num_running_evals_ == 0 || (timeout_ms
&& std::chrono::high_resolution_clock::now() > end)) {
en_.isolate()->ClearInterrupt();
en_.isolate()->RemoveCallCompletedCallback(call_completed_callback);
throw v8i::engine_error("Interruption failed");
}
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
~interruption() noexcept
{
if(!interrupted_) return;
en_.isolate()->ClearInterrupt();
en_.isolate()->RemoveCallCompletedCallback(call_completed_callback);
}
// Problem: has no data pointer ... check other possibilities
static void call_completed_callback()
{ }
static void interruption_callback(v8::Isolate* isolate, void* data)
{
interruption* me = reinterpret_cast<interruption*>(data);
if(me) {
me->interrupted_ = true;
std::unique_lock<mtx_t> lck(me->en_.interruption_mtx_);
}
}
private:
typedef std::timed_mutex mtx_t;
engine& en_;
std::atomic<bool > interrupted_;
std::unique_lock<mtx_t> lck_;
};
// </editor-fold>
private:
// <editor-fold desc="throw_js_exception" defaultstate="collapsed">
/**
* Converts an exception that was thrown in the javascript execution context into a
* std::exception compliant c++ exception and throws it.
* @param v8::TryCatch & try_catch
*/
static void throw_js_exception(v8::TryCatch & try_catch)
{
if(!try_catch.HasCaught()) return;
std::string except;
if(try_catch.Exception().IsEmpty()) {
except = "(unspecified exception)";
} else {
v8::String::Utf8Value s(try_catch.Exception());
except = (*s) ? static_cast<const char*>(*s) : "exception";
}
v8::Local<v8::Message> msg = try_catch.Message();
if(!msg.IsEmpty()) {
std::string file;
#if V8_VERSION >= 3028026L
if(!msg->GetScriptOrigin().ResourceName().IsEmpty()
&& msg->GetScriptOrigin().ResourceName()->IsString()
) {
v8::String::Utf8Value s(msg->GetScriptOrigin().ResourceName());
if(*s) file = static_cast<const char*>(*s);
}
#else
if(!msg->GetScriptResourceName().IsEmpty()
&& msg->GetScriptResourceName()->IsString()
) {
v8::String::Utf8Value s(msg->GetScriptResourceName());
if(*s) file = static_cast<const char*>(*s);
}
#endif
std::string source_line;
v8::Local<v8::String> sl(msg->GetSourceLine());
if(!msg->GetSourceLine().IsEmpty() && msg->GetSourceLine()->IsString()) {
v8::String::Utf8Value s(msg->GetSourceLine());
if(*s) source_line = static_cast<const char*>(*s);
}
std::string stack_trace;
if(!try_catch.StackTrace().IsEmpty() && try_catch.StackTrace()->IsString()) {
v8::String::Utf8Value s(try_catch.StackTrace());
if(*s) stack_trace = static_cast<const char*>(*s);
}
int line = msg->GetLineNumber();
int start = msg->GetStartColumn()+1;
int end = msg->GetEndColumn()+1;
while(!stack_trace.empty() && std::isspace(stack_trace.back())) {
stack_trace.resize(stack_trace.length()-1);
}
std::stringstream ss;
using std::endl;
ss << "Javascript exception: \"" << except << "\" in ";
if(!file.empty() && file != "(string source)") ss << "file \"" << file << "\" ";
if(line > 0) ss << "line " << line;
if(start > 0 && end >= start) {
std::string ls;
{ std::stringstream sss; sss<< line; ls = sss.str(); }
ss << endl << "|" << endl << "| " << ls << ": " << source_line << endl << "| ";
for(long i=0; i<=(long)ls.length(); ++i) ss << " ";
for(int i=1; i<start; ++i) ss << "_";
for(int i=start; i<end; ++i) ss << "^";
}
if(!stack_trace.empty()) {
ss << endl << endl << "Stack trace:" << endl << stack_trace;
}
try_catch.ReThrow();
throw js_exception(ss.str(), except, file, line, start, source_line, stack_trace);
}
}
// </editor-fold>
// <editor-fold desc="compile_run" defaultstate="collapsed">
/**
* Compile and run a string script source code in the isolate global scope.
*
* NOTE: Works in an already defined handle scope! Prefix something like:
*
* v8::Locker lck(isolate());
* v8::Isolate::Scope isolate_scope(isolate());
* v8::HandleScope handle_scope(isolate());
* v8::Local<v8::Context> ctx = v8::Local<v8::Context>::New(isolate(), context());
* v8::Context::Scope context_scope(ctx);
*
* @param std::string&& code
* @param std::string&& origin
* @return v8::Handle<v8::Value>
*/
v8::Handle<v8::Value> compile_run(std::string&& code, std::string origin)
{
run_recursion_monitor mon(this);
if(num_running_evals_ > max_running_evals_) {
throw js_exception("Recursion", "Recursion", origin);
}
if(origin.empty()) origin = "(eval)";
v8::TryCatch try_catch;
v8::Local<v8::Value> name = v8str(origin);
v8::Local<v8::String> src = v8str(code);
v8::ScriptOrigin orig(name);
v8::Local<v8::Script> comp = v8::Script::Compile(src, &orig);
if(comp.IsEmpty()) {
throw_js_exception(try_catch); // try to get a normal js_exception first
throw js_exception("Compile error", "Compile error", origin);
}
v8::Handle<v8::Value> result = comp->Run();
if(try_catch.HasCaught()) {
throw_js_exception(try_catch);
} else if(result.IsEmpty()) {
throw js_exception("Execution error", "Execution error", origin);
}
return result;
}
// </editor-fold>
// <editor-fold desc="include_script" defaultstate="collapsed">
/**
* Compiles a file and runs it in global scope of this engine's isolate/context.
* Returns a handle to the result.
* @param std::string filename
* @return v8::Handle<v8::Value>
*/
v8::Handle<v8::Value> include_file(std::string file, bool not_twice=false,
bool hide_file_in_js_runtime=true)
{
v8::Handle<v8::Object> reg_obj = find<v8::Object>("system.engine.includes");
if(reg_obj.IsEmpty() || !reg_obj->IsObject()) throw engine_error("system.engine.includes missing");
{
v8::Handle<v8::Object> cache = v8::Handle<v8::Object>::Cast(reg_obj->GetHiddenValue(v8str(file)));
if(!cache.IsEmpty() && cache->IsObject()) {
if(not_twice) throw callback_error(std::string("File ") + file + " already included");
v8::Local<v8::Value> o = cache->Get(v8str("value"));
if(o.IsEmpty()) throw engine_error(std::string("File ") + file + ": cache missing (FATAL)");
return o;
}
}
std::ifstream is(file);
if(!is) throw callback_error(std::string("Could not include '") + file);
std::string code((std::istreambuf_iterator<char>(is)), std::istreambuf_iterator<char>());
if(is.bad() && !is.eof()) throw callback_error(std::string("Could not include '") + file);
is.close();
if(!hide_file_in_js_runtime) {
v8::Local<v8::Array> arr = find<v8::Array>("system.engine.included");
arr->Set(arr->Length(), v8str(file));
}
{
v8::Local<v8::Object> o = v8::Object::New(v8::Isolate::GetCurrent());
if(o.IsEmpty()) throw engine_error("Failed to construct include information object");
o->ForceSet(v8str("file"), v8str(file), v8::ReadOnly);
o->ForceSet(v8str("code"), v8str(code), v8::ReadOnly);
reg_obj->SetHiddenValue(v8str(file), o);
v8::Handle<v8::Value> r = compile_run(std::move(code), file);
if(!o.IsEmpty() && !r.IsEmpty()) o->ForceSet(v8str("value"), r, v8::ReadOnly);
return r;
}
}
// </editor-fold>
// <editor-fold desc="implicit_define (empty object)" defaultstate="collapsed">
/**
* Defines an empty object by name "path" / "selector",
* e.g. "sys.my.detail" --> global['sys'] = { my: { detail: { } } }
* @param name
* @param define_opts
* @return engine&
*/
v8::Handle<v8::Object> implicit_define(std::string&& name, define_options define_opts)
{
v8::Local<v8::Context> cnt = v8::Local<v8::Context>::New(isolate(), context());
v8::Context::Scope context_scope(cnt);
v8::Local<v8::Object> h_next = cnt->Global();
std::string dispname = name;
if(name.empty()) return h_next;
if(name.front() == '.' || name.back() == '.'
|| name.find("..") != name.npos
|| std::count_if(name.begin(), name.end(), ::isspace) > 0) {
throw engine_error(std::string("Invalid object selector: '") + name + "'");
}
std::string head;
head.reserve(name.length());
for(auto p = name.find('.'); !name.empty(); p = name.find('.')) {
std::string chunk;
if(p != name.npos) {
chunk = name.substr(0, p);
name = name.substr(p+1);
} else {
chunk.swap(name);
}
if(!head.empty()) head += ".";
head += chunk;
v8::Local<v8::String> h_key = v8str(chunk);
if(chunk == "prototype") {
v8::Local<v8::Value> val = h_next->GetPrototype();
if(val.IsEmpty() || (!val->IsObject())) {
throw engine_error(std::string("v8::Object selector: '") + head
+ "' 'prototype' is not an object (but should be).");
}
h_next = v8::Local<v8::Object>::Cast<v8::Value>(val);
} else if(h_next->Has(h_key)) {
v8::Local<v8::Value> val = h_next->Get(h_key);
if(val.IsEmpty() || !val->IsObject()) {
throw engine_error(std::string("v8::Object selector: '") + head
+ "' already defined and not an object.");
}
h_next = v8::Local<v8::Object>::Cast<v8::Value>(val);
} else {
v8::Local<v8::Object> h_t_next = v8::Object::New(isolate());
h_next->ForceSet(
h_key,
h_t_next,
(v8::PropertyAttribute) (
((define_opts & defopt_overwritable) ? 0 : v8::ReadOnly) |
((define_opts & defopt_deletable) ? 0 : v8::DontDelete) |
((define_opts & defopt_enumerable) ? 0 : v8::DontEnum)
)
);
h_next = h_t_next;
if(h_next.IsEmpty() || !h_next->IsObject()) {
throw engine_error(std::string("Failed to define '" + dispname + "'"));
}
}
}
return h_next;
}
// </editor-fold>
// <editor-fold desc="(auxiliaries)" defaultstate="collapsed">
/**
* Finds a value by name ("path" / "selector"), returns an empty handle on fail.
* Note: Has to be in an already entered handle scope (before:
*
* if(!isolate()) throw engine_error("engine not initialized");
* v8::Locker lck(isolate());
* v8::Isolate::Scope isolate_scope(isolate());
* v8::HandleScope handle_scope(isolate());
*
* @param const std::string& name
* @return v8::Handle<typename ValueType>
*/
template <typename ValueType>
static v8::Handle<ValueType> find(std::string name)
{
std::vector<std::string> chunks = split_selector(std::string(name));
v8::Local<v8::Context> cnt = v8::Isolate::GetCurrent()->GetCurrentContext();
v8::Context::Scope context_scope(cnt);
if(chunks.empty()) return v8::Handle<ValueType>();
v8::Local<v8::Object> h_next = cnt->Global();
while(chunks.size() > 1) {
v8::Local<v8::String> h_key = v8str(chunks.back());
chunks.pop_back();
v8::Local<v8::Value> v = h_next->Get(h_key);
if(v.IsEmpty() || !v->IsObject()) return v8::Local<ValueType>();
h_next = v8::Local<v8::Object>::Cast<v8::Value>(v);
}
v8::Local<v8::String> h_key = v8str(chunks.back());
v8::Local<v8::Value> vv = h_next->Get(h_key);
v8::Local<ValueType> v = v8::Handle<ValueType>::Cast(vv);
if(v.IsEmpty()) {
return v8::Handle<ValueType>();
} else {
return v;
}
}
/**
* Splits a selector path like "a.b.c" into vector{"c", "b", "a"} (REVERSE, STACK-LIKE)
* @param std::string&& name
* @return std::vector<string>
*/
static std::vector<std::string> split_selector(std::string&& name)
{
std::vector<std::string> v;
std::string sel = name;
auto p = sel.rfind('.');
while(p != sel.npos) {
if(p >= sel.length()-1) { // "a.b..c.d" --> ".." could corrupt sel.substr(p+1)
throw engine_error(std::string("Invalid selector: '") + name + "'");
}
v.emplace_back(sel.substr(p+1));
sel.resize(p);
p = sel.find('.');
}
v.emplace_back(sel);
return v;
}
/**
* Redirects V8 calls to native functions
* @param const v8::FunctionCallbackInfo<v8::Value>& args
*/
static void function_proxy(const v8::FunctionCallbackInfo<v8::Value>& args)
{
if(args.Data().IsEmpty() || !args.Data()->IsUint32()) {
throw v8i::engine_error("function_proxy() arguments data is not an external value (BUG)");
}
engine* eng = nullptr;
{
v8::Isolate::Scope iscope(args.GetIsolate()); // stfwi: check if this can be omitted
v8::HandleScope handle_scope(args.GetIsolate());
v8::Local<v8::Context> ctx = args.GetIsolate()->GetCurrentContext();
if(!ctx.IsEmpty() && !ctx->Global().IsEmpty()) {
v8::Local<v8::Value> obj = ctx->Global()->GetHiddenValue(v8str("v8i::engine"));
if(!obj.IsEmpty() && obj->IsObject()) {
void* p = v8::Handle<v8::Object>::Cast(obj)->GetAlignedPointerFromInternalField(0);
if(p) {
eng = reinterpret_cast<engine*>(p);
}
}
}
if(!eng) {
v8i::js_exception::trigger("Native reference error");
return;
}
}
auto fid = args.Data()->Uint32Value();
assert(eng->defined_functions_.size() > fid);
auto fun = reinterpret_cast<callback_function_t>(eng->defined_functions_[fid]);
v8::TryCatch try_catch;
try {
v8i::function_callback_info fci(args);
fun(std::move(fci));
} catch(const v8i::not_convertible_error & e) {
v8i::js_exception::trigger(e.what());
} catch(const v8i::callback_error & e) {
v8i::js_exception::trigger(e.what());
} catch(const v8i::js_exception& e) {
if(try_catch.HasCaught()) {
try_catch.ReThrow();
} else {
v8i::js_exception::trigger(e.message());
}
}
}
/**
* Called when include() is invoked in the js runtime.
* @param v8i::function_callback_info&& cbi
* @return void
*/
static void include_callback(v8i::function_callback_info&& cbi)
{
std::string file = cbi.args.get<std::string, false>(0);
if(file.empty()) {
throw callback_error("include(): No file specified");
} else if(file.length() < 4 || file.find(".js") != file.length()-3) { // case sensitive?,,,
throw callback_error("include(): File is no '.js' file");
}
cbi.return_handle(cbi.engine().include_file(file, false, false));
}
/**
* Used to check compile_run() recursion. JS include calls will return references
* instead of "compile_run()'ing", and JS eval() is monitored by V8. C++ eval, e.g.
* in callbacks needs to be protected by this class.
*/
struct run_recursion_monitor
{
run_recursion_monitor(engine* eng) : eng_(eng)
{ ++(eng_->num_running_evals_); }
~run_recursion_monitor()
{ --(eng_->num_running_evals_); }
private:
engine* eng_;
};
// </editor-fold>
private:
// <editor-fold desc="instance variables" defaultstate="collapsed">
v8::Isolate* isolate_;
v8::Persistent<v8::Context> global_context_;
std::vector<void(*)()> defined_functions_;
std::string include_base_path_;
std::atomic<int> num_running_evals_;
std::timed_mutex interruption_mtx_;
// </editor-fold>
// <editor-fold desc="statics" defaultstate="collapsed">
static bool class_initialized_;
static int max_running_evals_;
// </editor-fold>
};
// <editor-fold desc="statics init" defaultstate="collapsed">
template <int N>
bool engine<N>::class_initialized_ = false;
template <int N>
int engine<N>::max_running_evals_ = 64;
// </editor-fold>
}
// </editor-fold>
}
#endif