openharmony-ability is a Rust + ArkTS framework for OpenHarmony (HarmonyOS) applications. Rust provides lifecycle management and typed, worker-safe plugin facades; the ArkTS NativeAbility package (@ohos-rs/ability) owns all platform objects and drives the real platform calls. The two sides communicate over an N-API bridge (napi-ohos) using stable named N-API types — never JSON. Native modules are built for *-unknown-linux-ohos targets; the host (macOS) can only run Rust unit tests, not the app. Licensed under MIT.
# Check the whole workspace
cargo check --workspace
# Host unit tests for a plugin crate (typeName / validation / mode+context assertions)
cargo test -p openharmony-ability-plugin-<name> --lib
# Lint: oxk lint for ArkTS/TS + clippy per OHOS arch with -D warnings
# (clippy excludes webview_example and xcomponent_example)
pnpm run lint
# Format: cargo fmt + oxk format for **/*.{ets,js,ts,json5}
pnpm run format
pnpm run format:check # CI check-only form
# Pre-commit hooks (rustfmt, oxk format, oxk lint)
pnpm run prek
# Build the native demo cdylib for a device
cd rust_example/demo_native && ohrs build --arch arm64
# Forbidden JSON-bridge scan — must stay empty
rg -n "BridgeJson|call_json|bridgeJson|requireBridgeJson|JSON\.stringify|JSON\.parse" \
crates/plugin-* plugins/*/src native_abilityArkTS UI (UIAbility / DefaultXComponent)
│ owns platform objects: UIAbilityContext, WindowStage, UIContext,
│ WebviewController, FrameNodes, lifecycle listeners
▼
NativeAbility (BridgeHost, per-module component root, BridgePluginFactory registry)
│ async via TSFN (Promise→future); worker→main sync via TSFN; events inside active napi_env
│ transport: named N-API values only (typeName-validated at the boundary)
▼
crates/ability bridge (BridgeRuntime / BridgeMainThread / PluginLifecycleEvent)
│
▼
Rust plugin facades (BridgePlugin) + application business code (run_loop)
| Crate | Purpose |
|---|---|
crates/ability |
Core: lifecycle/run_loop, bridge transport (impl_bridge_napi_type!, BridgeNapiType, BridgePlugin, BridgeRuntime, BridgeHost), ArkUI/xcomponent/ime binding re-exports |
crates/derive |
#[ability] entry macro |
crates/plugin-permission |
ohos.permission — async permission request |
crates/plugin-app-control |
ohos.app-control — sync main-thread terminate |
crates/plugin-window |
ohos.window — async avoid-area and multi-window operations |
crates/plugin-webview |
ohos.webview — WebView create, controller, custom protocol, JS proxy, callbacks |
crates/plugin-files |
ohos.files — file dialogs (open/save/folder) |
crates/plugin-url |
ohos.url — context.openLink |
crates/plugin-resource |
ohos.resource — inbound-only: ArkTS pushes resourceManager from Ability-scoped onInstall; no outbound actions |
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.
NativeAbility.onCreateopens each module/sessionBridgeHost, injects that module's bridge transport independently from rendering, then uses the Rust registry's{ id, execution, requires }declarations to select matching ArkTS factories before emittingability-create. Plugins and application registration never select by module.NativeAbility.onWindowStageCreateprovides theWindowStage, emitswindow-stage-create.- Each
DefaultXComponent.aboutToAppearbinds one distinct native module, resolves and observes that component's actualWindow, injects that module's rootFrameNode, then emits itsui-context-ready. One Ability may host multiple components/modules, including across windows; the same module cannot back two components concurrently. - Rust entry:
#[ability] fn init(app: OpenHarmonyApp)→app.register_plugin(P)…thenapp.run_loop(|event| …). - Per-module teardown order:
ui-context-destroy→ component detach →window-stage-destroy→ability-destroy→ dispose host/session.
BridgePlugintrait (crates/ability/src/bridge/mod.rs) — the stable Rust contract:type Mode = AsyncBridge | MainThreadSyncBridge,const ID,REQUIRED_CONTEXTS(ability|window-stage|ui-context).Modeis a closed trait, not a runtime flag. There is no numeric plugin version; incompatible ABI changes use a new action/typeName.impl_bridge_napi_type!(T, "ohos.<plugin>.<TypeName>")— pins a stable ABI typeName for#[napi(object)]structs; ArkTS validates the same string at parse and backfills it on response.- Async mode — Rust worker calls
BridgeRuntime::call_async::<P, Req, Resp>("action", req, options); data must beSend + 'static; the TSFN turns the ArkTS Promise into a future. - Sync mode — main thread: inside an active N-API callback,
app.with_main_thread_bridge(env, |b| b.call_sync::<P, Req, Resp>(…)); workers:BridgeRuntime::call_sync_from_worker(TSFN, execution still on the main thread, must not be called from the N-API main thread).BridgeMainThreadis!Send + !Sync, never cached. - Platform callbacks — ArkTS normally calls instance-scoped
context.invokeNativeSync(event, reqTypeName, respTypeName, value); Rust answers inBridgePlugin::on_main_thread_eventwithin the same callback. Only genuinely process-global transitions (ArkWeb engine initialization) useinvokeNativeSyncProcessWide. Fail-open (navigation) vs fail-closed (download) per event. - One component tree per native module — each
DefaultXComponentowns the single rootFrameNodefor its module, injected before that host'sui-context-ready; plugins mount viacontext.appendChild(key, node, cleanup)/removeChild(key). Multiple components/windows use multiple modules, while multiple WebViews in one component use distinct IDs. Built-inohos.nodegives Rust opaque u32 handles;FrameNodevalues never cross N-API. - Component-window routing —
windowStageEventremains Ability-scoped, but size/rect/avoid-area/keyboard listeners are attached to the actualWindowresolved from each component'sUIContext; those events are never broadcast from the main window to sub-window modules.
Read docs/plugin-development-standard.md (the single authoritative spec, Chinese) and invoke the bundled named-napi-contracts skill before adding or modifying any plugin. Non-negotiable rules:
- Paired packages only:
crates/plugin-<name>+plugins/<name>. ID, Mode, and requires must be identical on both sides (BridgeHost.configurePluginshard-validates).LazyPluginhas no module filter; each Rust registry selects its own matching factory. - No JSON across the bridge, ever. All requests/responses/platform callbacks are named N-API values.
BridgeJson,call_json,bridgeJson,requireBridgeJson,JSON.stringify/parseare banned. Built-in scalars:std.string,std.bytes,std.bool,std.i32,std.f64. - Actions are kebab-case verb phrases (
create,get-avoid-area,evaluate-script); IDs/actions/typeNames match^[A-Za-z0-9._-]+$. Rust fields snake_case, ArkTS camelCase. - Never store
Env,napi_value,napi_ref, ArkTS objects/functions in workers, statics, or long-lived caches. Sync paths neverawait; async paths never holdEnv. - Platform objects live in ArkTS only. Plugins subscribe to the lifecycle chain — never replace
NativeAbilitylifecycle callbacks.onDisposemust be idempotent and must not block other plugins' dispose. - WebView specifics: schemes must be registered via
WebviewProtocol::registerbeforeWebviewController.initializeWebEngine(); custom protocol (URL requests) and JS proxy (window.<obj>.<method>()) are distinct bridges;transparentis expressed atcreatetime.
Follow the local spec docs/plugin-development-standard.md (§1 creation order, §3 named N-API contracts, §8 registration/assembly, §9 acceptance checklist) plus the full implementation checklist in the bundled .agents/skills/named-napi-contracts/ skill.
- 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/prekhooks;ohrsfor native module builds