NJS API
Native JavaScript API for Bindings.
Introduction
NJS-API is a header-only C++ library that abstracts APIs of JavaScript virtual machines (VMs) into a single API that can be used by embedders to write bindings compatible with multiple JS engines. The motivation to start the project were breaking changes of V8 engine and the way V8 bindings are written (too verbose and too annoying to handle all possible error conditions). In addition, attempt to port node.js to use Chakra javascript engine instead of V8 contributed to the idea of creating a single API that can target multiple JS engines, not just V8.
Features
- Source-level abstraction layer on top of native VM APIs - JavaScript engine cannot be replaced at runtime, recompilation is needed as NJS wraps VM dependent data structures and classes
- High-performance interface that maps to native VMs as closely as hand-written bindings - the compiled code should be very close to a code you would have normally written for a particular VM
- High-level interface to decrease the amount of code you need to write and to decrease the size of the resulting compiled code - no more boilerplate for each function you want to bind
- Security is part of the contract - JavaScript code is not allowed to corrupt internals that the C++ side expects (function signatures, ...)
- Comfortable and meaningful error handling - let users of your bindings know where the problem happened. NJS contains many helpers to make error handling less verbose yet still powerful on C++ side (it turns the most annoying type checks to one-liners)
- Foundation to build extensions that can be used to implement concepts - data serialization and deserialization of custom user types
Disclaimer
NJS is a WORK-IN-PROGRESS that has been published to validate the initial ideas and to design the API with more people. The library started as a separate project when developing blend2d-js to make bindings easier to write, easier to maintain, and also more secure.
Base API
Classes provided by NJS-API:
njs::Result
- 32-bit unsigned integer representing result-codenjs::Utf8Ref
- Tagged reference to UTF-8 stringnjs::Utf16Ref
- Tagged reference to UTF-16 stringnjs::Latin1Ref
- Tagged reference to LATIN1 stringnjs::Range<T>
- Minimum and maximum value, that can be used to restrict JS to C++ value conversionnjs::Value
- Local javascript value (wraps v8::Local<v8::Value>
)njs::Persistent
- A persistent javascript value (wraps v8::Persistent<v8::Value>
)njs::HandleScope
- Handle scope, currently maps to v8::HandleScope
njs::Runtime
- Runtime is mapped to the underlying VM runtime / heapnjs::Context
- Javascript context
njs::ScopedContext
- Composition of Context
and HandleScope
njs::ExecutionContext
- Javascript context constructed during executionnjs::FunctionCallContext
- Function call contextnjs::ConstructCallContext
- Constructor call contextnjs::GetPropertyContext
- Property getter contextnjs::SetPropertyContext
- Property setter context
Namespaces provided by NJS-API:
njs::Globals
- Provides constants and limitsnjs::Internal
- Private namespace used by the implementation
Minimum Example
struct Point {
double x, y;
bool operator==(const Point& other) const noexcept { return x == other.x && y == other.y; }
bool operator!=(const Point& other) const noexcept { return x == other.x && y == other.y; }
Point& translate(double tx, double ty) noexcept {
x += tx;
y += ty;
return *this;
}
static Point vectorOf(const Point& a, const Point& b) noexcept {
return Point { b.x - a.x, b.y - a.y };
}
};
class PointWrap {
public:
NJS_BASE_CLASS(PointWrap, "Point", 0x01)
Point data;
inline PointWrap() noexcept
: data {} {}
inline PointWrap(double x, double y) noexcept
: data { x, y} {}
};
NJS_BIND_CLASS(PointWrap) {
NJS_BIND_CONSTURCTOR() {
unsigned int argc = ctx.argumentsLength();
if (argc == 0) {
return ctx.returnNew<PointWrap>();
}
if (argc == 2) {
double x, y;
NJS_CHECK(ctx.unpackArgument(0, x);
NJS_CHECK(ctx.unpackArgument(1, y);
return ctx.returnNew<PointWrap>(x, y);
}
return ctx.invalidArgumentsLength();
}
NJS_BIND_GET(x) {
return ctx.returnValue(self->data.x);
}
NJS_BIND_SET(x) {
double x;
NJS_CHECK(ctx.unpackValue(x));
self->data.x = x;
return njs::Globals::kResultOk;
}
NJS_BIND_GET(y) {
return ctx.returnValue(self->data.y);
}
NJS_BIND_SET(y) {
double y;
NJS_CHECK(ctx.unpackValue(y));
self->data.y = y;
return njs::Globals::kResultOk;
}
NJS_BIND_METHOD(translate) {
double tx, ty;
NJS_CHECK(ctx.verifyArgumentsLength(2));
NJS_CHECK(ctx.unpackArgument(0, tx));
NJS_CHECK(ctx.unpackArgument(1, ty));
self->data.translate(tx, ty);
return ctx.returnValue(ctx.This());
}
NJS_BIND_METHOD(equals) {
PointWrap* other;
NJS_CHECK(ctx.verifyArgumentsLength(1));
NJS_CHECK(ctx.unwrapArgument<PointWrap>(0, &other));
return ctx.returnValue(self->data == other->data);
}
NJS_BIND_STATIC(vectorOf) {
PointWrap* a;
PointWrap* b;
NJS_CHECK(ctx.verifyArgumentsLength(2));
NJS_CHECK(ctx.unwrapArgument<PointWrap>(0, &a));
NJS_CHECK(ctx.unwrapArgument<PointWrap>(1, &b));
njs::Value PointClass = ctx.propertyOf(ctx.data(), njs::Latin1Ref("Point"));
NJS_CHECK(PointClass);
njs::Value instance = ctx.newInstance(PointClass);
NJS_CHECK(instance);
PointWrap* instObj = ctx.unwrapUnsafe<PointWrap>(instance);
instObj->data = Point::vectorOf(a->data, b->data);
return instance;
}
};
NJS_MODULE(mylib) {
NJS_INIT_CLASS(PointWrap, exports);
}
TODO
Write more documentation and examples.