Skip to content

Writing New Builtin Module

Hosung Kim edited this page Jul 3, 2017 · 5 revisions

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:

Writing Builtin JavaScript Module

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

Writing Native Module Builtin

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.

Using native module in JavaScript module

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');

Registering native module

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

Native handler reads arguments from JavaScript, executes native operations, and returns the final value to JavaScript.

Arguments and Return

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.

Wrapping native object with JS object

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.

Callback

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)

Clone this wiki locally