Hi all,
I'm Jinho Bang and a contributor in this project.
Today, I'd like to suggest a new feature a.k.a WebIDL Binding Generator. If we use it, we can just generate N-API binding code automatically. I've been implementing POC demo with @yjaeseok and @nadongguri for last few weeks.
Let's look deep into WebIDL Binding Generator.
To help you better understand, let's see the following example. I did copy N paste an example from abi-stable-node-addon-api examples repo. As you already know, if we use it, we can invoke native add() function in JS side.
add() function#include <napi.h>
Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2) {
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return env.Null();
}
if (!info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
double arg0 = info[0].As<Napi::Number>().DoubleValue();
double arg1 = info[1].As<Napi::Number>().DoubleValue();
Napi::Number num = Napi::Number::New(env, arg0 + arg1);
return num;
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set(Napi::String::New(env, "add"),
Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(addon, Init)
The above example is not difficult but it requires developer write more code than they think. If we could simplify the above example as follows, we will reduce the developer's plumbing.
double add(double x, double y) {
return x + y;
}
Isn't this just native code? Yes! So, how can Javascript call the native code? The key solution is WebIDL. We can make Javascript call the native code internally just by writing WebIDL like this:
interface Calculator {
double add(double x, double y);
};
The WebIDL Binding Generator is based on WebIDL to generate a binding code automatically.
WebIDL Binding Generator is typically used in the Chromium project. Chromium uses the Blink engine and the Javascript V8 engine. They integrate these two engines, by WebIDL Binding Generator. This can avoid issues such as Type Checking, Type Converting, and Manage Isolate & Context in the Binding process and increase productivity. You can find out about using Chromium's WebIDL through the link below.
https://www.chromium.org/blink/webidl
It has the following advantages.
The binding code and the implementation of native code are separated so we can keep the code simple. Here's the example of comparing the code complexity when using WebIDL Binding Generator.

As mentioned earlier, we don't have to write N-API binding code so it means developers don't need to check issues such as Type checking, type converting and etc. For example, Javascript doesn't support the method overloading so developers have to check the types of arguments, the number of arguments and etc when using N-API.
We showed Example: add() function in the Introductions section. Let's say there are new two Add methods; one has 3 arguments and the other one has one Object type argument. It can be implemented as following(You don't have to understand the following code fully).
#include <napi.h>
Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() == 1) {
if(!info[0].IsObject()){
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return env.Null();
}
Napi::Value value1 = info[0].As<Napi::Object>().Get("value1");
Napi::Value value2 = info[0].As<Napi::Object>().Get("value2");
if (!value1.IsNumber() || !value2.IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
double arg0 = value1.As<Napi::Number>().DoubleValue();
double arg1 = value2.As<Napi::Number>().DoubleValue();
Napi::Number num = Napi::Number::New(env, arg0 + arg1);
return num;
} else if (info.Length() == 2) {
if (!info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
double arg0 = info[0].As<Napi::Number>().DoubleValue();
double arg1 = info[1].As<Napi::Number>().DoubleValue();
Napi::Number num = Napi::Number::New(env, arg0 + arg1);
return num;
} else if(info.Length() == 3) {
if (!info[0].IsNumber() || !info[1].IsNumber() || !info[2].IsNumber()) {
Napi::TypeError::New(env, "Wrong arguments").ThrowAsJavaScriptException();
return env.Null();
}
double arg0 = info[0].As<Napi::Number>().DoubleValue();
double arg1 = info[1].As<Napi::Number>().DoubleValue();
double arg2 = info[2].As<Napi::Number>().DoubleValue();
Napi::Number num = Napi::Number::New(env, arg0 + arg1 + arg2);
return num;
} else {
Napi::TypeError::New(env, "Wrong number of arguments").ThrowAsJavaScriptException();
return env.Null();
}
}
This code could be optimized but the point is that in order to support Method overloading, code would be complicated and developers should consider argument checking, type checking and etc.
By using the WebIDL binding generator, the code will be implemented as follows.
// Add.nidl
interface Calculator {
double add(double a, double b, optional double c = 0);
double add(TwoNumbers n);
}
dictionary TwoNumbers {
double value1;
double value2;
}
// Add.cc
double Add(double a, double b, double c = 0) {
return a + b + c;
}
double Add(TwoNumbers a) {
return a.value1() + a.value2();
}
Developers no longer need to write N-API binding code so they don't have to learn N-API.
※ IDL is very straightforward and easy to learn. It's like defining C++ headers.
You can test it here.
Our WebIDL Binding Generator works as follows:
binding.gypbinding.gyp as follows, WebIDL generator is initiated from node-gyp..nidl files from $PWD.nidl files by using WebIDL parser.nidl files. We are internally using WebIDL parser that W3C provides to. After this step, we can get AST(Abstract Syntax Tree) for .nidl files.@romandev this is great and along the lines of how I envisioned N-API would evolve. One question- can WebIDL generate other targets too other than N-API. Also, does WebIDL support describing cases where the code is not available (for example, if you have an existing shared library, is WebIDL expressive enough to state that the implementation of the function is in that library?)
But I agree, this makes sense to have as an affordance for N-API developers since it simplifies the amount of boiler plate needed considerably. @nodejs/addon-api any thoughts on whether this should be a part of node-addon-api or a separate project?
I'll add my 2 cents that I think this looks interesting as well. More tooling on top of N-API and node-addon-api to simplify native development is a great thing.
One of the key questions will be if it should be part of node-addon-api or a module which depends on it. The right answer might also change over time :)
@romandev this is great and along the lines of how I envisioned N-API would evolve. One question- can WebIDL generate other targets too other than N-API. Also, does WebIDL support describing cases where the code is not available (for example, if you have an existing shared library, is WebIDL expressive enough to state that the implementation of the function is in that library?)
Thank you for comment.
I think we can cover the shared library case using extended attribute.
For example,
interface Hello {
[Implementation="hello_world.so"] void world();
};
I'm not sure how we can implement it right now but we can consider it enough.
But I agree, this makes sense to have as an affordance for N-API developers since it simplifies the amount of boiler plate needed considerably. @nodejs/addon-api any thoughts on whether this should be a part of node-addon-api or a separate project?
In my humble opinion, this WebIDL generator feature should be implemented in node-addon-api repo for the following reasons:
node-addon-api. So, when any API face is changed in node-addon-api, the WebIDL generator should also be changed for adapting the API face change. But if there is a breaking change in upstream(node-addon-api), it's not easy to follow the change at the same time in downstream(WebIDL generator).Makes users add a new repo or package
Makes project promotion difficult
node-addon-api and webidl-node-addon-api. Their goals are the same. Then, which project should we promote to our users?A similar case: Emscripten project
Has any progress on this been made? I'm interested in using IDL to generate generic V8 bindings for non-node projects. For my particular use case I won't have NAPI or NAN since they are tightly coupled to node, but node support of IDL is interesting nonetheless.
@romandev I'm going to close this out since there has not been progress for quite some time. Please let us know if that is not the right thing to do.
Most helpful comment
@romandev this is great and along the lines of how I envisioned N-API would evolve. One question- can WebIDL generate other targets too other than N-API. Also, does WebIDL support describing cases where the code is not available (for example, if you have an existing shared library, is WebIDL expressive enough to state that the implementation of the function is in that library?)
But I agree, this makes sense to have as an affordance for N-API developers since it simplifies the amount of boiler plate needed considerably. @nodejs/addon-api any thoughts on whether this should be a part of node-addon-api or a separate project?