Native modules (.owm)
A native module is a compiled shared library that the kernel loads at runtime
with dlopen. You write C++, declare the functions, and call them from
TypeScript like any other module. This is how you push hot paths, hardware
access, or existing C/C++ code into an Owear app.
Where modules live in your app
Put .cpp files in native/. The Vite plugin and the CLI compile each file to
<name>.owm and load it at runtime.
native/
└── files.cpp → dist/modules/files.owm → ow.invoke('files', …)
In dev, ow dev compiles them to .owear/modules/ and adds that directory to
OW_MODULES_DIR.
A module in 15 lines
// native/files.cpp
#include <ow/Json.h>
#include <ow/Module.h>
static void readText(const ow_request_t* req, ow_response_t* res) {
// req->json holds a JSON array of the arguments.
// For readText(path): ["/etc/hostname"]
auto parsed = ow::json::Parse(std::string_view(req->json, req->json_len));
if (!parsed.value || !parsed.value->IsArray() || parsed.value->AsArray().empty())
return ow::Module::RespondError(res, "path required");
const std::string path = parsed.value->AsArray()[0].AsString();
// ... read the file ...
ow::Module::RespondOk(res, ow::json::Value(contents).Serialize().c_str());
}
OW_MODULE_BEGIN(files, "1.0.0")
OW_FN(readText)
OW_MODULE_END()
Call it from the renderer:
import { files } from '@owear/native'
const text = await files.readText('/etc/hostname')
The ABI
The ABI is C and small. A module exports exactly one symbol,
ow_module_descriptor, returning a ow_module_desc_t:
typedef void (*ow_fn_t)(const ow_request_t*, ow_response_t*);
typedef struct {
const char* name; // function name
ow_fn_t fn; // function pointer
} ow_fn_entry_t;
typedef struct {
const char* name; // module name
const char* version; // semantic version
const ow_fn_entry_t* fns; // function table
uint32_t fn_count;
} ow_module_desc_t;
The macros generate this from a flat table:
OW_MODULE_BEGIN(files, "1.0.0")
OW_FN(readText)
OW_FN(writeText)
OW_MODULE_END()
ow_request_t
typedef struct ow_request_t {
const char* json; // JSON array of arguments
uint32_t json_len;
const uint8_t* bin; // optional binary argument
uint32_t bin_len;
uint32_t window_id; // window that invoked (0 if none)
void* host; // opaque host handle
} ow_request_t;
ow_response_t
typedef struct ow_response_t {
int status; // 0 = ok, non-zero = error
const char* error; // error message when status != 0
const char* json; // JSON result
uint32_t json_len;
const uint8_t* bin; // optional binary result
uint32_t bin_len;
} ow_response_t;
Memory contract: buffers you return through res are only valid during the
call. The host copies them immediately. Recommended helpers
(ow::Module::RespondOk / RespondError) use a thread-local buffer that lives
until the next call on that thread, which is safe under this contract.
Never throw across the ABI. Wrap risky code in try/catch and return an
error via RespondError.
Available headers
| Header | Contents |
|---|---|
ow_api.h |
ow_module_desc_t, ow_fn_entry_t, ow_request_t, ow_response_t, OW_MODULE_EXPORT |
ow/Module.h |
OW_MODULE_BEGIN/FN/END, Module::RespondOk, Module::RespondError |
ow/Json.h |
json::Parse(sv) → ParseResult, Value (Null/Bool/Int/Double/String/Array/Object), .Find(), .As*, .Serialize(), json::JsLiteral() |
ow/Base64.h |
b64::Encode(string_view), b64::Decode(sv, out) |
ow/Shm.h |
ow_shm_put(data, len) → id, ow_shm_data(id, &len) → ptr, ow_shm_shutdown() |
Returning large binaries
For payloads ≥ 256 KB, publish them to shared memory and return a handle instead
of embedding base64. The renderer reads them with ow.readShared.
const char* id = ow_shm_put(reinterpret_cast<const uint8_t*>(png.data()), png.size());
std::string json = "{\"__ow_shm\":{\"id\":\"" + std::string(id) +
"\",\"size\":" + std::to_string(png.size()) + "}}";
ow::Module::RespondOk(res, json.c_str());
Emitting events
Modules can push events to the renderer. They receive a host handle at load time:
extern "C" OW_MODULE_EXPORT void ow_module_set_host(const ow_module_host_t* h) {
g_host = h;
}
// later, from any thread the host allows:
g_host->emit_event(g_host->ctx, window_id, "files.changed", payloadJson);
The renderer receives it with ow.on('files.changed', cb).
Two kinds of API: module vs builtin
| Kind | What it is | How it loads |
|---|---|---|
module |
Compiles to a standalone .owm |
dlopen at runtime from OW_MODULES_DIR / <exe>/modules |
builtin |
Coupled to the kernel (needs the WebView, ControlServer, etc.) |
Linked into the binary; registered by RegisterGeneratedBuiltins() |
As an app developer you almost always write modules. Framework contributors
add builtins inside the Owear repository (see
Adding an API).
Building
The Vite plugin runs owear-build-native for your app automatically. If you need
to call it directly:
OW_MODULES_OUT=dist/modules owear-build-native
It compiles every native/*.cpp and links against the framework headers found
via OW_INCLUDE_DIR.
Checklist
-
OW_MODULE_BEGIN/ENDwrap the function table. - Functions validate their arguments and never throw.
- Large payloads use shared memory, not base64.
- The module name matches the file name (
files.cpp→files). - You added the module to a manifest if it is part of the framework (not
needed for app modules under
native/).
Next steps
- Filesystem — study the stock
fsmodule for patterns. - C++ API reference.