Skip to content

Node Native Modules

Kevin Ushey edited this page Feb 22, 2022 · 4 revisions

Note: The following information is currently only relevant to the Electron builds of RStudio, which is currently a work in progress.

The desktop edition of RStudio uses Electron, which allows for native code extensions via native modules. This document briefly outlines the approach used to implement new native module methods.

Roadmap

These are (roughly speaking) the following steps to take when adding or extending a native node module to be used with RStudio:

  • Add your methods in src/native/desktop.cc or another similar appropriate file,
  • If you added more C++ sources, make sure to update the "sources" field in binding.gyp,
  • Update the appropriate .d.ts files in src/native,
  • Run npm install to rebuild the module,
  • Use it in Typescript like any other module.

The rest of the document describes these steps in more detail.

Adding a Method

Our native modules are implemented using C++. The sources for our native module(s) live within the src/native folder. The general structure is something like the following:

// actual method implementation
bool methodImpl(int a) { ... }

// a wrapper method for node-addon-api, which unpacks arguments
// and calls the underlying impl method
Napi::Value method(const Napi::CallbackInfo& info) {

	// retrieve evaluation environment
	auto env = info.Env();

	// unpack arguments
	auto a = env[0].As<Napi::Integer>().Value();

	// call implementation method
	auto result = methodImpl(a);

	// return wrapped value
	return Napi::Value::From(env, result);

}

// the entrypoint for the node module
// its primary responsibility is exporting the relevant methods,
// so they're visible from the imported node module
Napi::Object Init(Napi::Env env, Napi::Object exports) {

	exports.set(
		Napi::String::New(env, "method"),
		Napi::Function::New(env, method)
	);

	return exports;

}

If multiple C++ source files are used for compilation of a node module, then only one source file should define the Init() method.

Compiling with node-gyp

Node modules are automatically compiled when npm install is invoked. Compilation is performed by node-gyp, and the targets for their compilation is defined in binding.gyp. Think of this file as a Makefile for node add-ons -- it can be used to define targets, which is used to control how node native modules are built and bundled with the application.

The document's contents (at the time this article was written) are:

{
    "targets": [
        {
            "target_name": "desktop",
            "cflags!": [ "-fno-exceptions" ],
            "cflags_cc!": [ "-fno-exceptions" ],
            "sources": [ "src/native/desktop.cc" ],
            "include_dirs": [
                "<!@(node -p \"require('node-addon-api').include\")"
            ],
            "defines": [ "NAPI_DISABLE_CPP_EXCEPTIONS" ]
        },
        {
            "target_name": "copy",
            "type": "none",
            "dependencies": [ "desktop" ],
            "copies": [
                {
                    "files": ["<(module_root_dir)/build/Release/desktop.node"],
                    "destination": "<(module_root_dir)/src/native"
                }
            ]

        }
    ]
}

There are two targets defined:

  • The first is used to actually build the node module. In particular, note that it builds from the source file "src/native/desktop.cc".

  • The second is used to copy the generated node module from the build tree back into the source tree, mainly to make it easier to import and use in our .ts source scripts.

When npm install is invoked, node-gyp is also executed and used to compile the above targets.

See the official documentation at https://gyp.gsrc.io/docs/UserDocumentation.md for more information.

Using a Native Node Module

From the Typescript side, modules can be imported like "regular" Typescript modules. For example:

import desktop from '../native/desktop.node'

An accompanying desktop.node.d.ts file is used to define the interface actually available for interacting with the module. This file needs to be manually updated and kept in sync with the underlying native module as it changes. For example, the above method would be "documented" as:

export declare function method(a: int): bool;

Epilogue

Finally, see https://github.com/nodejs/node-addon-examples if you need inspiration or more examples on how node-addon-api can be used. Note that each folder provides examples for multiple different node interfaces; we're mainly interested in the node-addon-api versions.