diff --git a/README.md b/README.md index 2d740f5..140e7bf 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ We need a entry-point to start the application, and we use ArkTS to manage the a } ``` -5. Change the `moduleName` to your rust project name. For example, we need to change it with `hello` in this project. +5. Set `moduleName` to the bare module name, for example `hello`. The framework will load `libhello.so` internally. You can also pass `string[]` when one ability needs to initialize multiple native modules. 6. Build your rust project and copy the dynamic library to (Open-)Harmony(Next) project. diff --git a/crates/ability/README.md b/crates/ability/README.md index 9a8484f..6ec626b 100644 --- a/crates/ability/README.md +++ b/crates/ability/README.md @@ -4,6 +4,10 @@ openharmony-ability is a crate for the OpenHarmony/HarmonyNext ability. It provides a way to create an OpenHarmony/HarmonyNext application with rust. +## Runtime Context + +`RustAbility` passes the ArkTS init context into Rust during `init(context)`. In Rust, `OpenHarmonyApp` can read `moduleName`, `basePath`, `prefPath`, and `preferredLocales` via `init_context()`, `module_name()`, `base_path()`, `pref_path()`, and `preferred_locales()`. + ## License This project is licensed under the [MIT license](https://github.com/harmony-contrib/openharmony-ability/blob/main/LICENSE) \ No newline at end of file diff --git a/crates/ability/src/app.rs b/crates/ability/src/app.rs index c5d1936..1ef60eb 100644 --- a/crates/ability/src/app.rs +++ b/crates/ability/src/app.rs @@ -11,6 +11,7 @@ use std::{ }; use futures_channel::oneshot; +use napi_derive_ohos::napi; use napi_ohos::{ bindgen_prelude::{CallbackContext, Function, JsObjectValue, Object, Unknown}, threadsafe_function::ThreadsafeFunctionCallMode, @@ -65,6 +66,15 @@ fn parse_avoid_area_options(options: Object<'_>) -> Option<(AvoidAreaType, Avoid Some((area_type, avoid_area)) } +#[napi(object)] +#[derive(Clone, Debug, Default)] +pub struct AbilityInitContext { + pub base_path: Option, + pub pref_path: Option, + pub preferred_locales: Option, + pub module_name: Option, +} + #[derive(Clone)] pub struct OpenHarmonyAppInner { pub(crate) raw_window: Option, @@ -77,6 +87,7 @@ pub struct OpenHarmonyAppInner { pub(crate) rect: Rect, pub(crate) window_rect: Rect, pub(crate) avoid_areas: HashMap, + pub(crate) init_context: AbilityInitContext, } impl PartialEq for OpenHarmonyAppInner { @@ -132,6 +143,7 @@ impl OpenHarmonyAppInner { rect: Default::default(), window_rect: Default::default(), avoid_areas: HashMap::new(), + init_context: AbilityInitContext::default(), } } @@ -191,6 +203,14 @@ impl OpenHarmonyAppInner { default_display_scaled_density() } + pub fn init_context(&self) -> AbilityInitContext { + self.init_context.clone() + } + + pub fn set_init_context(&mut self, context: AbilityInitContext) { + self.init_context = context; + } + pub fn exit(&self, code: i32) -> Result<()> { let ret = unsafe { get_helper() }; if let Some(h) = ret.borrow().as_ref() { @@ -287,6 +307,31 @@ impl OpenHarmonyApp { .set_frame_rate(min, max, expected); } + #[doc(hidden)] + pub fn set_init_context(&self, context: AbilityInitContext) { + self.inner.write().unwrap().set_init_context(context); + } + + pub fn init_context(&self) -> AbilityInitContext { + self.inner.read().unwrap().init_context() + } + + pub fn module_name(&self) -> Option { + self.init_context().module_name + } + + pub fn base_path(&self) -> Option { + self.init_context().base_path + } + + pub fn pref_path(&self) -> Option { + self.init_context().pref_path + } + + pub fn preferred_locales(&self) -> Option { + self.init_context().preferred_locales + } + pub fn show_keyboard(&self) { let _guard = self .is_keyboard_show diff --git a/crates/derive/README.md b/crates/derive/README.md index 7d4d631..539bb5d 100644 --- a/crates/derive/README.md +++ b/crates/derive/README.md @@ -42,6 +42,10 @@ fn openharmony_app(app: OpenHarmonyApp) { } ``` +### Init Context + +The generated `init(context)` now forwards ArkTS init data into Rust. Inside your `OpenHarmonyApp`, you can read it through `app.init_context()`, `app.module_name()`, `app.base_path()`, `app.pref_path()`, and `app.preferred_locales()`. + ### webview Using `ArkWeb` to render everything. diff --git a/crates/derive/src/lib.rs b/crates/derive/src/lib.rs index de16c56..e4c9d77 100644 --- a/crates/derive/src/lib.rs +++ b/crates/derive/src/lib.rs @@ -29,24 +29,18 @@ fn parse_ability_args(attr: TokenStream) -> syn::Result { return Ok(args); } - // Parse attribute arguments as a list of Meta items - // Convert proc_macro::TokenStream to proc_macro2::TokenStream for parsing let attr_stream = proc_macro2::TokenStream::from(attr); let meta_list = syn::parse2::(attr_stream)?; - // Iterate over the meta items for meta in meta_list.metas { match meta { Meta::Path(path) => { - // Handle named flags like `webview` if path.is_ident("webview") { args.webview = true; } } Meta::NameValue(MetaNameValue { path, value, .. }) => { - // Handle key-value pairs like `protocol = "value"` if path.is_ident("protocol") { - // Parse the value as an expression and extract string literal if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit_str), .. @@ -61,10 +55,7 @@ fn parse_ability_args(attr: TokenStream) -> syn::Result { } } } - Meta::List(_) => { - // Handle nested list-style attributes if needed - // For now, we'll skip them - } + Meta::List(_) => {} } } @@ -120,7 +111,6 @@ pub fn ability(attr: TokenStream, item: TokenStream) -> TokenStream { } }; - // Register custom protocol if protocol is specified and webview is enabled let protocol_registrations_apply = if args.protocol.is_some() && args.webview { quote::quote! { #[napi_derive_ohos::napi] @@ -153,9 +143,6 @@ pub fn ability(attr: TokenStream, item: TokenStream) -> TokenStream { #protocol_registrations_apply - /// Get back press interceptor result - /// Can be called from ArkTS page lifecycle (onBackPress) - /// Returns true to intercept back press, false to pass through #[napi_derive_ohos::napi] pub fn on_back_press_intercept() -> bool { (*APP).get_back_press_interceptor() @@ -164,7 +151,9 @@ pub fn ability(attr: TokenStream, item: TokenStream) -> TokenStream { #[napi_derive_ohos::napi] pub fn init<'a>( env: &'a napi_ohos::Env, + context: Option, ) -> napi_ohos::Result> { + (*APP).set_init_context(context.unwrap_or_default()); let lifecycle_handle = openharmony_ability::create_lifecycle_handle(env, (*APP).clone())?; #fn_name((*APP).clone()); Ok(lifecycle_handle) diff --git a/package/README.md b/package/README.md index ea59c48..8f4d7cb 100644 --- a/package/README.md +++ b/package/README.md @@ -35,7 +35,7 @@ Here are some notes and tips: 1. For every lifecycle callback, you must call the super method to forward the event to rust code as first and then write your own logic. -2. `moduleName` is the name of your native module name which file name is `lib${moduleName}.so`. **You must define it in your project**. +2. `moduleName` is the bare native module name, such as `hello`. The framework resolves it to `libhello.so` internally. You must define it in your project. ### loadMode @@ -127,7 +127,7 @@ struct Index { Column() { SegmentButton({ options: this.tabOptions, selectedIndexes: $tabSelectedIndexes }) // Must use the default component to render the UI - DefaultXComponent() + DefaultXComponent({ moduleName: "example" }) } .width('100%') } diff --git a/rust_ability/ability_rust/README.md b/rust_ability/ability_rust/README.md index 404d0d8..a189ace 100644 --- a/rust_ability/ability_rust/README.md +++ b/rust_ability/ability_rust/README.md @@ -84,7 +84,7 @@ We need a entry-point to start the application, and we use ArkTS to manage the a } ``` -5. Change the `moduleName` to your rust project name. For example, we need to change it with `hello` in this project. +5. Set `moduleName` to the bare module name, for example `hello`. The framework will load `libhello.so` internally. You can also pass `string[]` when one ability needs to initialize multiple native modules. 6. Build your rust project and copy the dynamic library to (Open-)Harmony(Next) project. diff --git a/rust_ability/ability_rust/oh-package-lock.json5 b/rust_ability/ability_rust/oh-package-lock.json5 index 1d83484..f43ac5b 100644 --- a/rust_ability/ability_rust/oh-package-lock.json5 +++ b/rust_ability/ability_rust/oh-package-lock.json5 @@ -12,7 +12,7 @@ "libability_rust.so@src/main/cpp/types/libability_rust": { "name": "libability_rust.so", "version": "1.0.0", - "resolved": "", + "resolved": "src/main/cpp/types/libability_rust", "registryType": "local" } } diff --git a/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets b/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets index 0d3aea7..9bf4e69 100644 --- a/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets +++ b/rust_ability/ability_rust/src/main/ets/ability/RustAbility.ets @@ -1,35 +1,66 @@ import { AbilityConstant, Configuration, UIAbility, Want } from "@kit.AbilityKit"; +import common from "@ohos.app.ability.common"; import window from "@ohos.window"; import webview from "@ohos.web.webview"; import * as Entry from "../components/MainPage"; -import { ApplicationLifecycle, Module } from "./type"; +import { AbilityInitContext, ApplicationLifecycle, Module } from "./type"; import { Loadable } from "../helper/loadable"; export const STATE_KEY = "ohos.rs.ability.application.state"; export class RustAbility extends UIAbility { - /** - * load dynamic library - */ - public moduleName: string = ""; - /** - * Jump to defaultPage by default - * @default true - */ + public moduleName: string | string[] = ""; public defaultPage: boolean = true; - /** - * Current page mode,support xcomponent and webview - * @default xcomponent - * @deprecated don't use it. Since 0.3, we can render xcomponent and webview in mixed mode. - */ public mode: "xcomponent" | "webview" = "xcomponent"; - /** - * Load dynamic library mode. - * - */ public loadMode: "async" | "sync" = "async"; - private nativeModule: Module | null = null; - private lifecycle: ApplicationLifecycle | null = null; + private nativeModules: Module[] = []; + private lifecycles: ApplicationLifecycle[] = []; + private loadedModuleNames: string[] = []; + + protected resolveModuleNames(): string[] { + const moduleNames = Loadable.resolveModuleNames(this.moduleName); + if (moduleNames.length === 0) { + throw new Error("moduleName must contain at least one module"); + } + return moduleNames; + } + + protected forEachLifecycle(handler: (lifecycle: ApplicationLifecycle) => void): void { + for (const lifecycle of this.lifecycles) { + try { + handler(lifecycle); + } catch (_) {} + } + } + + protected parseSavedStateMap(rawState: string): Record { + if (!rawState) { + return {}; + } + + try { + const parsed = JSON.parse(rawState) as Record; + const mapped: Record = {}; + const keys = Object.keys(parsed); + for (const key of keys) { + const value = parsed[key]; + mapped[key] = typeof value === "string" ? value : String(value); + } + return mapped; + } catch (_) { + return {}; + } + } + + protected createInitContext(moduleName: string): AbilityInitContext { + const context = this.context as common.UIAbilityContext; + return { + basePath: context?.filesDir ?? "", + prefPath: context?.filesDir ?? "", + preferredLocales: context?.config?.language ?? "", + moduleName, + }; + } async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise { const isRestore: boolean = @@ -39,65 +70,134 @@ export class RustAbility extends UIAbility { AppStorage.setOrCreate("moduleName", this.moduleName); AppStorage.setOrCreate("loadMode", this.loadMode); - const packageName = `lib${this.moduleName}.so`; - this.nativeModule = await Loadable.load(packageName, this.loadMode); - // Register custom protocol as first - // You can define it by yourself or use ability to define. - if (typeof this.nativeModule?.registerCustomProtocol === "function") { - this.nativeModule!.registerCustomProtocol(); + const requestedModules: string[] = this.resolveModuleNames(); + const restoredStateMap: Record = this.parseSavedStateMap(state); + let webEngineRequired = false; + + this.nativeModules = []; + this.lifecycles = []; + this.loadedModuleNames = []; + + for (const moduleName of requestedModules) { + const module: Module = await Loadable.load(moduleName, this.loadMode); + this.nativeModules.push(module); + this.loadedModuleNames.push(moduleName); + + if (typeof module.registerCustomProtocol === "function") { + module.registerCustomProtocol(); + webEngineRequired = true; + } } - // Must call it when custom protocol is enabled. - webview.WebviewController.initializeWebEngine(); + if (webEngineRequired) { + webview.WebviewController.initializeWebEngine(); + } + + for (let i = 0; i < this.nativeModules.length; i++) { + const module: Module = this.nativeModules[i]; + const moduleName: string = this.loadedModuleNames[i]; + const lifecycle: ApplicationLifecycle = module.init(this.createInitContext(moduleName)); - this.lifecycle = this.nativeModule!.init(); - this.lifecycle?.windowStageEventCallback.onAbilityCreate(state); + this.lifecycles.push(lifecycle); + + const restoredState = restoredStateMap[moduleName] ?? state; + try { + lifecycle.windowStageEventCallback.onAbilityCreate(restoredState); + } catch (_) {} + } } async onWindowStageCreate(windowStage: window.WindowStage): Promise { - this.lifecycle?.windowStageEventCallback.onWindowStageCreate(); - - windowStage.on("windowStageEvent", (event: window.WindowStageEventType) => { - this.lifecycle?.windowStageEventCallback.onWindowStageEvent(event); - }); - - let win = await windowStage.getMainWindow(); - win.on("windowSizeChange", (size: window.Size) => { - this.lifecycle?.windowStageEventCallback.onWindowSizeChange(size); - }); - win.on("windowRectChange", (options: window.RectChangeOptions) => { - this.lifecycle?.windowStageEventCallback.onWindowRectChange(options); - }); - win.on("avoidAreaChange", (options: window.AvoidAreaOptions) => { - this.lifecycle?.windowStageEventCallback.onAvoidAreaChange(options); - }); - win.on("keyboardHeightChange", (height) => { - this.lifecycle?.keyboardEventCallback.onKeyboardHeightChange(height); - }); + this.forEachLifecycle((lifecycle) => lifecycle.windowStageEventCallback.onWindowStageCreate()); + + try { + windowStage.on("windowStageEvent", (event: window.WindowStageEventType) => { + this.forEachLifecycle((lifecycle) => + lifecycle.windowStageEventCallback.onWindowStageEvent(event), + ); + }); + } catch (_) {} + + let win: window.Window | null = null; + try { + win = await windowStage.getMainWindow(); + } catch (_) { + win = null; + } + + if (win) { + try { + win.on("windowSizeChange", (size: window.Size) => { + this.forEachLifecycle((lifecycle) => + lifecycle.windowStageEventCallback.onWindowSizeChange(size), + ); + }); + win.on("windowRectChange", (options: window.RectChangeOptions) => { + this.forEachLifecycle((lifecycle) => + lifecycle.windowStageEventCallback.onWindowRectChange(options), + ); + }); + win.on("avoidAreaChange", (options: window.AvoidAreaOptions) => { + this.forEachLifecycle((lifecycle) => + lifecycle.windowStageEventCallback.onAvoidAreaChange(options), + ); + }); + win.on("keyboardHeightChange", (height) => { + this.forEachLifecycle((lifecycle) => + lifecycle.keyboardEventCallback.onKeyboardHeightChange(height), + ); + }); + } catch (_) {} + } if (this.defaultPage) { - await windowStage.loadContentByName(Entry.RouteName); + try { + await windowStage.loadContentByName(Entry.RouteName); + } catch (_) {} } } onMemoryLevel(level: AbilityConstant.MemoryLevel): void { - this.lifecycle?.environmentCallback.onMemoryLevel(level); + this.forEachLifecycle((lifecycle) => lifecycle.environmentCallback.onMemoryLevel(level)); } onDestroy(): void | Promise { - this.lifecycle?.windowStageEventCallback.onAbilityDestroy(); + this.forEachLifecycle((lifecycle) => lifecycle.windowStageEventCallback.onAbilityDestroy()); } onConfigurationUpdate(newConfig: Configuration): void { - this.lifecycle?.environmentCallback.onConfigurationUpdated(newConfig); + this.forEachLifecycle((lifecycle) => + lifecycle.environmentCallback.onConfigurationUpdated(newConfig), + ); } onSaveState( reason: AbilityConstant.StateType, wantParam: Record, ): AbilityConstant.OnSaveResult { - const ret = this.lifecycle?.windowStageEventCallback.onAbilitySaveState(); - wantParam[STATE_KEY] = ret as string; + const stateMap: Record = {}; + + for (let i = 0; i < this.lifecycles.length; i++) { + const lifecycle = this.lifecycles[i]; + const moduleName = this.loadedModuleNames[i]; + if (!moduleName) { + continue; + } + + try { + stateMap[moduleName] = lifecycle.windowStageEventCallback.onAbilitySaveState() ?? ""; + } catch (_) {} + } + + const stateValues = Object.values(stateMap); + if (stateValues.length === 1) { + wantParam[STATE_KEY] = stateValues[0]; + } else if (stateValues.length > 1) { + wantParam[STATE_KEY] = JSON.stringify(stateMap); + } else { + wantParam[STATE_KEY] = ""; + } + return AbilityConstant.OnSaveResult.RECOVERY_AGREE; } } diff --git a/rust_ability/ability_rust/src/main/ets/ability/type.ets b/rust_ability/ability_rust/src/main/ets/ability/type.ets index b3445f8..4887250 100644 --- a/rust_ability/ability_rust/src/main/ets/ability/type.ets +++ b/rust_ability/ability_rust/src/main/ets/ability/type.ets @@ -61,17 +61,27 @@ export interface WebViewStyle { y?: number | string; } +export interface AbilityInitContext { + basePath?: string; + prefPath?: string; + preferredLocales?: string; + moduleName?: string; +} + export interface Module { - init: () => ApplicationLifecycle; - // XComponent mode + init: (context?: AbilityInitContext) => ApplicationLifecycle; render: (helper: ArkHelper, slot: NodeContent) => void; - - registerCustomProtocol: () => void; + registerCustomProtocol?: () => void; } export interface ArkHelper { exit: (code: number) => void; createWebview: (data: WebViewInitData) => Object; requestPermission: (permission: string | string[]) => Promise; - getWindowAvoidArea: (type: number) => Object | undefined; + getWindowAvoidArea: (type: number) => WindowAvoidAreaInfo | undefined; +} + +export interface WindowAvoidAreaInfo { + type: number; + area: Object; } diff --git a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets index d314cb8..29b8a06 100644 --- a/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets +++ b/rust_ability/ability_rust/src/main/ets/components/DefaultXComponent.ets @@ -1,5 +1,9 @@ import { NodeContent } from "@kit.ArkUI"; -import { ArkHelper, WebViewInitData as NativeWebViewInitData } from "../ability/type"; +import { + ArkHelper, + WebViewInitData as NativeWebViewInitData, + WindowAvoidAreaInfo, +} from "../ability/type"; import { exit } from "../helper"; import { Loadable } from "../helper/loadable"; import { requestPermission } from "../helper/permission"; @@ -12,8 +16,10 @@ import { } from "../webview/DefaultWebview"; export const RouteName = "RustAbility"; + @Component export struct DefaultXComponent { + moduleName: string = ""; private rootSlot = new NodeContent(); private webviewController = new RustWebviewNodeController(this.getUIContext()); private nativeModule: ESObject; @@ -23,7 +29,7 @@ export struct DefaultXComponent { const context = this.getUIContext().getHostContext() as common.UIAbilityContext; return await requestPermission(context, permission); }, - getWindowAvoidArea: (type: number): Object | undefined => { + getWindowAvoidArea: (type: number): WindowAvoidAreaInfo | undefined => { try { const context = this.getUIContext().getHostContext() as common.UIAbilityContext; const win = context.windowStage.getMainWindowSync(); @@ -31,7 +37,7 @@ export struct DefaultXComponent { return { type: avoidType, area: win.getWindowAvoidArea(avoidType), - } as Object; + } as WindowAvoidAreaInfo; } catch (_) { return undefined; } @@ -43,7 +49,6 @@ export struct DefaultXComponent { scriptRules: ["*"], } as ScriptItem; }); - // url set to empty string avoid double load. const init: WebviewInitData = { webTag: data?.id, url: data?.url, @@ -62,7 +67,6 @@ export struct DefaultXComponent { onTitleChange: data?.onTitleChange, } as WebviewInitData; - // transparent only be set when backgroundColor is null. if (data?.transparent && !init.style?.backgroundColor) { init.style!.backgroundColor = Color.Transparent; } @@ -80,15 +84,16 @@ export struct DefaultXComponent { node?.update(init); }; - // Return controller and use controller to control webview behavior return ret.controller; }, }; - @StorageProp("moduleName") name: string = ""; @StorageProp("loadMode") loadMode: "async" | "sync" = "async"; async aboutToAppear(): Promise { - const moduleName = `lib${this.name}.so`; + const moduleName = this.moduleName.trim(); + if (!moduleName) { + throw new Error("DefaultXComponent.moduleName is required"); + } this.nativeModule = await Loadable.load(moduleName, this.loadMode); this.nativeModule.render(this.helper, this.rootSlot); } diff --git a/rust_ability/ability_rust/src/main/ets/components/MainPage.ets b/rust_ability/ability_rust/src/main/ets/components/MainPage.ets index f419152..90cfd87 100644 --- a/rust_ability/ability_rust/src/main/ets/components/MainPage.ets +++ b/rust_ability/ability_rust/src/main/ets/components/MainPage.ets @@ -6,16 +6,22 @@ export const RouteName = "RustAbility"; @Entry({ routeName: RouteName }) @Component struct Index { - @StorageProp("moduleName") name: string = ""; + @StorageProp("moduleName") moduleNames: string | string[] = ""; + @StorageProp("loadMode") loadMode: "async" | "sync" = "async"; private nativeModule: ESObject = null; + @State private primaryModuleName: string = ""; async aboutToAppear(): Promise { - const moduleName = `lib${this.name}.so`; - this.nativeModule = await Loadable.load(moduleName, "async"); + const moduleName = Loadable.resolvePrimaryModuleName(this.moduleNames); + if (!moduleName) { + throw new Error("dynamic moduleName is empty"); + } + + this.nativeModule = await Loadable.load(moduleName, this.loadMode); + this.primaryModuleName = moduleName; } onBackPress(): boolean { - // Call the back press interceptor from Rust if (typeof this.nativeModule?.on_back_press_intercept === "function") { return this.nativeModule.on_back_press_intercept(); } @@ -25,7 +31,9 @@ struct Index { build() { Row() { Column() { - DefaultXComponent() + if (this.primaryModuleName) { + DefaultXComponent({ moduleName: this.primaryModuleName }); + } }.width("100%") }.height("100%"); } diff --git a/rust_ability/ability_rust/src/main/ets/helper/loadable.ets b/rust_ability/ability_rust/src/main/ets/helper/loadable.ets index 40c3037..8b55fa2 100644 --- a/rust_ability/ability_rust/src/main/ets/helper/loadable.ets +++ b/rust_ability/ability_rust/src/main/ets/helper/loadable.ets @@ -1,28 +1,69 @@ import { Module } from "../ability/type"; export class Loadable { - static mod: ESObject; + private static modMap: Record = {}; - static async load(libName: string, mode: "async" | "sync" = "async"): Promise { - if (!!Loadable.mod) { - return Loadable.mod; + static resolveModuleNames(name: string | string[]): string[] { + const names = Array.isArray(name) ? name : [name]; + const resolved: string[] = []; + + for (const item of names) { + const moduleName = item.trim(); + if (!moduleName || resolved.indexOf(moduleName) !== -1) { + continue; + } + if (moduleName.startsWith("lib") || moduleName.endsWith(".so")) { + throw new Error(`moduleName must not include lib/.so wrapper: ${moduleName}`); + } + resolved.push(moduleName); + } + + return resolved; + } + + static resolvePrimaryModuleName(name: string | string[]): string { + const names = Loadable.resolveModuleNames(name); + return names.length > 0 ? names[0] : ""; + } + + static toLibraryName(moduleName: string): string { + return `lib${moduleName}.so`; + } + + static async load(moduleName: string, mode: "async" | "sync" = "async"): Promise { + const resolvedModuleName = Loadable.resolvePrimaryModuleName(moduleName); + const cached = Loadable.modMap[resolvedModuleName]; + if (cached) { + return cached; } + + const libraryName = Loadable.toLibraryName(resolvedModuleName); let currentMod: ESObject; if (mode === "async") { - currentMod = await import(libName); + currentMod = await import(libraryName); } else { - currentMod = loadNativeModule(libName); + try { + currentMod = loadNativeModule(libraryName); + } catch (error) { + throw new Error(`${libraryName} failed to load: ${String(error)}`); + } } - if ( + + let loadedModule: Module | null = null; + if (typeof currentMod?.render === "function" && typeof currentMod?.init === "function") { + loadedModule = currentMod as Module; + } else if ( typeof currentMod?.default?.render === "function" && typeof currentMod?.default?.init === "function" ) { - Loadable.mod = currentMod.default; - return Loadable.mod; - } else if (typeof currentMod?.render === "function" && typeof currentMod?.init === "function") { - Loadable.mod = currentMod; - return Loadable.mod; + loadedModule = currentMod.default as Module; } - throw new Error(`${libName} is not a valid dynamic library`); + + if (!loadedModule) { + throw new Error(`${libraryName} is not a valid dynamic library`); + } + + Loadable.modMap[resolvedModuleName] = loadedModule; + return loadedModule; } } diff --git a/rust_example/webview_example/src/lib.rs b/rust_example/webview_example/src/lib.rs index 1ad1cda..a1bf230 100755 --- a/rust_example/webview_example/src/lib.rs +++ b/rust_example/webview_example/src/lib.rs @@ -17,7 +17,6 @@ thread_local! { const INDEX: &str = include_str!("index.html"); -// test add more napi method #[napi] pub fn handle_change(env: &Env) -> napi_ohos::Result<()> { let web_tag = String::from("webview_example"); @@ -37,7 +36,7 @@ pub fn handle_change(env: &Env) -> napi_ohos::Result<()> { .map_err(|_| napi_ohos::Error::from_reason("custom_protocol error".to_string()))?; let _ = webview.on_controller_attach(move || { - hilog_info!(format!("ohos-rs macro on_controller_attach").as_str()); + hilog_info!("ohos-rs macro on_controller_attach"); let _ = WebProxyBuilder::new(web_tag.clone(), "test".to_string()) .add_method("test", |_web_tag: String, args: Vec| { hilog_info!(format!("ohos-rs macro test: {:?}", args).as_str()); @@ -47,11 +46,11 @@ pub fn handle_change(env: &Env) -> napi_ohos::Result<()> { }); let _ = webview.on_page_begin(|| { - hilog_info!(format!("ohos-rs macro on_page_begin").as_str()); + hilog_info!("ohos-rs macro on_page_begin"); }); let _ = webview.on_page_end(|| { - hilog_info!(format!("ohos-rs macro on_page_end").as_str()); + hilog_info!("ohos-rs macro on_page_end"); }); let ret = unsafe { std::mem::transmute(webview.inner().get_value(env)?) }; @@ -91,18 +90,25 @@ pub fn set_visible(env: &Env, visible: bool) -> napi_ohos::Result<()> { #[ability(webview, protocol = "wry,custom,other")] fn openharmony_app(app: OpenHarmonyApp) { + hilog_info!(format!( + "init context => module={:?}, base={:?}, pref={:?}, locales={:?}", + app.module_name(), + app.base_path(), + app.pref_path(), + app.preferred_locales() + ) + .as_str()); + app.run_loop(|types| match types { Event::Input(k) => match k { InputEvent::ImeEvent(s) => { hilog_info!(format!("ohos-rs macro input_text: {:?}", s).as_str()); } _ => { - hilog_info!(format!("ohos-rs macro input:").as_str()); + hilog_info!("ohos-rs macro input:"); } }, - Event::WindowRedraw(_) => { - // hilog_info!(format!("ohos-rs macro window_redraw").as_str()); - } + Event::WindowRedraw(_) => {} _ => { hilog_info!(format!("ohos-rs macro: {:?}", types.as_str()).as_str()); } diff --git a/rust_example/xcomponent_example/src/lib.rs b/rust_example/xcomponent_example/src/lib.rs index 4f51495..ae04ede 100755 --- a/rust_example/xcomponent_example/src/lib.rs +++ b/rust_example/xcomponent_example/src/lib.rs @@ -55,6 +55,15 @@ pub fn toggle_back_press_intercept() -> bool { #[ability] fn openharmony_app(app: OpenHarmonyApp) { INNER_APP.write().unwrap().replace(app.clone()); + hilog_info!(format!( + "init context => module={:?}, base={:?}, pref={:?}, locales={:?}", + app.module_name(), + app.base_path(), + app.pref_path(), + app.preferred_locales() + ) + .as_str()); + let permission_app = app.clone(); app.on_back_press_intercept(|| { let intercept = BACK_PRESS_INTERCEPT_ENABLED.load(Ordering::SeqCst); @@ -94,11 +103,11 @@ fn openharmony_app(app: OpenHarmonyApp) { hilog_info!(format!("ohos-rs macro input_text: {:?}", s).as_str()); } _ => { - hilog_info!(format!("ohos-rs macro input:").as_str()); + hilog_info!("ohos-rs macro input:"); } }, Event::WindowRedraw(_) => { - hilog_info!(format!("ohos-rs macro window_redraw").as_str()); + hilog_info!("ohos-rs macro window_redraw"); } _ => { hilog_info!(format!("ohos-rs macro: {:?}", types.as_str()).as_str()); diff --git a/webview_example/entry/src/main/cpp/types/libwebview_example/Index.d.ts b/webview_example/entry/src/main/cpp/types/libwebview_example/Index.d.ts index 1bd7769..82094cb 100644 --- a/webview_example/entry/src/main/cpp/types/libwebview_example/Index.d.ts +++ b/webview_example/entry/src/main/cpp/types/libwebview_example/Index.d.ts @@ -6,6 +6,13 @@ export interface ApplicationLifecycle { windowStageEventCallback: WindowStageEventCallback; } +export interface AbilityInitContext { + basePath?: string; + prefPath?: string; + preferredLocales?: string; + moduleName?: string; +} + export interface ArkTSHelper { exit: (arg: number) => void; createWebview: (arg: WebViewInitData) => Object; @@ -46,7 +53,7 @@ export interface WindowStageEventCallback { export declare function handleChange(): void; -export declare function init(): ApplicationLifecycle; +export declare function init(context?: AbilityInitContext): ApplicationLifecycle; export declare function webviewRender(helper: ArkTSHelper): WebViewComponentEventCallback; export declare function setBackgroundColor(color: string): void; diff --git a/webview_example/entry/src/main/ets/pages/Index.ets b/webview_example/entry/src/main/ets/pages/Index.ets index d01510a..5407f91 100644 --- a/webview_example/entry/src/main/ets/pages/Index.ets +++ b/webview_example/entry/src/main/ets/pages/Index.ets @@ -1,6 +1,8 @@ import { DefaultXComponent } from "@ohos-rs/ability"; import { handleChange, setBackgroundColor, setVisible } from "libwebview_example.so"; +const MODULE_NAME = "webview_example"; + @Entry @Component struct Index { @@ -25,7 +27,7 @@ struct Index { this.visible = !this.visible; }) } - DefaultXComponent() + DefaultXComponent({ moduleName: MODULE_NAME }) }.width("100%") }.height("100%"); } diff --git a/xcomponent_example/entry/src/main/cpp/types/libexample/Index.d.ts b/xcomponent_example/entry/src/main/cpp/types/libexample/Index.d.ts index e5ee6bf..ed23a0e 100644 --- a/xcomponent_example/entry/src/main/cpp/types/libexample/Index.d.ts +++ b/xcomponent_example/entry/src/main/cpp/types/libexample/Index.d.ts @@ -7,6 +7,13 @@ export interface ApplicationLifecycle { windowStageEventCallback: WindowStageEventCallback; } +export interface AbilityInitContext { + basePath?: string; + prefPath?: string; + preferredLocales?: string; + moduleName?: string; +} + export interface EnvironmentCallback { onConfigurationUpdated: () => void; onMemoryLevel: () => void; @@ -20,4 +27,5 @@ export interface WindowStageEventCallback { onAbilitySaveState: () => void; } -export declare function init(slot: NodeContent): ApplicationLifecycle; +export declare function init(context?: AbilityInitContext): ApplicationLifecycle; +export declare function render(slot: NodeContent): void; diff --git a/xcomponent_example/entry/src/main/cpp/types/libwgpu_in_app/Index.d.ts b/xcomponent_example/entry/src/main/cpp/types/libwgpu_in_app/Index.d.ts index 646d7a3..dd5de09 100644 --- a/xcomponent_example/entry/src/main/cpp/types/libwgpu_in_app/Index.d.ts +++ b/xcomponent_example/entry/src/main/cpp/types/libwgpu_in_app/Index.d.ts @@ -7,6 +7,13 @@ export interface ApplicationLifecycle { windowStageEventCallback: WindowStageEventCallback; } +export interface AbilityInitContext { + basePath?: string; + prefPath?: string; + preferredLocales?: string; + moduleName?: string; +} + export interface EnvironmentCallback { onConfigurationUpdated: () => void; onMemoryLevel: () => void; @@ -20,6 +27,6 @@ export interface WindowStageEventCallback { onAbilitySaveState: () => void; } -export declare function init(slot: NodeContent): ApplicationLifecycle; +export declare function init(context?: AbilityInitContext): ApplicationLifecycle; export declare function changeRender(index: number): void; diff --git a/xcomponent_example/entry/src/main/ets/pages/Index.ets b/xcomponent_example/entry/src/main/ets/pages/Index.ets index 6fd4df5..7aba8a8 100644 --- a/xcomponent_example/entry/src/main/ets/pages/Index.ets +++ b/xcomponent_example/entry/src/main/ets/pages/Index.ets @@ -4,6 +4,8 @@ import { toggleBackPressIntercept, } from "libxcomponent_example.so"; +const MODULE_NAME = "xcomponent_example"; + @Entry @Component struct Index { @@ -27,7 +29,7 @@ struct Index { Button(this.interceptEnabled ? "back intercept: ON" : "back intercept: OFF") .onClick(() => this.handleToggleBackIntercept()) Text("Swipe back to test intercept behavior.") - DefaultXComponent() + DefaultXComponent({ moduleName: MODULE_NAME }) }.width("100%") }.height("100%"); }