# Uasm

HBuilderX 5.31+ 支持 uni-app x 蒸汽模式中使用 uasm插件

# 背景

webassembly 是浏览器中动态加载二进制库的方案,让js把部分高压力计算交给更高性能的c/c++等语言编译的二进制库来处理。

在小程序上,微信、支付宝、抖音也支持webassembly,但规范略有差异。

app平台,也都有加载二进制库的方案:

  • Android平台:so库
  • iOS平台:framework
  • 鸿蒙平台:so库

每个平台都有使用二进制库的能力,但并没有一套统一的方案。

既然 uni-app x 跨了所有平台,那么能不能统一二进制库的加载方案?

这就是 UniAssembly ,简称 uasm。

让一个c/c++写的库,比如zstd压缩解压库,可以在 uni-app x 的全平台用统一的方式来调用。

# uasm插件

uasm插件,是和uts插件并列的一种新插件。

uts插件uasm同样在uni_modules下,它在专属目录uasm中,和utssdk目录并列。

uasm插件和uts插件都能兼容全平台,但定位不同:

  • uts插件:面向的是os原生能力或原生sdk的封装。每个平台代码都不一样,利用uts能编译成平台原生语言(kotlin、Swift、arkts、js)的能力,把不同平台的原生os api或原生sdk封装给前端页面使用。
  • uasm插件:面向的是本来就跨平台的c、rust等库,它和原生语言无关,也不需要uts,而是js直通二进制。把一个c/c++等库的能力封装给所有平台使用。

当然在理论上,uasm插件,也可以覆盖uts插件的需求,即调用原生能力,但这需要开发者编写c++代码了:

  • 在iOS上,objective-c就是原生语言,有原生所有的API能力。可以用uasm来把系统原生能力封装暴露给js使用。
  • 在Android和鸿蒙上,也有 c api,把这部分ndk能力暴露给js,也可以通过uasm插件。不过这2个平台上c不是原生语言,c api 相对于原生的kotlin和ArkTS的API要少一些。
  • 当然在Android和鸿蒙上,开发者也可以继续使用Android的jni、鸿蒙的node-api,桥接kotlin和ets,把更多原生能力从kotlin、ets转交给c,而后再转交给js,实现原生能力的封装。

uasm插件提供了这样一种能力: 一个二进制库,比如c写的zstd解压库,可以生成所有 uni-app x 支持的平台的胶水层,包括3个App平台、web和小程序平台,并在 uni-app x 的页面中通过统一的api uni.loadUasm() 来调用这个二进制库的能力进行解压。

uasm插件,虽然对js提供了统一的API,但在内部,App和非App的技术方案有差异。

  • 在web和小程序上,是其webassembly方案的封装。可以复用web生态。
  • 在App平台上,是基于Node-API。可以复用Node生态。

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平台库。

# FAQ

  • 一个二进制库通过uts插件混编,也可以封装给前端页面用。和uasm有什么区别?

uts插件封装二进制库,在很多地方不如uasm:

  1. 性能。uasm让js直通二进制,不过原生,性能更高。
  2. 插件包装便利性,符合wasm规范和node-api规范的c库非常多。这些可以方面的对接到uasm中。即使没有封装为这些规范的c库,也可以让ai快速封装。
  3. 插件使用便利性,前端使用uasm,不需要打自定义基座。(除了Windows下使用iOS的库)
  • uasm插件和uts插件,是否可并存于一个uni_modules中?

可以并存于一个uni_modules中。

  • jscore和v8,自身也支持webassembly,和uasm的区别是什么?

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插件使用示例

使用 uasm 插件分为两步:

  1. 安装插件:从插件市场将 uasm 插件导入到项目中。
  2. 加载插件:通过 API 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插件 支持热刷新无需打自定义基座
  • UTS插件 在windows系统上需要自定义基座,macOS支持本地编译

# uasm插件封装教程

插件作者在开发uasm时,注意需要安装原生编译环境。

包括 Android studio的ndk编译环境、xcode、deveco,emscripten(web/微信小程序/支付宝小程序)

然后新建一个uni_modules,选择uasm插件。

# package.json

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等目录。

  • 在web和小程序目录下,放置wasm文件和封装层js
  • 在app-android目录下,按cpu类型目录,存放so库
  • 在app-ios目录下,存放framework
  • 在app-harmony目录下存放so库

下面的结构是 uni-zstd 插件示例,zstd是一种压缩算法,该库是c语言开发的。

  • uni-zstd/uasm/app-* 由uni-gyp编译输出
  • uni-zstd/uasm/mp-*|web 是WebAssembly规范的编译产物,使用emscripten或其他编译工具链生成
  • uni-zstd/utssdk/ 目前需要开发者自行适配对UTS插件的支持,可让AI参考zstd示例适配
.// 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平台需要服务器支持响应头

# 如何封装web和小程序平台的webassembly

  1. 配置编译环境,详情 emscripten
  2. 将编译产物分别拷贝到不同的目录下:
  • 2.1 web : <plugin-id>.js | <plugin-id>.wasm 拷贝到 uni_modules/<plugin-id>/uasm/web
  • 2.2 微信小程序: <plugin-id>.js | <plugin-id>.wasm.br 拷贝到 uni_modules/<plugin-id>/uasm/mp-weixin
  • 2.3 支付宝小程序: <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平台

