# 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',
          },
          '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',
          },
          '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的一些方法