Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,33 @@ jobs:

- name: Check
run: pnpm check

# Pure C module (c_module): compile the framework and demo for arm64-v8a with the OHOS SDK.
# The SDK-bundled cmake/ninja are used so no extra tooling is required.
C-Module:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v4

- name: Setup OpenHarmony SDK
uses: openharmony-rs/setup-ohos-sdk@v0.1
id: setup-ohos
with:
version: "5.0.0"

- name: Build
run: |
# setup-ohos-sdk exports OHOS_SDK_NATIVE (native SDK root); fall back to $OHOS_SDK/native.
SDK_NATIVE="${OHOS_SDK_NATIVE:-${OHOS_SDK}/native}"
if [ -x "$SDK_NATIVE/build-tools/cmake/bin/cmake" ]; then
CMAKE="$SDK_NATIVE/build-tools/cmake/bin/cmake"
PATH="$SDK_NATIVE/build-tools/cmake/bin:$PATH"
else
CMAKE="$(command -v cmake)"
fi
"$CMAKE" -S c_module -B c_module/build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE="$SDK_NATIVE/build/cmake/ohos.toolchain.cmake" \
-DOHOS_ARCH=arm64-v8a
"$CMAKE" --build c_module/build
test -f c_module/build/example/demo_native/libdemo_native.so
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,11 @@ package/libs
dist
oh_modules
build/
c_module/build*/

# logs and build metadata
*.log
*.tmp
compile_commands.json
.cxx/
cmake-build-*/
21 changes: 20 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pnpm run prek
# Build the native demo cdylib for a device
cd rust_example/demo_native && ohrs build --arch arm64

# Build the pure C demo module (c_module) without touching the demo app
scripts/build-c-demo.sh
# ...and install it into demo/entry/libs (replaces the Rust-built libdemo_native.so)
scripts/build-c-demo.sh --install

