Native C++ Module
Add a native C++ module for platform-specific APIs and performance-critical 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
Adding a native module to an existing project
To add a native module to the project, run:
npm run add native
Note: Projects created with an earlier version of create-mobrowser-app may not have the add script in package.json. In such projects, run npm run mobrowser add native instead.
The command:
- Creates
CMakeLists.txtin the project root andsrc/native/main.cc. - Creates the
src/native/proto/directory for your Protobuf definitions. - Adds the
@mobrowser/nativedependency and installs it.
If the project already has a CMakeLists.txt, the command stops without changing anything: the native module would otherwise be built with whatever configuration that file describes. Move that file aside and run the command again.
The module is created without any services. To add one, follow the workflow below: define a service in a .proto file in src/native/proto/, run npm run gen, then implement the generated service class in src/native/main.cc and register it in the launch() function.
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/native/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/native/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/native/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/native/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")