A versatile script engine abstraction layer
ScriptX is a script engine abstraction layer. A variety of script engines are encapsulated on the bottom and a unified API is exposed on the top, so that the upper-layer caller can completely isolate the underlying engine implementation (back-end).
ScriptX not only isolates several JavaScript engines, but can even isolate different scripting languages, so that the upper layer can seamlessly switch between scripting engine and scripting language without changing the code.
In ScriptX terminology, "front-end" refers to the external C++ API, and "back-end" refers to different underlying engines. The currently implemented back-ends include: V8, node.js, JavaScriptCore, WebAssembly, Lua.
backend | language | version | states |
---|---|---|---|
V8 | JavaScript | 7.4+ | done |
JavaScriptCore | JavaScript | 7604.1.38.0.7+ (iOS 10+/macOS10.12+) |
done |
Node.js | JavaScript | 14.x+ | done |
QuickJs | JavaScript | 2021-03-27 | done |
WebAssembly | JavaScript | Emscripten-2.0.5+ | done |
Lua | Lua | 5.4+ | done |
CPython | Python | todo | |
YARV | Ruby | todo | |
Mono | C# | todo |
The interface of ScriptX uses modern C++ features. And to be 100% in line with the C++ standard, completely cross-platform.
All APIs are exposed in the ScriptX.h
aggregate header file.
Design goals: Multi-language | Multi-engine implementation | High performance | API easy to use | Cross-platform
We use a relatively complete code to leave an overall impression of ScriptX.
EngineScope enter(engine);
try {
engine->eval("function fibo(x) {if (x<=2) return 1; else return fibo(x-1) + fibo(x-2) }");
Local<Function> fibo = engine->get("fibo").asFunction();
Local<Value> ret = fibo.call({}, 10);
ret.asNumber().toInt32() == 55;
auto log = Function::newFunction(
[](const std::string& msg) {
std::cerr << "[log]: "<< msg << std::endl;
});
// or use: Function::newFunction(std::puts);
engine->set("log", log);
engine->eval("log('hello world');");
auto json = engine->eval(R"( JSON.parse('{"length":1,"info":{"version": "1.18","time":132}}'); )")
.asObject();
json.get("length").asNumber().toInt32() == 1;
auto info = json.get("info").asObject();
info.get("version").asString().toString() == "1.18";
info.get("time").asNumber().toInt32() == 132;
Local<Object> bind = engine->eval("...").asObject();
MyBind* ptr = engine->getNativeInstance<MyBind>(bind);
ptr->callCppFunction();
} catch (Exception& e) {
FAIL() << e.message() << e.stacktrace();
// or FAIL() << e;
}
- Use
EngineScope
to enter the engine environment - Most APIs can accept C++ native types as parameters and automatically convert types internally
- Script functions can be created directly from C/C++ functions (native binding)
- Support script exception handling
- API strong typing
At the beginning of the design of ScriptX, the goal was to support multiple scripting languages, and the engine package of V8 and JavaScriptCore was implemented on JavaScript. In order to verify the multi-language design of ScriptX, a complete Lua binding was implemented. Currently support for WebAssembly has also been completed.
API design conforms to modern C++ style, such as:
- Three reference types Local/Global/Weak, using copy and move semantics to achieve automatic memory management (automatic reference counting)
- Use variadic template to support the very convenient Function::call syntax
- Use Template Meta-Programing to directly bind C++ functions
Modern language features, refer to null pointer safety (nullability safety please refer to the concept of kotlin).
Note: ScriptX requires C++17 (or 1z) or higher compiler support, and needs to turn on the exception feature (you can turn off the RTTI feature).
High performance is an important indicator in the design of ScriptX. The C++ idea of Zero-Overhead is also fully embodied in the implementation process. And pass relevant performance tests when adding functional features.
Test indicator: single time consuming from JS to C++ function call, microsecond
Test environment: iMac i9-9900k 32G RAM @macOS 10.15
Test Index: single function call from js to C++ in micro seconds. Test Environment: iMac i9-9900k 32G RAM @macOS 10.15
The performance test shows that in Release mode, ScriptX can achieve almost the same performance as the native binding. (Because ScriptX uses a large number of templates, please do not perform performance testing in the Debug version)
ScriptX has realized the ability of script exceptions and C++ exceptions to communicate with each other through a series of technical means. There is no need to judge the return value when calling the engine API, and unified exception handling can be used to avoid crashes.
For example, users can catch exceptions thrown by js code in the C++ layer, and get the message and stack; they can also throw a C++ exception (script::Exception
) in the native function and pass it through to the js code.
For details, please refer to ExceptionTest and Related Documents
Easy-to-use API => Happy Engineer => Efficient => High Quality
ScriptX was designed with full consideration of the ease of use of the API, including friendly and simple operation, not easy to make mistakes, obvious error messages, and easy to locate problems. Under this guiding ideology, ScriptX has done a lot of things that native engines can't do.
For example: V8 does not perform GC when destroying, resulting in many bound native classes that cannot be released. ScriptX does additional logic to handle this situation.
V8 and JSCore require that other APIs of ScriptX cannot be called in the finalize callback, otherwise it will crash, which also makes the code logic difficult to implement. ScriptX uses MessageQueue to avoid this problem perfectly.
The global references of V8 and JSCore must be released before the engine is destroyed, otherwise it will cause problems such as crash and failure to destroy. ScriptX guarantees to actively reset all Global / Weak references during Engine destruction.
When an app is used as a host to use a scripting engine, it is usually necessary to inject a large number of native-bound functions/classes to provide capabilities for the scripting logic. The ClassDeifine
related binding API designed by ScriptX is simple and easy to use, and can support direct binding of C++ functions, which greatly improves work efficiency.
While ScriptX provides engine encapsulation, it also provides a set of tools and methods to achieve mutual conversion between native types and ScriptX types.
For details, please refer to InteroperateTest and Related Documents
High code quality requirements
- Hundreds of test cases, UnitTests coverage rate reaches 90+%
- The cyclomatic complexity is only 1.18.
- Use clang-format to ensure uniform code format.
- Use clang-tidy to find potential problems.
- Both clang and MSVC compilers have opened "warning as error" level error messages.
root
├── README.md
├── src
│ ├── Engine.h
│ └── ...
├── backend
│ ├── JavaScriptCore
│ ├── Lua
│ ├── Python
│ ├── QuickJs
│ ├── Ruby
│ ├── SpiderMonkey
│ ├── Template
│ ├── V8
│ ├── WKWebView
│ └── WebAssembly
├── docs
│ ├── Basics.md
│ └── ...
└── test
├── CMakeLists.txt
└── src
├── Demo.cc
└── ...
src
: External API, mainly header filesbackend
: Implementation of various engine backendsdocs
: Rich documentationtest
: Various unit tests
Some important classes in ScriptX:
ScriptEngine
EngineScope
Exception
Value
,Null
,Object
,String
,Number
,Boolean
,Function
,Array
,ByteBuffer
,Unsupported
Local<Value>
,Local<Null>
,Local<Object>
,Local<String>
,Local<Number>
,Local<Boolean>
,Local<Function>
,Local<Array>
,Local<ByteBuffer>
,Local<Unsupported>
Global<T>
,Weak<T>
Before officially using ScriptX, please spend half an hour read carefully the project documents, and be familiar with several concepts in ScriptX.