# Forbidden JSON-bridge scan — must stay empty
rg -n "BridgeJson|call_json|bridgeJson|requireBridgeJson|JSON\.stringify|JSON\.parse" \
crates/plugin-* plugins/*/src native_ability
Expand Down Expand Up @@ -65,6 +70,20 @@ Rust plugin facades (BridgePlugin) + application business code (run_loop)

Every `crates/plugin-<name>` is paired with an ArkTS HAR in `plugins/<name>` that exports the matching `BridgePluginFactory`; core (`crates/ability`) never imports any `plugin-*` crate.

### Pure C Framework (`c_module`)

`c_module/ability` is the C99 counterpart of `crates/ability`: it implements the same five native
module exports (`init`/`render`/`onBackPressIntercept`/`onBridgeSyncEvent`/`onBridgeLifecycle`)
and exposes an SDL-style application model (`OHAbility_StartApp` + AppInit/AppEvent/AppIterate/
AppQuit callbacks; the application thread starts on the first XComponent surface).
`c_module/example/demo_native` is the pure C demo module reusing the `demo_native` name and the
Rust demo's export surface (Index.d.ts), so the demo app is unchanged when its
`libdemo_native.so` is swapped. Read `c_module/README.md` before touching the C module.
Non-negotiable rules that carry over: no napi values across threads or in statics (builders and
responders run on the ArkTS main thread), identifier validation `^[A-Za-z0-9._-]+$`,
fail-closed sync-event dispatch, and every queued TSFN request must be freed by its call-js
callback (including the env==NULL abort drain).

### Startup Flow

1. `NativeAbility.onCreate` opens the module/session `BridgeHost`, creates factories, emits `ability-create`.
Expand Down Expand Up @@ -101,4 +120,4 @@ Follow the local spec `docs/plugin-development-standard.md` (§1 creation order,

- **N-API bridge**: `napi-ohos`, `napi-derive-ohos`, `napi-build-ohos`, `napi-sys-ohos` (1.2, napi8)
- **OHOS bindings**: `ohos-arkui-binding`, `ohos-xcomponent-binding`, `ohos-web-binding`, `ohos-ime-binding`, `ohos-display-binding`, `ohos-hilog-binding`, `ohos-resource-manager-binding`
- **Tooling**: pnpm@10.22.0; `@ohos-rs/oxk` (oxk format/lint for ets/js/ts/json5); `@j178/prek` hooks; `ohrs` for native module builds
- **Tooling**: pnpm@10.22.0; `@ohos-rs/oxk` (oxk format/lint for ets/js/ts/json5); `@j178/prek` hooks; `ohrs` for native module builds; OHOS SDK cmake/ninja + `ohos.toolchain.cmake` for `c_module` builds (CI job `C-Module` compiles it on every PR)
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ OpenHarmony applications are driven by callbacks, so there are two important con
- `native_ability` — ArkTS package source shared by Rust and C/SDL native modules
- `package` — packaged ohpm artifact source
- `demo` — unified Harmony demo project
- `rust_example/demo_native` — unified native demo implementation
- `rust_example/demo_native` — unified native demo implementation (Rust)
- `c_module` — pure C framework (`c_module/ability`) + pure C demo module
(`c_module/example/demo_native`); SDL-style integration for business C code. See
`c_module/README.md`.

## Usage

Expand Down Expand Up @@ -79,8 +82,18 @@ ohrs build --arch arm64

- Harmony demo project: `demo`
- Native demo module (Rust example): `rust_example/demo_native/src/lib.rs`
- Native demo module (pure C example): `c_module/example/demo_native/src/main.c`
- ArkTS package source: `native_ability`

## Pure C Integration

Business code in C99 can use the same ArkTS host and plugin set without Rust: `c_module/ability`
implements the native module contract (`init`/`render`/`onBackPressIntercept`/
`onBridgeSyncEvent`/`onBridgeLifecycle`) and offers an SDL-style application model
(`OHAbility_StartApp` with AppInit/AppEvent/AppIterate/AppQuit), the three bridge call modes
(async/sync/worker-sync), C plugin registration, and the full event surface. Build the C demo
module with `scripts/build-c-demo.sh` (optionally `--install` to swap it into the demo app).

## License

[MIT](./LICENSE)
7 changes: 7 additions & 0 deletions c_module/.clang-format
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Formatting for the pure C framework and demo (clang-format).
BasedOnStyle: LLVM
IndentWidth: 4
ColumnLimit: 100
# Keep explicit include blocks: the pixelmap_native.h header must precede arkui/native_node.h
# in this SDK (missing OH_PixelmapNative declaration), which alphabetical sorting would break.
IncludeBlocks: Preserve
32 changes: 32 additions & 0 deletions c_module/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
cmake_minimum_required(VERSION 3.16.0)

project(ohos-c-ability C)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

# ---------------------------------------------------------------------------
# OHOS SDK stub library resolution.
#
# The NDK ships stub libraries (lib*.z.so or plain names) in the sysroot. The
# ohos.toolchain.cmake exposes CMAKE_SYSROOT and OHOS_TOOLCHAIN_NAME
# (e.g. aarch64-linux-ohos). Some SDK versions only ship one spelling, so we
# try both and fall back to the default search.
# ---------------------------------------------------------------------------
function(ohos_find_library out_var name)
set(candidates "lib${name}.z.so" "lib${name}.so")
find_library(${out_var}
NAMES ${candidates}
HINTS "${CMAKE_SYSROOT}/usr/lib/${OHOS_TOOLCHAIN_NAME}"
NO_DEFAULT_PATH)
if(NOT ${out_var})
find_library(${out_var} NAMES ${candidates})
endif()
if(NOT ${out_var})
message(FATAL_ERROR "OHOS SDK library '${name}' was not found")
endif()
message(STATUS "OHOS SDK library ${name} -> ${${out_var}}")
endfunction()

add_subdirectory(ability)
add_subdirectory(example/demo_native)
166 changes: 166 additions & 0 deletions c_module/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
# c_module — Pure C access to the ArkTS bridge

This directory adds a **pure C** path to the `@ohos-rs/ability` ArkTS host, mirroring how the
[richerfu/SDL](https://github.com/richerfu/SDL) OHOS backend integrates: business code written in
C99 implements the same native module contract (`init` / `render` / `onBackPressIntercept` /
`onBridgeSyncEvent` / `onBridgeLifecycle`) and drives the existing ArkTS plugins through the
typed N-API bridge — no Rust, no C++ ABI.

```
c_module/
├── ability/ # Pure C framework library (static `oh_ability`)
│ ├── include/
│ │ ├── oh_ability.h # Public C API (SDL-style entry model)
│ │ └── oh_ability_events.h
│ └── src/ # module/lifecycle/bridge/registry/xcomponent/ime/node/...
└── example/demo_native/ # Pure C demo native module (libdemo_native.so)
├── src/
│ ├── main.c # NAPI entry + application callbacks + plugin assembly
│ ├── exports.c # export table (demo.* exports + plugin exports)
│ ├── bridge_demos.c # shared helpers (deferred/object plumbing, back-press)
│ └── plugins/ # one directory per plugin (Rust crates/plugin-* layout)
│ ├── permission/ # ohos.permission
│ ├── files/ # ohos.files
│ ├── url/ # ohos.url
│ ├── resource/ # ohos.resource (inbound plugin)
│ ├── webview/ # ohos.webview (inbound plugin + scheme + JS proxy)
│ ├── window/ # ohos.window (reserved)
│ └── app-control/ # ohos.app-control (reserved)
└── types/libdemo_native/ # Index.d.ts aligned with the Rust demo's export surface
```

## What the framework provides

- **Module contract** — `OHAbility_RegisterModule(env, exports)` exports the five functions the
ArkTS host expects; `init()` returns the `ApplicationLifecycle` object; `render()` mounts the
XComponent and keeps the bridge bindings.
- **SDL-style application model** — `OHAbility_StartApp()` with
`AppInit` / `AppIterate` / `AppEvent` / `AppQuit` callbacks. The application thread starts
automatically when the first XComponent surface is created.
- **Bridge calls** —
- `OHAbility_CallAsync` / `OHAbility_CallAsyncPromise` (any thread; request builders and
response handlers run on the ArkTS main thread),
- `OHAbility_CallSync` (main-thread N-API callbacks only),
- `OHAbility_CallSyncFromWorker` (worker blocks; execution on the main thread).
- **C plugins** — `OHAbility_RegisterPlugin(id, version, on_sync_event, on_lifecycle)` receives
ArkTS-originated main-thread events (`context.invokeNativeSync(...)`) and lifecycle
notifications.
- **Built-in `ohos.node` surface plugin** — `OHAbility_NodeCreateContainer` /
`OHAbility_NodeAppendChild` / `OHAbility_NodeMountIntoRoot` / `OHAbility_NodeDispose` (+
`...InWindow` variants) compose the session FrameNode tree through opaque handles. The plugin
is installed automatically by the ArkTS BridgeHost; the C side is outbound-only (Rust
`NodeExt` / `NodeSurface` parity), and acknowledgement rejection surfaces as an error.
- **Event surface** — lifecycle, window stage, configuration, memory, surface, touch/key/mouse/
hover, frame callbacks, IME, keyboard height, avoid areas; delivered as a tagged union
(`oh_ability_events.h`).
- **Platform access** — init context (`basePath` / `prefPath` / `preferredLocales` /
`moduleName`), native `resourceManager`, `OHNativeWindow`, frame-rate ranges, back-press
interception, saved-state slot, IME show/hide, `OHAbility_Wake`.
- **Snapshot accessors (Rust `OpenHarmonyApp` parity)** — `OHAbility_GetConfiguration`,
`OHAbility_GetContentRect`, `OHAbility_GetWindowRect`, `OHAbility_GetAvoidArea(type)`,
`OHAbility_GetScale` (display density), kept in sync with the latest platform callbacks.

## Threading rules (the framework enforces these)

- N-API values never cross threads and never live in long-lived storage: `ValueBuilder` and
`ValueResponder` callbacks always run on the ArkTS main thread with a live `napi_env`.
- `OHAbility_CallSync` is rejected outside the main-thread N-API environment;
`OHAbility_CallSyncFromWorker` is rejected on the main thread (deadlock guard).
- Event strings are owned by the framework for the duration of `AppEvent` only.

## Plugin switches (compile-time macros)

Every plugin (and the two platform capabilities) is gated by a macro, mirroring cargo features
/ SDL subsystems. All default to ON; disabled plugins keep their export names as explicit-error
stubs so the demo Index page still imports cleanly, and their platform libraries are not linked.

| Macro | Scope | Effect when OFF |
| --- | --- | --- |
| `OH_ABILITY_PLUGIN_NODE` | framework | built-in `ohos.node` facade not compiled (`OHAbility_Node*` undeclared); composed webview stub |
| `OH_ABILITY_PLUGIN_PERMISSION` | demo | `demoRequestPermissionFromMainThread` stub |
| `OH_ABILITY_PLUGIN_FILES` | demo | `demoFileDialogOpen`/`Save` stubs |
| `OH_ABILITY_PLUGIN_URL` | demo | `demoOpenUrl` stub |
| `OH_ABILITY_PLUGIN_RESOURCE` | demo | `ohos.resource` inbound plugin not registered; resource exports stub |
| `OH_ABILITY_PLUGIN_WEBVIEW` | demo | webview plugin/demos stub; `libohweb` not linked |
| `OH_ABILITY_PLUGIN_WINDOW` | demo | reserved (no demo surface yet) |
| `OH_ABILITY_PLUGIN_APP_CONTROL` | demo | reserved (no demo surface yet) |
| `OH_ABILITY_ENABLE_IME` | framework | IME support not compiled; `libohinputmethod` not linked |
| `OH_ABILITY_ENABLE_DISPLAY` | framework | `OHAbility_GetScale` returns 1.0; `libnative_display_manager` not linked |

```bash
# Build with plugins disabled
cmake -S c_module -B c_module/build -G Ninja \
-DCMAKE_TOOLCHAIN_FILE=<SDK>/native/build/cmake/ohos.toolchain.cmake \
-DOHOS_ARCH=arm64-v8a \
-DOH_ABILITY_PLUGIN_WEBVIEW=OFF -DOH_ABILITY_PLUGIN_RESOURCE=OFF
```

## Building

```bash
# Build for a device ABI (defaults to arm64-v8a)
scripts/build-c-demo.sh

# Build and install into the demo app (replaces the Rust-built libdemo_native.so)
scripts/build-c-demo.sh --install

# Other ABIs / clean
scripts/build-c-demo.sh --arch=x86_64 --clean
```

The script resolves the OHOS SDK from `$OHOS_SDK` or the newest DevEco Studio installation and
uses the SDK-bundled cmake/ninja when present. The demo app is untouched unless `--install` is
passed; `libdemo_native.so` is git-ignored (`*.so`), so the C module can be swapped in and out
of the demo without affecting the repository.

## Demo

`c_module/example/demo_native` is the pure C counterpart of `rust_example/demo_native`: it reuses
the `demo_native` module name and the exact export surface of the Rust demo
(`types/libdemo_native/Index.d.ts`), so the demo app's Index page runs unchanged against a pure C
backend. Every `demo*` export is a thin wrapper over the bridge:

| Demo | Bridge path |
| --- | --- |
| `demoPluginString` / `demoPluginBytes` / `demoPluginProfile` | `demo.raw` echo / reverse-bytes / bump-profile |
| `demoPluginLogin` | `demo.login` authorize → publish chain |
| `demoPluginSyncContext` / `demoPluginSyncFromWorker` | `demo.main-thread` inspect (sync / worker-sync) |
| `demoRequestPermissionFromMainThread` | `ohos.permission` request |
| `demoOpenUrl` | `ohos.url` open-url |
| `demoFileDialogOpen` / `demoFileDialogSave` | `ohos.files` file-dialog |
| `createDemoWebview` / composed / bottom / sub-window | `ohos.webview` create (+ `ohos.node` for composed) |
| `setBackgroundColor` / `setVisible` / `evaluateDemoWebviewScript` | `ohos.webview` controller actions |
| `demoResourceManagerReady` / `demoResourceRawDirCount` | native `resourceManager` (rawfile) |
| `toggleBackPressIntercept` | framework back-press interceptor |

The module also registers two C plugins: `ohos.resource` (answers the ArkTS-pushed
`resource-manager-ready` event) and `ohos.webview` (answers the sync events the WebView plugin
emits: `before-engine-init`, `engine-initialized`, `controller-attached`/`removed`,
`navigation-request`, `download-start`, `download-end`, `title-change` — with event logging
aligned to the Rust demo).

WebView parity with the Rust demo:

- The custom `demoweb` scheme is registered at module import time (before the WebView engine
initializes) and served natively on the ArkWeb IO thread (`ArkWeb_SchemeHandler` +
`ArkWeb_ResourceHandler`, responding with the same page as the Rust demo's index.html).
- The `window.test` JavaScript proxy is registered on `controller-attached` through
`OH_NativeArkWeb_RegisterJavaScriptProxy`, before the initial load.
- The demo page (`demoweb://index`) mirrors the Rust page, including the `test test !` proxy
button; `evaluateDemoWebviewScript` returns the same `document.title`.

The application thread runs the SDL-style callbacks and logs events; state saving uses the
framework's saved-state slot.

## Verification

```bash
scripts/build-c-demo.sh # builds libdemo_native.so
scripts/build-c-demo.sh --install # installs it into demo/entry/libs/arm64-v8a/
```

Then build the demo app (DevEco Studio or `hvigorw assembleHap`) and run it on a device:
lifecycle logs (`ohAbility` / `ohosCDemo` HiLog tags), the XComponent surface, and every demo
button should behave like the Rust demo. Switch back by rebuilding the Rust module
(`cd rust_example/demo_native && ohrs build --arch arm64`) and copying its `dist/arm64-v8a/
libdemo_native.so` into `demo/entry/libs/arm64-v8a/`.
59 changes: 59 additions & 0 deletions c_module/ability/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Pure C framework library: implements the @ohos-rs/ability native module
# contract and the SDL-style application model for business C code.
#
# Capability switches (compile-time, like cargo features / SDL subsystems):
# -DOH_ABILITY_ENABLE_IME=OFF disable IME support (no ohinputmethod link)
# -DOH_ABILITY_ENABLE_DISPLAY=OFF disable display density queries (no native_display_manager link)
# -DOH_ABILITY_PLUGIN_NODE=OFF disable the built-in ohos.node surface plugin
# Disabled capabilities keep their API declarations out of oh_ability.h and return
# OH_ABILITY_ERROR_NOT_READY from the corresponding accessors.
option(OH_ABILITY_ENABLE_IME "Enable IME support (links ohinputmethod)" ON)
option(OH_ABILITY_ENABLE_DISPLAY "Enable display density queries (links native_display_manager)" ON)
option(OH_ABILITY_PLUGIN_NODE "Enable the built-in ohos.node surface plugin" ON)

set(OH_ABILITY_SOURCES
src/state.c
src/module.c
src/lifecycle.c
src/event.c
src/bridge.c
src/registry.c
src/xcomponent.c
src/configuration.c
)
if(OH_ABILITY_ENABLE_IME)
list(APPEND OH_ABILITY_SOURCES src/ime.c)
endif()
if(OH_ABILITY_PLUGIN_NODE)
list(APPEND OH_ABILITY_SOURCES src/node.c)
endif()

add_library(oh_ability STATIC ${OH_ABILITY_SOURCES})

target_include_directories(oh_ability PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)

# -Wno-unused-command-line-argument: the OHOS toolchain passes --gcc-toolchain to clang, which
# rejects it as unused for C compilation on some SDK versions; combined with -Werror it fails.
target_compile_options(oh_ability PRIVATE -Wall -Wextra -Werror -Wno-unused-command-line-argument)

# The capability macros are PUBLIC: consumers include <oh_ability.h>, whose declarations are
# gated on them.
target_compile_definitions(oh_ability PUBLIC
$<$<BOOL:${OH_ABILITY_ENABLE_IME}>:OH_ABILITY_ENABLE_IME>
$<$<BOOL:${OH_ABILITY_ENABLE_DISPLAY}>:OH_ABILITY_ENABLE_DISPLAY>
$<$<BOOL:${OH_ABILITY_PLUGIN_NODE}>:OH_ABILITY_PLUGIN_NODE>
)

# hilog for framework diagnostics.
ohos_find_library(OHOS_HILOG_LIB hilog_ndk)
target_link_libraries(oh_ability PUBLIC ${OHOS_HILOG_LIB})

if(OH_ABILITY_ENABLE_IME)
ohos_find_library(OHOS_INPUTMETHOD_LIB ohinputmethod)
target_link_libraries(oh_ability PUBLIC ${OHOS_INPUTMETHOD_LIB})
endif()

if(OH_ABILITY_ENABLE_DISPLAY)
ohos_find_library(OHOS_DISPLAY_MANAGER_LIB native_display_manager)
target_link_libraries(oh_ability PUBLIC ${OHOS_DISPLAY_MANAGER_LIB})
endif()
Loading
Loading