Native C++ Module
Learn how to add a native C++ module to your application to access platform-specific APIs and maximize performance for computationally intensive tasks.
Overview
The native C++ module is a dynamically linked shared object that can be loaded in the main process and used for direct access to C++ libraries and platform-specific APIs that are not available through Node.js.
Use a native module when you need:
- Performance-critical operations. Computationally intensive tasks such as image processing, video encoding, cryptography, or complex mathematical calculations that benefit from C++ performance.
- Platform-specific APIs. Direct access to operating system APIs that are not exposed through Node.js and MōBrowser API.
- Third-party C++ libraries. Integration with existing C++ libraries.
- Hardware access. Low-level access to hardware devices or peripherals.
Most applications do not need a native module. MōBrowser API and built-in Node.js APIs cover the majority of use cases.
Prerequisites
To compile the native C++ module, you need to install C++ compiler in your environment.
- Windows 10 (64-bit) or later.
- Install Microsoft C++ Build Tools. Make sure you select the Desktop development with C++ workload during installation.
- macOS 14 (Apple Silicon) or later.
- Install Command Line Tools using
xcode-select --install.
- Ubuntu 22.04 (64-bit) or later.
- Install GCC that supports C++20 or later:
sudo apt install build-essential
Creating a project with a native module
To create a project that includes a native module, run the scaffolding tool and select the option to include the native module when prompted:
npm create mobrowser-app@latest
When asked “Do you want to add a native C++ module?”, select “Yes”.
After the project is created, install the dependencies:
npm install
Project structure
A project with a native C++ module includes additional files and directories:
.vscode/
assets/
resources/
src/
├── main/
├── native/
├── main.cc
├── proto/
├── greet.proto
├── renderer/
CMakeLists.txt
mobrowser.conf.json
package.json
tsconfig.json
vite.config.tsWorkflow
The native module uses Protocol Buffers (Protobuf) to define a type-safe interface between TypeScript and C++. The workflow is:
- Define messages and services in
.protofiles in thesrc/proto/directory. - Generate code for both TypeScript and C++ from the
.protofiles. The code generator runs automatically when you build the project. - Provide the C++/TypeScript service implementation by inheriting from the generated service classes.
- Invoke the C++/TypeScript service using the generated functions.
Let’s go through the workflow step by step in the sections below.
Defining Protobuf messages and services
The src/proto/ directory contains .proto files. In these files you can define:
- Messages: Data structures that can be passed between TypeScript and C++.
- Services: RPC interfaces that define the functions you can call.
Example of the src/proto/greet.proto file:
syntax = "proto3";
import "google/protobuf/wrappers.proto";
// Message definition.
message Person {
string name = 1;
}
// Service definition.
service GreetService {
rpc SayHello (Person) returns (google.protobuf.StringValue);
}
Generating code
To generate code for TypeScript and C++, run the following command:
npm run gen
The code generator will generate the code for TypeScript and C++ based on the .proto files in the src/proto/ directory.
- The generated TypeScript code will be placed in the
src/main/gendirectory. - The generated C++ code will be placed in the
src/native/gendirectory.
.vscode/
assets/
resources/
src/
├── main/
├── gen/
├── native/
├── gen/
├── proto/
├── renderer/
CMakeLists.txt
mobrowser.conf.json
package.json
tsconfig.json
vite.config.tsCalling C++ from TypeScript
In src/native/main.cc, implement the services by inheriting from the generated service classes and register them in the launch() function:
#include <iostream>
#include "rpc.h"
#include "gen/greet.rpc.h"
using google::protobuf::StringValue;
using mo::rpc::Callback;
class GreetServiceImpl : public GreetService {
public:
void SayHello(const Person* person,
Callback<StringValue> done) override {
StringValue response;
response.set_value("Hello, " + person->name() + "!");
std::move(done).Complete(response);
}
};
void launch() {
mo::rpc::RegisterService(new GreetServiceImpl());
}
In src/main/index.ts, you need to import the generated code and call the C++ service from TypeScript:
import { app } from '@mobrowser/api';
import { native } from './gen/native';
const response = await native.greet.SayHello({ name: 'John Doe' })
console.log(response.value) // Hello, John Doe!
Note: You can work with the native module only after the application is initialized. The native module is loaded in the main process during the application initialization.
Calling TypeScript from C++
In src/main/index.ts, you need to implement the service by inheriting from the generated service classes and register it using the native.registerService() function:
import { native } from './gen/native';
import { GreetServiceDescriptor } from './gen/native_service';
import { Person } from './gen/native/greet';
native.registerService(GreetServiceDescriptor, {
async SayHello(person: Person) {
return { value: `Hello, ${person.name}!` };
},
})
In src/native/main.cc, you can call the TypeScript service from C++ code:
#include <iostream>
#include "gen/greet.rpc.h"
void launch() {
Person person;
person.set_name("John Doe");
mo::rpc::greet.SayHello(person, [](mo::rpc::Result<StringValue> result) {
if (auto value = result.value()) {
std::cout << value->value() << std::endl;
}
});
}
Modifying CMakeLists.txt
You can modify the CMakeLists.txt file in the root of your project to add source files and third-party libraries to your native module.
Adding source files
# The source files of your app.
set(APP_SOURCES
src/native/main.cc
src/native/other.h
src/native/other.cc
)
Adding include directories
# Add include directories for the native module.
target_include_directories(mobrowser_lib PUBLIC
${PROJECT_SOURCE_DIR}/src/native/gen
${MOBROWSER_SDK_NATIVE_DIR}/include
${protobuf_SOURCE_DIR}/src
${protobuf_SOURCE_DIR}/third_party/utf8_range
${absl_SOURCE_DIR}
/path/to/your/library/include
)
Adding link libraries
# Add link libraries for the native module.
target_link_libraries(mobrowser_lib PRIVATE "-framework Cocoa")