-
Notifications
You must be signed in to change notification settings - Fork 438
Writing New Module
This document provides a guide on how to write a module for IoT.js.
Contents
- Writing JavaScript Module
- Writing Native Module
- Platform dependent native parts
- Native handler
- Arguments and Return
- Wrapping native object with JS object
- Callback
- Writing "Mixed" Module
- Using native module in JavaScript module
- Advanced usage
- Module specific CMake file
- Writing Dynamically loadable modules
- Module structure generator
See also:
JavaScript module can be written in the same way as writing Node.js module. JavaScript file should be located anywhere on your filesystem.
- Use
./tools/build.py --external-modules=my-module
when building - Enable your module in a profile or as an additional CMake parameter
Important: the name of the module must be in lowercase. It is not allowed to use uppercase characters.
Your new module will look like below:
my-module/js/mymodule.js:
module.exports = {
foo: function() { console.log("OK"); },
bar: 123
}
my-module/modules.json:
{
"modules": {
"mymodule": {
"js_file": "js/mymodule.js",
"require": ["buffer", "console"]
}
}
}
user.js:
var mymodule = require('mymodule');
mymodule.foo(); // prints "OK"
console.log(mymodule.bar); // prints "123"
and execute:
$ ./tools/build.py --external-modules=./my-module --cmake-param=-DENABLE_MODULE_MYMODULE=ON
$ ${PATH_TO}/iotjs user.js
OK
123
Note: --cmake-param=-DENABLE_MODULE_MYMODULE=ON
option must be used in case of an
external module, because the default profile enables only the basic and core modules.
An uppercase ENABLE_MODULE_[NAME]
CMake variable will be generated for every module,
where NAME
is the name of the module in the modules.json
. To enable or disable a
module by setting the corresponding ENABLE_MODULE_[NAME]
to ON or OFF. It will override
the defult settings of the profile.
The purpose of the "profile" is to describe the default settings of enabled modules for
the build. A profile file is a list of ENABLE_MODULE_[NAME]
macros. Those module whos
ENABLE_MODULE_[NAME]
macro is not listed will be disabled by defult.
my-module/mymodule.profile:
ENABLE_MODULE_IOTJS_BASIC_MODULES
ENABLE_MODULE_IOTJS_CORE_MODULES
ENABLE_MODULE_MYMODULE
Execute:
./tools/build.py --external-modules=./my-module --profile=my-module/mymodule.profile
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 of you can call JerryScript API functions directly.
- For native modules you must define an
init
function that provides the JS object that represents your module. - You can define multiple native files.
- Directory of your module will be added to the include path.
- Use
./tools/build.py --external-modules=my-module
when building. - Enable your module in a profile or as an additional CMake parameter.
Your new module will look like below:
my-module/my_module.c:
#include "iotjs_def.h"
jerry_value_t InitMyNativeModule() {
jerry_value_t mymodule = jerry_create_object();
iotjs_jval_set_property_string_raw(mymodule, "message", "Hello world!");
return mymodule;
}
my-module/modules.json:
{
"modules": {
"mymodule": {
"native_files": ["my_module.c"],
"init": "InitMyNativeModule"
}
}
}
user.js:
var mymodule = require('mymodule');
console.log(mymodule.message); // prints "Hello world!"
and execute:
$ ./tools/build.py --external-modules=./my-module --cmake-param=-DENABLE_MODULE_MYMODULE=ON
$ ${PATH_TO}/iotjs user.js
Hello world!
You can define the platform dependent low level parts in the modules.json
.
Structure of the directory of the custom module:
my_module
|-- linux
|-- my_module_platform_impl.c
|-- nuttx
|-- my_module_platform_impl.c
|-- tizenrt
|-- my_module_platform_impl.c
|-- other
|-- my_module_platform_impl.c
|-- modules.json
|-- my_module.h
|-- my_module.c
modules.json:
{
"modules": {
"mymodule": {
"platforms": {
"linux": {
"native_files": ["linux/my_module_platform_impl.c"]
},
"nuttx": {
"native_files": ["nuttx/my_module_platform_impl.c"]
},
"tizenrt": {
"native_files": ["tizenrt/my_module_platform_impl.c"]
},
"undefined": {
"native_files": ["other/my_module_platform_impl.c"]
}
},
"native_files": ["my_module.c"],
"init": "InitMyModule"
}
}
}
Note: Undefined platform means a general implementation. If the module does not support your platform then it will use the undefined
platform implementation.
It is possible that the external module depends/requires an already compiled third-party shared object or static library.
Such libraries can be specified in the modules.json
file so they will be linked when the IoT.js module is used.
To specify third-party libraries the external_libs
key should be used in the module specification.
For example in the modules.json
:
{
"modules": {
"mymodule": {
"platforms": {
"linux": {
"native_files": ["linux/my_module_platform_impl.c"],
"external_libs": ["curl"]
}
},
"native_files": ["my_module.c"],
"external_libs": ["lib_shared_on_all_platforms_if_it_truly_exists"],
"init": "InitMyNativeModule"
}
}
}
The external_libs
key can be specified on the module level or for each platform also.
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
:
JS_FUNCTION(Stdout) {
DJS_CHECK_ARGS(1, string);
iotjs_string_t msg = JS_GET_ARG(0, string);
fprintf(stdout, "%s", iotjs_string_data(&msg));
iotjs_string_destroy(&msg);
return jerry_create_undefined();
}
Using JS_GET_ARG(index, type)
macro inside JS_FUNCTION()
will read JS-side argument. Since JavaScript values can have dynamic types, you must check if argument has valid type with DJS_CHECK_ARGS(number_of_arguments, type1, type2, type3, ...)
macro, which throws JavaScript TypeError when given condition is not satisfied.
JS_FUNCTION()
must return with an jerry_value_t
into JS-side.
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 {
jerry_value_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 jerry_value_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 jerry_value_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.
Modules could be a combination of JS and native code. In that case the Javascript file must export the objects of the module. In such cases the native part will be hidden.
For simple explanation, console
module will be used as an example.
src
|-- js
|-- console.js
|-- modules
|-- iotjs_module_console.c
|-- modules.json
modules.json
{
"modules": {
...
"console": {
"native_files": ["modules/iotjs_module_console.c"],
"init": "InitConsole",
"js_file": "js/console.js",
"require": ["util"]
},
...
}
}
Logging to console needs native functionality, so console
JavaScript module in src/js/console.js
passes its arguments into native handler like:
Console.prototype.log = native.stdout(util.format.apply(this, arguments) + '\n');
Where native
is the JS object returned by the native InitConsole
function in iotjs_module_console.c
.
Note: native
is undefined if there is no native part of the module.
Bridge module provides two interfaces for sending synchronous and asynchronous message from Javascript to the native module. The Native module simply rersponds back to the requst using a simple inteface that can create return message. Of course you can use the IoT.js and JerryScript APIs to respond directly to the request of JavaScript module, but sometimes using a simpliffied method is more efficient in providing simple functionality in a short time.
For example, JavaScript module can request resource path synchronously, and native module can simply return a resource path by just calling a function.
in the bridge_sample.js of bridge_sample module
bridge_sample.prototype.getResPath = function(){
return this.bridge.sendSync("getResPath", "");
};
in the iotjs_bridge_sample.c of bridge_sample module
if (strncmp(command, "getResPath", strlen("getResPath")) == 0) {
iotjs_bridge_set_return(return_message, "res/");
return 0;
}
For the complete sample code, please see the bridge_sample in samples/bridge_sample folder.
For each module, it is possible to define one extra cmake file.
This can be done by specifying the cmakefile
key file for
a module in the related modules.json
file.
For example:
{
"modules": {
"demomod": {
...
"cmakefile": "module.cmake"
}
}
}
This module.cmake
is a module-specific CMake file
which will be searchd for in the module's base directory.
In this file it is possible to specify additonal dependecies,
build targets, and other things.
However, there are a few important rules which must be followed in the CMake file:
-
The
MODULE_DIR
andMODULE_BINARY_DIR
will be set by the IoT.js build system. Do NOT overwrite them in the CMake file! -
The
MODULE_NAME
CMake variable must be set. Example:set(MODULE_NAME "demomod")
-
The
add_subdirectory
call must specify the output binary dir. Please use this template:add_subdirectory(${MODULE_DIR}/lib/ ${MODULE_BASE_BINARY_DIR}/${MODULE_NAME})
wherelib/
is a subdirectory of the module directory. -
If there is an extra library which should be used during linking, the following template should be used:
list(APPEND MODULE_LIBS demo)
wheredemo
is the extra module which must be linked. Any number of modules can be appended to theMODULE_LIBS
list variable. -
The source files which are specified in the
modules.json
file must NOT be specified in the CMake file.
An example module CMake file:
set(MODULE_NAME "mymodule")
add_subdirectory(${MODULE_DIR}/myLib/ ${MODULE_BASE_BINARY_DIR}/${MODULE_NAME})
list(APPEND MODULE_LIBS myLib)
To ease creation of modules which contains extra CMake files there is a module generator as described below.
IoT.js support loading specially crafted shared libraries.
To create such modules the source files must be compiled into a
shared object - preferably using the .iotjs
extension - and
must have a special entry point defined using the IOTJS_MODULE
macro.
The IOTJS_MODULE
macro has the following four parameters:
-
iotjs_module_version
: target IoT.js target module version asuint32_t
value. TheIOTJS_CURRENT_MODULE_VERSION
can be used to get the current IoT.js module version when the module is compiling. -
module_version
: the module version as auint32_t
value. -
initializer
: the entry point of module which should return an initialized module object. Note: the macro will automaticall prefix the specified name withinit_
.
For example, in the testmodule.c
file:
#include <iotjs_binding.h>
jerry_value_t init_testmodule(void) {
jerry_value_t object = jerry_create_object();
/* add properties to 'object' */
return object;
}
IOTJS_MODULE(IOTJS_CURRENT_MODULE_VERSION, 1, testmodule);
Should be compiled with the following command:
$ gcc -Wall -I<path/to/IoT.js headers> -I<path/to/JerryScript headers> -shared -o testmodule.iotjs testmodule.c
After the shared module is created it can be loaded via require
call.
Example JS code:
var test = require('testmodule.iotjs');
/* access the properties of the test module. */
During the dynamic module loading if the iotsj_module_version
returned by the module does not match the IOTJS_CURRENT_MODULE_VERSION
value of the running IoT.js instance, the module loading will fail.
Please note that the dynamically loadable module differs from modules mentioned before in the following points:
- The entry point must be specified with the
IOTJS_MODULE
macro. - The shared module is not compiled with the IoT.js binary.
- There is no need for the
modules.json
file.
As previously shown, there are a few files required to create a module.
These files can be createad manually or by the tools/iotjs-create-module.py
script.
The module generator can generate two types of modules:
- basic built-in module which is compiled into the IoT.js binary.
- shared module which can be dynamically loaded via the
require
call.
To generate a module with the IoT.js module generator the module template should be specified and the name of the new module.
Important note: The module name must be in lowercase.
The template
paramter for the module creator is optional, if it is
not specified basic modules are created.
The generated module(s) have simple examples in it which can be used to bootstrap ones own module(s). On how to use them please see the previous parts of this document.
Example basic module generation:
$ python ./iotjs/tools/iotjs-create-module.py --template basic demomod
Creating module in ./demomod
loading template file: ./iotjs/tools/module_template/module.cmake
loading template file: ./iotjs/tools/module_template/modules.json
loading template file: ./iotjs/tools/module_template/js/module.js
loading template file: ./iotjs/tools/module_template/src/module.c
Module created in: /mnt/work/demomod
By default the following structure will be created by the tool:
demomod/
|-- js
|-- module.js
|-- module.cmake
|-- modules.json
|-- src
|-- module.c
Example shared module generation:
$ python ./iotjs/tools/iotjs-create-module.py --template shared demomod
Creating module in ./demomod
loading template file: ./iotjs/tools/module_templates/shared_module_template/CMakeLists.txt
loading template file: ./iotjs/tools/module_templates/shared_module_template/README
loading template file: ./iotjs/tools/module_templates/shared_module_template/js/test.js
loading template file: ./iotjs/tools/module_templates/shared_module_template/src/module_entry.c
Module created in: /mnt/work/demomod
The generated demomod
will have a CMakeLists.txt
file which contains
path variables to the IoT.js headers and JerryScript headers. These path
variables are absolute paths and required to the module compilation.
Please adapt the paths if required.
Additionnally the README
file contains basic instructions on
how to build and test the new module.