注意事项:

  • 不支持使用多线程,受小程序的限制
  • 支付宝需要对worker单独封装到(uni_modules/<plugin-id>/workers/mp-alipay)转发到js调用层,受支付宝限制只能在worker中使用,需要真机验证,否者抛异常
  • 支付宝小程序不支持 BigInt,如果编译出的胶水中生成了 0n 语法会出现加载错误,可暂时通过 emscripten 已弃用参数 -s WASM_BIGINT=0 移除该语法
  • 支付宝小程序的worker包装需要使用支付宝小程序自己的worker语法,然后提供异步方法桥接

# 如何封装app平台的uasm

本节以 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
  1. 安装npm包

新建项目目录,在项目根目录执行:

# 初始化
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>
  • UTS插件项目模版uni-gyp create <plugin-id> --template basic-and-uts,

提示:插件id和UTS插件的JNI包名有映射关系 如果调整了名称对应的JNI也需要更新

  1. 开始编译 c/c++ 代码,使用node api/napi

文件名 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)
  1. 编写gyp文件
{
  '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)',
      ]
    }
  ]
}
  1. 接着编写 package.json 文件内容, 用于uni-gyp编译,仅展示需要的配置
{
  "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

# uts插件中使用uasm怎么封装?

前文讲述的是uasm面向js的封装。如果要在uts插件,也就是kotlin/swift中使用,还得继续封装。

# kotlin封装

  1. 在C/C++层通过JNI导出

文件 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());
}
  1. 在Kotlin中调用(UTS插件)
// 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层的包名必须对应

  1. C/C++层代码需要参与gyp编译配置
  • 新增编译文件 jni_binding.cc
  • 新增自定义导出配置 android.exports

// 仅展示需要新增的配置

{
  '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封装

  1. 新增 swift_binding

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)); }
  1. 在swift中调用(UTS插件)
// 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() 时自动加载

  1. C/C++层代码需要参与gyp编译配置
  • 新增编译文件 swift_binding.h swift_binding.cc
  • mac_framework_headers swift_binding.h
  • 新增自定义导出配置 ios.exports

// 仅展示需要新增的配置

{
  '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

# ets封装

  1. 新建文件 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
}
  1. 新建文件 uni_modules/test-uasm/utssdk/app-harmony/index.uts
function testInvoke() {
  try {
    const libTestUasm = uni.loadUasmSync("uni_modules/test-uasm")
    libTestUasm.add(1, 2) // 3
    libTestUasm.concat("uni", "-app") // uni-app
  } catch (e) {
  }
}

# 有现成node封装怎么办,没node封装怎么办。

# 已有node封装

  1. 在.gyp 文件的 targets.conditions 节点新增 android/ios/harmony

示例如下:

{
  '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使用教程

  1. 新增 package.json 命令
{
  "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",
  }
}

# 没有node封装

  1. 新增binding.gyp文件
  2. 编写node api/napi的适配层,让AI参考uni-zstd适配
  3. 可选在package.json中新增编译命令

uni-gyp教程,另见uni-gyp使用教程

如何保障各平台封装的调用一致性?

因node api运行时存在差异,harmony平台的node api运行时由鸿蒙系统提供,虽然使用了node api规范,在参数支持并没有和node api对齐

差异点如下:

  • android/ios 当前的 Buffer 使用了 UInt8Array 替代,缺少Buffer的一些方法

# 模块加载错误排查指南

加载原生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 宿主集成及启动日志

# 1. 加载参数不合法

错误码: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 无法完整读取路径字符串 确认参数正常;持续出现时,向运行时提供方反馈完整日志及复现步骤

# 2. 无法确定模块位置

错误码: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,表示运行时未能提供有效的加载位置。确认传入路径后,联系宿主或运行时提供方排查。

# 3. 动态库加载失败

错误码: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/&lt;plugin-id>/uasm/app-android/libs/arm64-v8a/libUasmBb.so
in namespace classloader-namespace

这表示系统找到了 libUasmBb.so,但没有找到它依赖的 libUasmAa.so。建议依次检查:

  1. 设备上是否实际存在 libUasmAa.so,而不只是开发电脑上有。
  2. libUasmAa.so 是否与应用进程、libUasmBb.so 使用相同 ABI。
  3. 依赖库名称是否正确,所在目录是否能被当前加载环境搜索和访问。

把两个库放在同一目录,不代表依赖一定能被找到。 通过绝对路径加载 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 系统未提供具体原因;保留设备信息、完整启动日志及复现步骤进行反馈

# 4. 模块缺少必要入口

错误码: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 模块封装。

# 5. Node-API 版本不兼容

错误码:ERR_NATIVE_MODULE_VERSION_MISMATCH。

Native module requires an unsupported Node-API version

表示模块声明的 Node-API 版本高于当前运行时支持上限,或者模块返回了无效版本。

使用与当前运行时兼容的模块版本,或在确认支持关系后升级运行时。原生插件开发者应核对构建时的 NAPI_VERSION 和版本入口返回值。此消息不包含双方的具体版本号,需要结合模块及运行时的构建信息确认。

# 6. 模块初始化失败

错误码: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 异常,会保留原异常消息,应按该消息排查模块逻辑。初始化函数没有返回新导出值、且没有异常时,可以继续使用原导出对象,不会仅因此报错。

如果发生原生崩溃,需要查看崩溃堆栈;这类问题不会自动转换成上述错误码。

# 7. 运行环境或模块状态异常

错误码: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 无法读取已加载模块的导出对象 检查运行环境是否已销毁,以及调用时机是否正确

替换动态库后,应完全退出并重新启动应用进程再验证,避免继续使用已加载的旧版本。

# 8. 原生插件开发者常用检查命令

以下命令针对 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