-
Notifications
You must be signed in to change notification settings - Fork 438
Writing New Builtin Module
This document provides a guide on how to write a builtin module for IoT.js.
Contents:
- Writing Builtin JavaScript Module
- Writing Native Module Builtin
- Using native module in JavaScript module
- Registering native module
- Native handler
- Arguments and Return
- Wrapping native object with JS object
- Callback
You can see more information on the Optimization Tips page.
It will be easier to write a new IoT.js module if you have background on:
- Node.js module (for writing IoT.js JavaScript module)
- Node.js native addon (for writing IoT.js native module builtin)
Builtin JavaScript module can be written in the same way as writing Node.js module. JavaScript file should be located in src/js/
directory, and you should notify to our build script that your module should be included in one of following ways:
- Use
./tools/build.py --iotjs-include-module mymodule
when building - Add your module in
build.config
file
Your new module will look like below:
src/js/mymodule.js:
module.exports = {
foo: function() { console.log("OK"); },
bar: 123
}
user.js:
var mymodule = require('mymodule');
mymodule.foo(); // prints "OK"
console.log(mymodule.bar); // prints "123"
and execute:
$ ./tools/build.py
$ ${PATH_TO}/iotjs user.js
OK
123
You can implement some part of the builtin module in C, to enhance performance and to fully exploit the H/W functionality, etc. It has similar concept with Node.js native addon, but we have different set of APIs. Node.js uses its own binding layer with v8 API, but we use our own binding layer which wraps JerryScript API. You can see src/iotjs_binding.*
files to find more APIs to communicate with JS-side values from native-side.
For simple explanation, console
module will be used as an example.
Logging to console needs native functionality, so console
JavaScript module in src/js/console.js
passes its arguments into native handler like:
var consoleBuiltin = process.binding(process.binding.console);
...
Console.prototype.log = consoleBuiltin.stdout(util.format.apply(this, arguments) + '\n');
According to the code above, process.binding.console
should be defined before evaluating JavaScript code. IoT.js source code can automatically register native module if some functions are implemented as expected. First you should register your new module into MODULE_LIST
macro in src/iotjs_module.h
:
#define MAP_MODULE_LIST(F) \
E(F, BUFFER, Buffer, buffer) \
E(F, CONSOLE, Console, console) \
E(F, CONSTANTS, Constants, constants) \
...
Then iotjs_jval_t Init##ModuleName()
function will be called automatically when registering native module. We already have its implementation in src/module/iotjs_module_console.c
:
iotjs_jval_t InitConsole() {
iotjs_jval_t console = iotjs_jval_create_object();
iotjs_jval_set_method(&console, "stdout", Stdout);
iotjs_jval_set_method(&console, "stderr", Stderr);
return console;
}
The return value of initializer function (in this case, iotjs_jval_t console
,) will be passed to JS-side, as a return value of calling process.binding(process.binding.modulename)
. Calling iotjs_jval_create_object()
will create a JavaScript object in c code.
And you might want to define some functions and properties to the newly created object. iotjs_jval_set_method()
will register a native handler as a JavaScript function property. (That's how we was able to call consoleBuiltin.stdout()
in JavaScript.) And iotjs_jval_set_property_*()
will define a non-function property into object. You can find the example of registering a constant value as a JavaScript property in src/module/iotjs_module_constants.c
.
Native handler reads arguments from JavaScript, executes native operations, and returns the final value to JavaScript.
Let's see an example in src/module/iotjs_module_console.c
:
JHANDLER_FUNCTION(Stdout) {
JHANDLER_CHECK_ARGS(1, string);
iotjs_string_t msg = JHANDLER_GET_ARG(0, string);
fprintf(stdout, "%s", iotjs_string_data(&msg));
iotjs_string_destroy(&msg);
}
Using JHANDLER_GET_ARG(index, type)
macro inside JHANDLER_FUNCTION()
will read JS-side argument. Since JavaScript values can have dynamic types, you must check if argument has valid type with JHANDLER_CHECK_ARGS(number_of_arguments, type1, type2, type3, ...)
macro, which throws JavaScript TypeError when given condition is not satisfied.
Calling void iotjs_jhandler_return_*()
function inside JHANDLER_FUNCTION()
will return value into JS-side. undefined
will be returned if you didn't explicitly returned something, like normal JavaScript function does. Console methods doesn't have to return values, but you can easily find more examples from other modules.
console
module is state-free module, i.e., console module implementation doesn't have to hold any values with it. It just passes value and that's all it does.
However, there are many cases that module should maintain its state. Maintaining the state in JS-side would be simple. But maintaining values in native-side is not an easy problem, because native-side values should follow the lifecycle of JS-side values. Let's take Buffer
module as an example. Buffer
should maintain the native buffer content and its length. And the native buffer content should be deallocated when JS-side buffer variable becomes unreachable.
There's iotjs_jobjectwrap_t
struct for that purpose. if you create a new iotjs_jobjectwrap_t
struct with JavaScript object as its argument and free handler, the registered free handler will be automatically called when its corresponding JavaScript object becomes unreachable. Buffer
module also exploits this feature.
// This wrapper refer javascript object but never increase reference count
// If the object is freed by GC, then this wrapper instance will be also freed.
typedef struct {
iotjs_jval_t jobject;
} iotjs_jobjectwrap_t;
typedef struct {
iotjs_jobjectwrap_t jobjectwrap;
char* buffer;
size_t length;
} iotjs_bufferwrap_t;
static void iotjs_bufferwrap_destroy(iotjs_bufferwrap_t* bufferwrap);
IOTJS_DEFINE_NATIVE_HANDLE_INFO(bufferwrap);
iotjs_bufferwrap_t* iotjs_bufferwrap_create(const iotjs_jval_t* jbuiltin,
size_t length) {
iotjs_bufferwrap_t* bufferwrap = IOTJS_ALLOC(iotjs_bufferwrap_t);
iotjs_jobjectwrap_initialize(&_this->jobjectwrap,
jbuiltin,
&bufferwrap_native_info); /* Automatically called */
...
}
void iotjs_bufferwrap_destroy(iotjs_bufferwrap_t* bufferwrap) {
...
iotjs_jobjectwrap_destroy(&_this->jobjectwrap);
IOTJS_RELEASE(bufferwrap);
}
You can use this code like below:
const iotjs_jval_t* jbuiltin = /*...*/;
iotjs_bufferwrap_t* buffer_wrap = iotjs_bufferwrap_create(jbuiltin, length);
// Now `jbuiltin` object can be used in JS-side,
// and when it becomes unreachable, `iotjs_bufferwrap_destroy` will be called.
Sometimes native handler should call JavaScript function directly. For general function calls (inside current tick), you can use iotjs_jhelper_call()
function to call JavaScript function from native-side.
And for asynchronous callbacks, after libtuv
calls your native function, if you want to call JS-side callback you should use iotjs_make_callback()
. It will not only call the callback function, but also handle the exception, and process the next tick(i.e. it will call iotjs_process_next_tick()
).
For asynchronous callbacks, you must consider the lifetime of JS-side callback objects. The lifetime of JS-side callback object should be extended until the native-side callback is really called. You can use iotjs_reqwrap_t
and iotjs_handlewrap_t
to achieve this.
(Work In Progress)