HBuilderX 5.31+ 支持 uni-app x 蒸汽模式中使用 uasm插件
webassembly 是浏览器中动态加载二进制库的方案,让js把部分高压力计算交给更高性能的c/c++等语言编译的二进制库来处理。
在小程序上,微信、支付宝、抖音也支持webassembly,但规范略有差异。
app平台,也都有加载二进制库的方案:
每个平台都有使用二进制库的能力,但并没有一套统一的方案。
既然 uni-app x 跨了所有平台,那么能不能统一二进制库的加载方案?
这就是 UniAssembly ,简称 uasm。
让一个c/c++写的库,比如zstd压缩解压库,可以在 uni-app x 的全平台用统一的方式来调用。
uasm插件,是和uts插件并列的一种新插件。
uts插件uasm同样在uni_modules下,它在专属目录uasm中,和utssdk目录并列。
uasm插件和uts插件都能兼容全平台,但定位不同:
当然在理论上,uasm插件,也可以覆盖uts插件的需求,即调用原生能力,但这需要开发者编写c++代码了:
uasm插件提供了这样一种能力:
一个二进制库,比如c写的zstd解压库,可以生成所有 uni-app x 支持的平台的胶水层,包括3个App平台、web和小程序平台,并在 uni-app x 的页面中通过统一的api uni.loadUasm() 来调用这个二进制库的能力进行解压。
uasm插件,虽然对js提供了统一的API,但在内部,App和非App的技术方案有差异。
uni-app x 蒸汽模式,在3个App平台使用了不同的js引擎,Android的V8、iOS的jscore、鸿蒙的arkTS/jsvm。 但 uni-app x 对外提供了一套统一的 Node-API,屏蔽了各js引擎的差异。
这相当于把nodejs部分引入了手机端。
也就是一个c库,只要有webassembly版本、node版本,就可以直接变成uasm插件,在所有 uni-app x 支持的平台被调用。这大大丰富了uasm插件生态。
如果一个c库没有webassembly,也没有 node 对接封装,那么在AI时代,封装 webassembly 和 node-api 胶水也很简单。
DCloud提供了一些工具,方便二进制库对接为uasm。
首先已经支持webassembly的库,可以直接把胶水层和库放到uasm的web目录下。
对于app平台,DCloud提供了uni-gyp工具,如果一个二进制库已经有 node 对接封装,则可以直接利用该工具生成 uasm 库。
如果一个c库没有webassembly,也没有 node 对接封装,那么在AI时代,封装也很简单。让 AI 把这个c库生成 webassembly 和 node-api 胶水,然后再用 uni-gyp工具 转为 uasm 的 app平台库。
uts插件封装二进制库,在很多地方不如uasm:
可以并存于一个uni_modules中。
js引擎的webassembly能力受限于web规范,功能和性能不如OS原生本来就提供的二进制库调用。 而uasm,是对原生二进制能力的包装,功能性能更优。
c、c++、rust、zig、go、kmp很多语言都可以生成二进制,都可以封装为uasm插件。
sqlite、crypto、cmark、FFmpeg、mqtt、lame、Tesseract、Pandoc、Tree-sitter...很多库都已经有webassembly版本或node版本,这些都可以封装为uasm。
使用 uasm 插件分为两步:
uni.loadUasm() 或 uni.loadUasmSync() 加载插件,获得其导出的方法集。两个 API 的返回类型如下:
| API | 返回类型 | 说明 |
|---|---|---|
uni.loadUasm() | Promise<UserExport> | 异步加载,全平台适用 |
uni.loadUasmSync() | UserExport | 同步加载,仅 App 平台支持 |
其中 UserExport 为插件作者自定义的导出结构,即插件对外暴露的方法集合。
// 已有 uni_modules/hello-uasm
// 同步调用,仅支持 App 平台
function invokeUasmSync(a, b) {
var libHelloUasm = uni.loadUasmSync("uni_modules/hello-uasm")
var result = libHelloUasm.add(1, 2)
console.log('result', result); //输出3
}
// 异步调用,需要跨平台 同时支持 web/微信小程序/支付宝小程序时使用异步风格
async function invokeUasm(a, b) {
try {
var libHelloUasm = await uni.loadUasm("uni_modules/hello-uasm")
var result = libHelloUasm.add(1, 2)
console.log('result', result); //输出3
} catch (e) {
console.log(e);
}
}
// 提示:不用每次都调用 uni.loadUasm/uni.loadUasmSync,当前代码仅做演示
// 上面调用的 add 方法由 hello-uasm 模块提供
真机运行说明:
插件作者在开发uasm时,注意需要安装原生编译环境。
包括 Android studio的ndk编译环境、xcode、deveco,emscripten(web/微信小程序/支付宝小程序)
然后新建一个uni_modules,选择uasm插件。
package.json 为 uni_modules 插件配置清单文件,负责描述插件的基本配置。
{
"id": "uasm-helloworld",
"displayName": "uasm插件名称",
"version": "0.1",
"description": "uasm插件描述",
"uni_modules": {
}
}
在uasm目录中,同样有 web、mp-weixin、app-android、app-ios、app-harmony等目录。
下面的结构是 uni-zstd 插件示例,zstd是一种压缩算法,该库是c语言开发的。
uni-gyp编译输出.// uni-app x 项目根目录
|-- static
|-- uni_modules
| |-- uni-zstd // 插件目录名
| | |-- uasm // 包含app平台的编译产物so/xcframework; 小程序/web平台的wasm|js文件
| | | |-- app-android // Android平台
| | | | |-- libs
| | | | | |-- arm64-v8a // arm64架构
| | | | | | |-- libUasmUniZstd.so // elf格式so库
| | | | | |-- x86_64 // x86_64架构
| | | | | | |-- libUasmUniZstd.so // elf格式so库
| | | |-- app-harmony // HarmonyOS平台
| | | | |-- libs
| | | | | |-- arm64-v8a // arm64架构
| | | | | | |-- libUasmUniZstd.so // elf格式so库
| | | | | |-- x86_64 // x86_64架构
| | | | | | |-- libUasmUniZstd.so // elf格式so库
| | | |-- app-ios // iOS平台
| | | | |-- frameworks
| | | | | |-- UasmUniZstd.xcframework // 不参与主项目的链接,首次使用时通过dlopen加载,不支持卸载否者可能导致无法预知的意外
| | | | | | |-- ios-arm64 // arm64真机
| | | | | | | |-- UasmUniZstd.framework
| | | | | | | | |-- UasmUniZstd // mach-o格式动态库
| | | | | | | | |-- Headers // 头文件目录
| | | | | | | | | |-- swift_binding.h // 通过dlopen加载后和swift绑定
| | | | | | | | | |-- uni-zstd.h // 引用swift_binding.h
| | | | | | | | |-- Info.plist
| | | | | | |-- ios-arm64_x86_64-simulator // 同时支持arm/x86架构模拟器
| | | | | | | |-- UasmUniZstd.framework
| | | | | | | | |-- UasmUniZstd // mach-o格式动态库(肥胖型)
| | | | | | | | |-- Headers // 头文件目录
| | | | | | | | | |-- swift_binding.h // 通过dlopen加载后和swift绑定
| | | | | | | | | |-- uni-zstd.h // 引用swift_binding.h
| | | | | | | | |-- Info.plist
| | | | | | |-- Info.plist
| | | |-- mp-alipay // 支付宝小程序目录,目录下的.wasm|.wasm.br只能使用其中一个
| | | | |-- uni-zstd.js // 支付宝小程序限制必须在worker中使用,创建Worker使用异步API风格拉齐差异
| | | | |-- uni-zstd.wasm // wasm文件
| | | | |-- uni-zstd.wasm.br // Brotli-compressed格式
| | | |-- mp-weixin // 微信小程序目录,目录下的.wasm|.wasm.br只能使用其中一个
| | | | |-- uni-zstd.js // 胶水绑定文件
| | | | |-- uni-zstd.wasm // wasm文件
| | | | |-- uni-zstd.wasm.br // Brotli-compressed格式
| | | |-- web // Web平台目录,目录下的.wasm|.wasm.br只能使用其中一个
| | | | |-- uni-zstd.js // 胶水绑定文件
| | | | |-- uni-zstd.wasm // wasm文件
| | | | |-- uni-zstd.wasm.br // Brotli-compressed格式
| | | |-- index.d.ts // 仅用于IDE语法提示
| | |-- utssdk // 支持在UTS插件中使用时需要该目录
| | | |-- app-android
| | | | |-- index.uts // 包装UniZstd.kt并导出
| | | | |-- UniZstd.kt // 通过jni和so绑定
| | | |-- app-ios
| | | | |-- index.uts // 包装UniZstd.swift并导出
| | | | |-- UniZstd.swift // 和xcframework绑定
| | |-- workers // uni-app worker目录规范
| | | |-- mp-alipay
| | | | |-- uni-zstd.js // wasm编译出的胶水文件,在uni-zstd-worker-index中调用
| | | | |-- uni-zstd-worker-index.js // worker入口
| | |-- package.json // 插件配置
提示: .wasm.br中的 .br 为 Brotli-compressed 格式和wasm没有直接关系,web平台需要服务器支持响应头
<plugin-id>.js | <plugin-id>.wasm 拷贝到 uni_modules/<plugin-id>/uasm/web<plugin-id>.js | <plugin-id>.wasm.br 拷贝到 uni_modules/<plugin-id>/uasm/mp-weixin<plugin-id>.js 文件拷贝到 uni_modules/<plugin-id>/workers/mp-alipay,<plugin-id>.wasm.br 文件拷贝到 uni_modules/<plugin-id>/uasm/mp-alipay注意:推荐在小程序上使用 wasm.br 减少文件大小,web可以由服务器配置gzip压缩
web平台编译参数说明
-s MODULARIZE=1 模块导出 因为需要异步加载等待wasm文件就绪-s EXPORT_ES6=1 使用ES6-s ENVIRONMENT=web 仅需支持web平台小程序编译参数说明
-s DYNAMIC_EXECUTION=0 禁用JS动态行为 因微信小程序不支持如 new Function-s MODULARIZE=1 模块导出 因为需要异步加载等待wasm文件-s EXPORT_ES6=1 使用ES6-s ENVIRONMENT=web 仅需支持web平台注意事项:
uni_modules/<plugin-id>/workers/mp-alipay)转发到js调用层,受支付宝限制只能在worker中使用,需要真机验证,否者抛异常BigInt,如果编译出的胶水中生成了 0n 语法会出现加载错误,可暂时通过 emscripten 已弃用参数 -s WASM_BIGINT=0 移除该语法本节以 double 相加和 string 拼接两个最简单的方法为例,完整演示 App 平台 uasm 插件的封装过程。
示例模块名为 test-uasm。
JS 层调用示例如下:
const uasm = uni.loadUasmSync("uni_modules/test-uasm");
uasm.add(1, 2); // 3
uasm.concat('uni', '-app'); // uni-app
新建项目目录,在项目根目录执行:
# 初始化
npm init
# 安装uni-gyp
pnpm add -D @dcloudio/uni-gyp
# or
npm install --save-dev @dcloudio/uni-gyp
# 可选napi风格,添加node-addon-api
npm i node-addon-api -save
# 可选桌面测试,添加node-gyp
npm i node-gyp -save
执行后会生成 package.json
uni-gyp 提供如下项目模版:
uni-gyp create <plugin-id>uni-gyp create <plugin-id> --template basic-and-uts,提示:插件id和UTS插件的JNI包名有映射关系 如果调整了名称对应的JNI也需要更新
文件名 binding.cc
#include <cmath>
#include <cstring>
#include <cstdint>
#include <limits>
#include <memory>
#include <new>
#include <string>
#include <node_api.h>
double add(double a, double b) {
return a + b;
}
napi_value Init(napi_env env, napi_value exports) {
napi_status status;
napi_value fn;
status = napi_create_function(env, nullptr, 0, [](napi_env env, napi_callback_info info) -> napi_value {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
double a, b;
napi_get_value_double(env, args[0], &a);
napi_get_value_double(env, args[1], &b);
napi_value result;
napi_create_double(env, add(a, b), &result);
return result;
}, nullptr, &fn);
status = napi_set_named_property(env, exports, "add", fn);
napi_value concat_fn;
status = napi_create_function(env, nullptr, 0, [](napi_env env, napi_callback_info info) -> napi_value {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
if (argc < 2) {
napi_throw_type_error(env, nullptr, "concat expects two strings");
return nullptr;
}
napi_valuetype type_a, type_b;
napi_typeof(env, args[0], &type_a);
napi_typeof(env, args[1], &type_b);
if (type_a != napi_string || type_b != napi_string) {
napi_throw_type_error(env, nullptr, "concat expects two strings");
return nullptr;
}
size_t length_a, length_b;
napi_get_value_string_utf8(env, args[0], nullptr, 0, &length_a);
napi_get_value_string_utf8(env, args[1], nullptr, 0, &length_b);
// Read both values directly into one output buffer to avoid three
// intermediate string allocations and the final c_str() scan.
std::string result(length_a + length_b + 1, '\0');
size_t actual_a = 0;
size_t actual_b = 0;
napi_get_value_string_utf8(env, args[0], result.data(), length_a + 1, &actual_a);
napi_get_value_string_utf8(env, args[1], result.data() + actual_a, length_b + 1, &actual_b);
napi_value value;
napi_create_string_utf8(env, result.data(), actual_a + actual_b, &value);
return value;
}, nullptr, &concat_fn);
status = napi_set_named_property(env, exports, "concat", concat_fn);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
{
'variables': {
"uni-gyp-root-dir%": "<!(node -p \"require('uni-gyp').root_dir\")",
'module-root-dir%': '<!(node -p "require(\'path\').resolve(\'.\')")',
'test-sources': [
'binding.cc',
],
},
'targets': [
{
'target_name': 'UasmTestUasm',
'type': 'shared_library',
"cflags_cc": [ "-std=c++20", "-fno-exceptions" ],
'conditions': [
['OS == "android"', {
'ldflags': [
'-Wl,--unresolved-symbols=ignore-all',
# The app supplies libc++_shared.so from its default native lib
# directory; prevent the toolchain from adding libc++ statically.
'-nostdlib++',
],
'libraries': [
'-lc++_shared',
],
}],
['OS == "ios"', {
'mac_bundle': 1,
'xcode_settings': {
'PRODUCT_BUNDLE_IDENTIFIER': 'uts.sdk.modules.testUasm',
'CURRENT_PROJECT_VERSION': '1',
'MARKETING_VERSION': '1.0.0',
},
'ldflags': [
'-Wl,-undefined,dynamic_lookup',
],
}],
['OS == "harmony"', {
'libraries': [
'-lace_napi.z',
],
"ldflags": [
"-Wl,--unresolved-symbols=ignore-all",
],
}],
],
'sources': [
'<@(test-sources)',
]
}
]
}
{
"name": "test-uasm",
"version": "1.0.0",
"description": "test-uasm-plugin",
"gypfile": true,
"scripts": {
"build:test-uasm:android:release": "uni-gyp -platform=android --depth=. -arch=arm64 --build=Release binding.gyp && uni-gyp -platform=android --depth=. -arch=x64 --build=Release binding.gyp",
"build:test-uasm:harmony:release": "uni-gyp -platform=harmony --depth=. -arch=arm64 --build=Release binding.gyp && uni-gyp -platform=harmony --depth=. -arch=x64 --build=Release binding.gyp",
"build:test-uasm:ios:release": "uni-gyp -platform=ios --depth=. -sdk=all -arch=arm64,x64 --build=Release binding.gyp",
"build:test-uasm:pack:uni-module:release": "uni-gyp module-pack --target test-uasm",
"build:test-uasm:all": "npm run build:test-uasm:android:release && npm run build:test-uasm:harmony:release && npm run build:test-uasm:ios:release && npm run build:test-uasm:pack:uni-module:release",
}
}
一个命令生成 uni-module
// 在项目的根目录下执行,生成目录为 `uni_modules/test-uasm`
npm run build:test-uasm:all
将生成的 test-uasm 目录拷贝到 uni-app x 项目的 uni_modules/ 目录下
注意:上面的命令包含编译iOS的xcframework,仅在macOS上支持
最终编写 uni-app x 项目下的 uni_modules/test-uasm/uasm/index.d.ts 类型声明文件。
该文件仅用于 IDE 的语法提示,声明 uni.loadUasm() / uni.loadUasmSync() 返回的导出结构。封装 App 平台时,可让 AI 参考 C/C++ 导出代码自动生成。
interface TestUasm {
add(a: number, b: number): number
concat(a: string, b: string): string
}
declare const testUasm: TestUasm
export = testUasm
前文讲述的是uasm面向js的封装。如果要在uts插件,也就是kotlin/swift中使用,还得继续封装。
文件 jni_binding.cc
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jdouble JNICALL
Java_uts_sdk_modules_testUasm_TestUasm_nativeAdd(JNIEnv *, jobject, jdouble a, jdouble b) {
return a + b;
}
extern "C" JNIEXPORT jstring JNICALL
Java_uts_sdk_modules_testUasm_TestUasm_nativeConcat(JNIEnv *env, jobject, jstring a, jstring b) {
if (!a || !b) return nullptr;
const char *a_chars = env->GetStringUTFChars(a, nullptr);
const char *b_chars = env->GetStringUTFChars(b, nullptr);
if (!a_chars || !b_chars) {
if (a_chars) env->ReleaseStringUTFChars(a, a_chars);
if (b_chars) env->ReleaseStringUTFChars(b, b_chars);
return nullptr;
}
std::string result(a_chars);
result += b_chars;
env->ReleaseStringUTFChars(a, a_chars);
env->ReleaseStringUTFChars(b, b_chars);
return env->NewStringUTF(result.c_str());
}
// uni_modules/test-uasm/utssdk/app-android/TestUasm.kt
package uts.sdk.modules.testUasm
private object TestUasmLibrary {
init {
// 不需要手动加载编译出的so库,当用户调用 uni.loadUasm()/uni.loadUasmSync() 时自动加载
// 在开发期间 .so 库的位置不在android默认的lib目录下
// System.loadLibrary("UasmTestUasm")
}
fun load() = Unit
}
// uni.loadUasm()` / `uni.loadUasmSync() 返回类型 模块名的驼峰格式,首字母大写
object TestUasm {
// init {
// TestUasmLibrary.load()
// }
fun add(a: Double, b: Double): Double = nativeAdd(a, b)
fun concat(a: String, b: String): String = nativeConcat(a, b)
// 对应C/C++层的方法
private external fun nativeAdd(a: Double, b: Double): Double
private external fun nativeConcat(a: String, b: String): String
}
注意:C层的JNI命名和Kotlin层的包名必须对应
// 仅展示需要新增的配置
{
'targets': [
{
'conditions': [
['OS == "android"', {
'sources': [
'jni_binding.cc',
],
'ldflags': [
'-Wl,--version-script=<(module-root-dir)/android.exports',
],
}],
]
}
]
}
android.exports文件内容
{
global:
napi_register_module_v1;
node_api_module_get_api_version_v1;
Java_uts_sdk_modules_testUasm_TestUasm_nativeAdd;
Java_uts_sdk_modules_testUasm_TestUasm_nativeConcat;
local:
*;
};
swift_binding.h
#ifndef UASM_TEST_SWIFT_BINDING_H_
#define UASM_TEST_SWIFT_BINDING_H_
#ifdef __cplusplus
extern "C" {
#endif
#if defined(__GNUC__)
#define UASM_TEST_SWIFT_API __attribute__((visibility("default")))
#else
#define UASM_TEST_SWIFT_API
#endif
UASM_TEST_SWIFT_API double uasm_test_add(double a, double b);
UASM_TEST_SWIFT_API const char *uasm_test_concat(const char *a, const char *b);
UASM_TEST_SWIFT_API void uasm_test_free_string(const char *value);
#ifdef __cplusplus
}
#endif
#endif
swift_binding.cc
#include "swift_binding.h"
#include <cstdlib>
#include <cstring>
double uasm_test_add(double a, double b) { return a + b; }
const char *uasm_test_concat(const char *a, const char *b) {
if (!a || !b) return nullptr;
const size_t a_size = std::strlen(a), b_size = std::strlen(b);
char *result = static_cast<char *>(std::malloc(a_size + b_size + 1));
if (!result) return nullptr;
std::memcpy(result, a, a_size);
std::memcpy(result + a_size, b, b_size + 1);
return result;
}
void uasm_test_free_string(const char *value) { std::free(const_cast<char *>(value)); }
// uni_modules/test-uasm/utssdk/app-ios/TestUasm.swift
import Darwin
import Foundation
private typealias AddFunction = @convention(c) (Double, Double) -> Double
private typealias ConcatFunction = @convention(c) (UnsafePointer<CChar>?, UnsafePointer<CChar>?) -> UnsafePointer<CChar>?
private typealias FreeStringFunction = @convention(c) (UnsafePointer<CChar>?) -> Void
private final class TestUasmAPI {
let add: AddFunction
let concat: ConcatFunction
let freeString: FreeStringFunction
private let handle: UnsafeMutableRawPointer
private init(handle: UnsafeMutableRawPointer) throws {
self.handle = handle
self.add = try Self.symbol("uasm_test_add", from: handle, as: AddFunction.self)
self.concat = try Self.symbol("uasm_test_concat", from: handle, as: ConcatFunction.self)
self.freeString = try Self.symbol("uasm_test_free_string", from: handle, as: FreeStringFunction.self)
}
static let shared: TestUasmAPI = {
let path = Bundle.main.privateFrameworksURL?.appendingPathComponent("UasmTestUasm.framework/UasmTestUasm").path ?? ""
guard let handle = dlopen(path, RTLD_LAZY | RTLD_LOCAL) else { fatalError("unable to load UasmTestUasm.framework") }
do { return try TestUasmAPI(handle: handle) } catch { fatalError("unable to load UasmTestUasm symbols") }
}()
private static func symbol<T>(_ name: String, from handle: UnsafeMutableRawPointer, as type: T.Type) throws -> T {
guard let value = dlsym(handle, name) else { throw NSError(domain: "TestUasm", code: 1) }
return unsafeBitCast(value, to: type)
}
}
// uni.loadUasm()` / `uni.loadUasmSync() 返回类型 模块名的驼峰格式,首字母大写
public enum TestUasm {
public static func add(_ a: Double, _ b: Double) -> Double {
TestUasmAPI.shared.add(a, b)
}
public static func concat(_ a: String, _ b: String) -> String {
a.utf8CString.withUnsafeBufferPointer { aBuffer in
b.utf8CString.withUnsafeBufferPointer { bBuffer in
guard let value = TestUasmAPI.shared.concat(aBuffer.baseAddress, bBuffer.baseAddress) else {
return a + b
}
defer { TestUasmAPI.shared.freeString(value) }
return String(cString: value)
}
}
}
}
xcframework 不参与主程序的链接,当用户调用 uni.loadUasm() / uni.loadUasmSync() 时自动加载
// 仅展示需要新增的配置
{
'targets': [
{
'conditions': [
['OS == "ios"', {
'sources': [
'swift_binding.cc',
'swift_binding.h',
],
'mac_framework_headers': [
'swift_binding.h',
],
'ldflags': [
'-Wl,-exported_symbols_list,<(module-root-dir)/ios.exports',
],
}],
]
}
]
}
ios.exports文件内容
_napi_register_module_v1
_node_api_module_get_api_version_v1
_uasm_test_add
_uasm_test_concat
_uasm_test_free_string
uni_modules/test-uasm/utssdk/app-harmony/TestUasm.ets// 调用 uni.loadUasmSync("uni_modules/test-uasm") 后返回该类型
export interface TestUasm {
add(a: number, b: number): number
concat(a: string, b: string): string
}
uni_modules/test-uasm/utssdk/app-harmony/index.utsfunction testInvoke() {
try {
const libTestUasm = uni.loadUasmSync("uni_modules/test-uasm")
libTestUasm.add(1, 2) // 3
libTestUasm.concat("uni", "-app") // uni-app
} catch (e) {
}
}
示例如下:
{
'variables': {
"uni-gyp-root-dir%": "<!(node -p \"require('uni-gyp').root_dir\")",
'module-root-dir%': '<!(node -p "require(\'path\').resolve(\'.\')")',
},
'targets': [
{
'conditions': [
['OS == "android"', {
'ldflags': [
'-Wl,--unresolved-symbols=ignore-all',
# The app supplies libc++_shared.so from its default native lib
# directory; prevent the toolchain from adding libc++ statically.
'-nostdlib++',
],
'libraries': [
'-lc++_shared',
],
}],
['OS == "ios"', {
'mac_bundle': 1,
'xcode_settings': {
'PRODUCT_BUNDLE_IDENTIFIER': 'uts.sdk.modules.testUasm',
'CURRENT_PROJECT_VERSION': '1',
'MARKETING_VERSION': '1.0.0',
},
'ldflags': [
'-Wl,-undefined,dynamic_lookup',
'-Wl,-exported_symbols_list,<(module-root-dir)/ios.exports',
],
}],
['OS == "harmony"', {
'libraries': [
'-lace_napi.z',
],
"ldflags": [
"-Wl,--unresolved-symbols=ignore-all",
"-Wl,--version-script=<(uni-gyp-root-dir)/harmony.exports",
],
}],
]
}
]
}
平台自定义导出见uni-gyp使用教程
{
"scripts": {
"build:test-uasm:android:release": "uni-gyp -platform=android --depth=. -arch=arm64 --build=Release binding.gyp && uni-gyp -platform=android --depth=. -arch=x64 --build=Release binding.gyp",
"build:test-uasm:harmony:release": "uni-gyp -platform=harmony --depth=. -arch=arm64 --build=Release binding.gyp && uni-gyp -platform=harmony --depth=. -arch=x64 --build=Release binding.gyp",
"build:test-uasm:ios:release": "uni-gyp -platform=ios --depth=. -ios_sdk=all -arch=arm64,x64 --build=Release binding.gyp",
"build:test-uasm:pack:uni-module:release": "uni-gyp module-pack --target test-uasm",
"build:test-uasm:all": "npm run build:test-uasm:android:release && npm run build:test-uasm:harmony:release && npm run build:test-uasm:ios:release && npm run build:test-uasm:pack:uni-module:release",
}
}
uni-gyp教程,另见uni-gyp使用教程
如何保障各平台封装的调用一致性?
因node api运行时存在差异,harmony平台的node api运行时由鸿蒙系统提供,虽然使用了node api规范,在参数支持并没有和node api对齐
差异点如下:
加载原生Uasm模块失败时,先查看错误码,再根据完整错误消息定位原因。本文列出加载过程中的固定错误和常见系统错误,供应用开发者、原生插件开发者及宿主集成开发者排查。
部分错误消息后会附带系统返回的详细原因,例如:
Failed to open native module: dlopen failed: library "libUasmXXX.so" not found
其中 Failed to open native module 表示动态库加载失败,冒号后面的内容用于判断具体原因。请保留完整日志,避免只截取错误前缀。
| 错误码 | 含义 | 优先检查 |
|---|---|---|
ERR_NATIVE_MODULE_INVALID_ARGUMENT | 加载参数不合法 | 是否传入有效的路径字符串 |
ERR_NATIVE_MODULE_RESOLVE_FAILED | 无法确定模块的加载位置 | 模块名称、路径及应用内的打包位置 |
ERR_NATIVE_MODULE_LOAD_FAILED | 动态库加载失败 | 库文件、依赖库、架构及系统详细错误 |
ERR_NATIVE_MODULE_ENTRY_MISSING | 模块缺少必要入口 | 是否使用兼容的原生模块产物 |
ERR_NATIVE_MODULE_VERSION_MISMATCH | 模块要求的 Node-API 版本不受支持 | 模块与运行时的版本兼容性 |
ERR_NATIVE_MODULE_INIT_FAILED | 模块初始化失败 | 模块初始化逻辑及运行环境 |
ERR_NATIVE_MODULE_STATE | 加载环境或模块状态异常 | 运行时生命周期及调用时机 |
ERR_NATIVE_MODULE_INSTALL_FAILED | 原生模块加载能力初始化失败 | Harmony 宿主集成及启动日志 |
错误码:ERR_NATIVE_MODULE_INVALID_ARGUMENT。这类错误表现为 TypeError。
| 错误消息 | 含义 | 处理办法 |
|---|---|---|
Native module path must be provided | 未提供模块路径,或路径参数读取失败 | 检查调用处是否传入第一个参数 |
Native module path must be a string | 模块路径不是字符串,或无法识别参数类型 | 传入字符串,避免传入 undefined、null、对象或数字 |
Native module path is invalid | 路径为空、超过 4096 个 UTF-8 字节、包含空字符,或无法读取长度 | 检查路径实际内容;长度按字节计算 |
Failed to read native module path | 无法完整读取路径字符串 | 确认参数正常;持续出现时,向运行时提供方反馈完整日志及复现步骤 |
错误码:ERR_NATIVE_MODULE_RESOLVE_FAILED。
常见前缀为 Failed to resolve native module,后面可能附带以下详细原因:
| 详细原因 | 含义 | 处理办法 |
|---|---|---|
Android native module path is invalid | Android 模块路径无效 | 检查路径是否为空或包含空字符 |
Harmony native module path is invalid | Harmony 模块路径无效 | 检查路径是否为空或包含空字符 |
iOS native module path is invalid | iOS 模块路径无效 | 检查路径是否为空或包含空字符 |
iOS native module path is not valid UTF-8 | iOS 模块路径编码无效 | 使用有效的 UTF-8 路径字符串 |
Native module is not present in the application bundle | 未在指定位置找到 iOS 模块文件 | 确认模块已嵌入最终 App,并检查名称及路径 |
如果消息为 Native module resolver returned an invalid result,表示运行时未能提供有效的加载位置。确认传入路径后,联系宿主或运行时提供方排查。
错误码:ERR_NATIVE_MODULE_LOAD_FAILED。
错误前缀为 Failed to open native module。此时应重点查看后面的系统详细错误。Android 常见情况如下,具体文本可能因系统版本而不同。
| 系统错误关键词 | 含义 | 处理办法 |
|---|---|---|
目标库 not found | 找不到要加载的库 | 检查设备上的实际文件路径,以及打包、复制或解压结果 |
依赖库 not found,并带有 needed by | 目标库依赖的其他库未找到 | 补齐依赖库,检查依赖名称、架构和搜索路径 |
not accessible,并带有 namespace | 库对当前加载环境不可访问 | 检查库所在目录及宿主加载配置;必要时联系宿主提供方 |
cannot locate symbol | 缺少模块需要的函数或变量 | 检查依赖版本、符号导出及运行时兼容性 |
32 / 64 位或 e_machine 不匹配 | 库与当前应用进程架构不一致 | 使用匹配的 ABI,并检查所有依赖库 |
| ELF magic、header、section 或 segment 错误 | 文件损坏、不是有效动态库,或构建产物不符合系统要求 | 重新构建并核对传输后的文件 |
| 权限、打开或映射失败 | 系统无法访问或映射库文件 | 根据完整日志检查文件位置、权限及系统限制 |
Android 原生库使用 ELF 格式,arm64-v8a 对应 64 位 ARM。应检查文件本身的架构,不能只看目录名称。Android ABI 文档
Failed to open native module: dlopen failed: library "libUasmAa.so" not found
needed by uni_modules/<plugin-id>/uasm/app-android/libs/arm64-v8a/libUasmBb.so
in namespace classloader-namespace
这表示系统找到了 libUasmBb.so,但没有找到它依赖的 libUasmAa.so。建议依次检查:
libUasmAa.so,而不只是开发电脑上有。libUasmAa.so 是否与应用进程、libUasmBb.so 使用相同 ABI。把两个库放在同一目录,不代表依赖一定能被找到。 通过绝对路径加载 libUasmBb.so,也不会自动把该目录加入依赖搜索路径。classloader-namespace 只是本次查找所在环境的名称,单凭它不能判断是命名空间配置错误。
对于自定义目录,可由原生插件或宿主开发者检查依赖搜索配置。Android 从 API 24 起支持通过 DT_RUNPATH 中的 $ORIGIN 指向库自身目录;需要兼容更低版本时,应另行处理依赖加载。仅修改 RTLD_LOCAL / RTLD_GLOBAL 不能解决文件缺失或目录未被搜索的问题。Android 动态链接器说明
| 消息 | 含义与处理办法 |
|---|---|
Dynamic library name is invalid | 加载名称无效;确认路径非空且不包含空字符 |
Dynamic library symbol name is invalid | 查询的入口名称无效;联系原生插件或运行时提供方 |
Unknown dynamic linker error | 系统未提供具体原因;保留设备信息、完整启动日志及复现步骤进行反馈 |
错误码:ERR_NATIVE_MODULE_ENTRY_MISSING。
| 错误消息前缀 | 含义 | 处理办法 |
|---|---|---|
Native module API version entry is missing | 缺少版本入口 node_api_module_get_api_version_v1 | 使用包含该导出入口的模块产物 |
Native module initializer is missing | 缺少初始化入口 napi_register_module_v1 | 使用包含该导出入口的模块产物 |
原生插件开发者应确认两个入口名称准确、使用 C linkage,并对外导出。只编译第三方 C/C++ 库,通常还需要添加 Node-API 模块封装。
错误码:ERR_NATIVE_MODULE_VERSION_MISMATCH。
Native module requires an unsupported Node-API version
表示模块声明的 Node-API 版本高于当前运行时支持上限,或者模块返回了无效版本。
使用与当前运行时兼容的模块版本,或在确认支持关系后升级运行时。原生插件开发者应核对构建时的 NAPI_VERSION 和版本入口返回值。此消息不包含双方的具体版本号,需要结合模块及运行时的构建信息确认。
错误码:ERR_NATIVE_MODULE_INIT_FAILED。
| 错误消息 | 含义 | 处理办法 |
|---|---|---|
Failed to create native module exports | 无法创建模块导出对象 | 检查运行环境是否正常,并查看同一时间的原生日志 |
Failed to retain native module exports | 无法保存模块导出对象 | 检查运行时状态;持续出现时联系运行时提供方 |
Failed to inspect native module initialization | 无法确认模块初始化后的异常状态 | 检查模块初始化逻辑是否正确使用 Node-API |
Failed to retain initialized native module exports | 无法保存模块初始化后返回的导出值 | 原生插件开发者应检查初始化函数返回值是否符合要求 |
如果模块初始化时自己抛出了 JS 异常,会保留原异常消息,应按该消息排查模块逻辑。初始化函数没有返回新导出值、且没有异常时,可以继续使用原导出对象,不会仅因此报错。
如果发生原生崩溃,需要查看崩溃堆栈;这类问题不会自动转换成上述错误码。
错误码:ERR_NATIVE_MODULE_STATE。
| 错误消息 | 含义 | 处理办法 |
|---|---|---|
Native module loader is not available | 当前环境无法提供模块加载能力 | 确认运行时已初始化,且尚未销毁 |
Native module loader callback is invalid | 加载调用与当前运行环境不匹配 | 检查是否跨环境使用加载函数,或在环境销毁后继续调用 |
Native module cache is invalid | 已加载模块的缓存状态异常 | 重启应用验证;持续出现时向运行时提供方反馈 |
Failed to read cached native module exports | 无法读取已加载模块的导出对象 | 检查运行环境是否已销毁,以及调用时机是否正确 |
替换动态库后,应完全退出并重新启动应用进程再验证,避免继续使用已加载的旧版本。
以下命令针对 Android 动态库。llvm-readelf、llvm-nm 使用 Android NDK 提供的工具,将示例文件名替换为实际文件名。
# 检查目标库和依赖库的真实架构
llvm-readelf -h libUasmXyz.so
llvm-readelf -h libUasmAbc.so
# 检查依赖名称(NEEDED)、库名称(SONAME)及搜索路径(RUNPATH)
llvm-readelf -d libUasmXyz.so
llvm-readelf -d libUasmAbc.so
# 检查是否导出了两个必要的 Node-API 入口
llvm-nm -D --defined-only libUasmXyz.so
# 检查需要由其他库提供的符号
llvm-nm -D --undefined-only libUasmXyz.so
# 检查最终 APK 中是否包含目标库及依赖库 (开发期间不在lib目录下)
unzip -l app.apk