# uni-gyp

# 介绍

uni-gyp 是一个跨平台编译工具,用于把一个C/C++语言编写的库,编译成uasm插件的App平台版本。

uni-gyp 基于 gyp-next,扩展了Android、iOS 和 HarmonyOS 的 CMake 工程生成及编译能力。 这三个目标平台的编译不依赖 node-gyp;node-gyp 仅用于对比 Node.js 在桌面环境中的构建差异和执行相关测试。

# 环境要求

# 通用环境

  • Node.js 22 或更高版本(用于运行 uni-gyp)
  • Python 3.9 或更高版本
  • CMake 3.22 或更高版本(编译到iOS平台时需要),Android 和 HarmonyOS 由 Android NDK/HarmonyOS NDK 提供
  • Ninja(Android 和 HarmonyOS 构建需要 NDK自带)

以上要求适用于 Android、HarmonyOS 和 iOS 平台编译,不要求安装或使用node-gyp。只有执行桌面 Node.js 对比测试时才会使用项目中的 node-gyp; 当前版本支持 Node.js 22.22.2+、24.15.0+ 或 26+。

可以先检查本机环境:

node --version
python3 --version
cmake --version
ninja --version

Windows 上如果没有 python3 命令,可改用:

py -3 --version

# macOS

  • Android:只需安装 Android NDK,不要求安装 Android Studio 或完整的Android SDK;也可以通过 Android Studio 的 SDK Manager 获取 NDK。
  • HarmonyOS:只需安装 Command Line Tools 不要求安装 DevEco Studio;也可以通过 DevEco Studio 的 SDK Manager 获取 NDK。
  • iOS:只能在 macOS 上构建,需要完整安装 Xcode;同时确保 cmake 命令位于 PATH 中。
  • 本机安装 Xcode Command Line Tools:
  xcode-select --install

# Windows

  • Android:只需安装 Android NDK,不要求安装 Android Studio 或完整的Android SDK;也可以通过 Android Studio 的 SDK Manager 获取 NDK。
  • HarmonyOS:只需安装 Command Line Tools 不要求安装 DevEco Studio;也可以通过 DevEco Studio 的 SDK Manager 获取。
  • Android 和 HarmonyOS:确保 cmake.exe 和 ninja.exe 所在目录已加入PATH。
  • Windows 不支持 iOS 编译;iOS 编译命令必须在 macOS 上执行。

# 配置环境变量

# Android

只需将 ANDROID_NDK_HOME 指向 Android NDK 目录即可编译:

  • ANDROID_NDK_HOME:具体 NDK 版本目录,目录中应存在build/cmake/android.toolchain.cmake。

如果没有配置 ANDROID_NDK_HOME,uni-gyp 也会尝试通过 ANDROID_SDK_ROOT、ANDROID_HOME 或 adb 自动查找已安装的最新 NDK。 ANDROID_SDK_ROOT 仅用于自动查找 NDK,并不是必需的。

macOS:

export ANDROID_NDK_HOME="/path/to/android-ndk"

如需永久生效,将以上内容写入 ~/.zshrc,然后执行:

source ~/.zshrc

Windows PowerShell(仅对当前终端生效):

$env:ANDROID_NDK_HOME = "C:\path\to\android-ndk"
$env:Path = "C:\Program Files\CMake\bin;C:\path\to\ninja;$env:Path"

如需永久生效,在 Windows 的“编辑用户环境变量”中添加ANDROID_NDK_HOME,并将 CMake 和 Ninja 的目录添加到用户 Path,然后重新打开终端。

# HarmonyOS

推荐配置 OHOS_NDK_HOME。它可以指向以下任一目录:

  • SDK 版本目录;
  • 版本目录下的 native 目录;
  • DevEco Studio 中与 native 同级的 toolchains 目录。

最终解析出的 Native SDK 目录中必须存在build/cmake/ohos.toolchain.cmake。

macOS:

export OHOS_NDK_HOME="/path/to/openharmony/native"

同样可将配置写入 ~/.zshrc 以永久生效。

Windows PowerShell:

$env:OHOS_NDK_HOME = "C:\path\to\openharmony\native"

Windows 上如需永久生效,在“编辑用户环境变量”中添加 OHOS_NDK_HOME。项目也兼容 HARMONY_NDK_HOME、OHOS_SDK_NATIVE 和 HARMONY_SDK_HOME。

# Python

uni-gyp 通常会自动查找 Python。如果本机安装了多个 Python,或自动查找失败,可通过 UNI_GYP_PYTHON 指定 Python 3.9 及以上版本:

macOS:

export UNI_GYP_PYTHON="/path/to/python3"

Windows PowerShell:

$env:UNI_GYP_PYTHON = "C:\path\to\python.exe"

# 安装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

或全局安装 uni-gyp 创建项目

# 基础模版
uni-gyp create <plugin-id>

# 包含UTS插件模版
uni-gyp create <plugin-id> --template basic-and-uts

# 执行编译

# 编译命令

命令参数:

命令/参数 值 描述
-platform android / ios / harmony 生成对应平台的 CMake 工程
-arch arm64 / x64 指定 CPU 架构,默认 arm64, iOS 支持多个架构用逗号分隔(模拟器)
--deployment-target 15.0 指定 iOS 最小支持版本
--build Debug / Release 执行构建
module-pack --target <plugin-id> 生成 uni-module,插件名由 --target 指定
  • 默认 arch 为 arm64。
  • 默认最低 Android API 为 23。
  • 默认最低 iOS 系统版本为 iOS 15.0。
  • 单次 uni-gyp 编译仅支持一个 CPU 架构,iOS 除外(模拟器支持多架构合并)。

示例:

# 只生成 build/<platform>/<abi>/CMakeLists.txt
uni-gyp -platform=android --depth=. source/binding.gyp

# 生成 + 执行编译(Release模式)
uni-gyp -platform=android --depth=. --build=Release source/binding.gyp

# 指定 CPU 架构(Android 示例,x64 同理)
uni-gyp -platform=android --depth=. -arch=arm64 --build=Release source/binding.gyp

输出目录结构规则:

build/android/&lt;abi>/&lt;configuration>/lib.target/lib&lt;UasmProduct>.so

build/harmony/&lt;arch>/&lt;configuration>/lib.target/lib&lt;UasmProduct>.so

build/ios/xcframework/&lt;configuration>/&lt;UasmProduct>.xcframework

# 生成 uni-module

// 生成目录为 `uni_modules/<plugin-id>`
uni-gyp module-pack --target <plugin-id>
  • plugin-id 为插件目录名即插件id

输入/输出结构规则

build/android/&lt;abi>/&lt;configuration>/lib.target/lib&lt;UasmProduct>.so
  -> uni_modules/&lt;target>/uasm/app-android/libs/&lt;abi>/lib&lt;UasmProduct>.so

build/harmony/&lt;arch>/&lt;configuration>/lib.target/lib&lt;UasmProduct>.so
  -> uni_modules/&lt;target>/uasm/app-harmony/libs/&lt;arch>/lib&lt;UasmProduct>.so

build/ios/xcframework/&lt;configuration>/&lt;UasmProduct>.xcframework
  -> uni_modules/&lt;target>/uasm/app-ios/frameworks/&lt;UasmProduct>.xcframework

# 项目示例

本示例使用zstd演示,zstd是一个C语言编写的压缩解压库。

项目源码gitcode仓库https://gitcode.com/dcloud/uasm-zstd

  • uni-module 名称为 uni-zstd
  • 支持UTS插件时需要实现 JNI/Swift 绑定
. 项目根目录
|-- zstd
|   |-- lib               // zstd C源码目录
|   |-- binding.gyp       // gyp 编译配置
|   |-- android.exports   // gyp 编译配置 --> 自定义平台导出符号配置
|   |-- ios.exports       // gyp 编译配置 --> 自定义平台导出符号配置
|   |-- zstd_codec.h      // cxx 通用封装
|   |-- zstd_codec.cc     // cxx 通用封装
|   |-- jni_binding.cc    // JNI 绑定
|   |-- Zstd.kt           // JNI 绑定
|   |-- zstd.h            // swift 绑定
|   |-- swift_binding.h   // swift 绑定
|   |-- swift_binding.cc  // swift 绑定
|   |-- Zstd.swift        // swift 绑定
|   |-- wasm_binding.cc   // WebAssembly 绑定
|-- package.json

zstd/binding.gyp 文件内容

{
  'variables': {
    "uni-gyp-root-dir%": "<!(node -p \"require('uni-gyp').root_dir\")",
    'module-root-dir%': '<!(node -p "require(\'path\').resolve(\'.\')")',
    'zstd_sources': [
      # cxx_library(name='debug')
      'lib/common/debug.c',

      # cxx_library(name='bitstream')
      # [no .c files]

      # cxx_library(name='cpu')
      # [no .c files]

      # cxx_library(name='entropy')
      'lib/common/entropy_common.c',
      'lib/common/fse_decompress.c',
      'lib/compress/fse_compress.c',
      'lib/compress/huf_compress.c',
      'lib/decompress/huf_decompress.c',

      # cxx_library(name='pool')
      'lib/common/pool.c',

      # cxx_library(name='threading')
      'lib/common/threading.c',

      # cxx_library(name='xxhash')
      'lib/common/xxhash.c',

      # cxx_library(name='zstd_common')
      'lib/common/zstd_common.c',

      # cxx_library(name='errors')
      'lib/common/error_private.c',

      # cxx_library(name='mem')
      # [no .c files]

      # cxx_library(name='compiler')
      # [no .c files]

      # cxx_library(name='compress')
      'lib/compress/hist.c',
      # glob(compress/zstd*.c)
      'lib/compress/zstd_compress.c',
      'lib/compress/zstd_compress_literals.c',
      'lib/compress/zstd_compress_sequences.c',
      'lib/compress/zstd_compress_superblock.c',
      'lib/compress/zstd_double_fast.c',
      'lib/compress/zstd_fast.c',
      'lib/compress/zstd_lazy.c',
      'lib/compress/zstd_ldm.c',
      'lib/compress/zstd_opt.c',
      'lib/compress/zstd_preSplit.c',
      'lib/compress/zstdmt_compress.c',

      # cxx_library(name='decompress')
      # glob(decompress/zstd*.c)
      'lib/decompress/zstd_ddict.c',
      'lib/decompress/zstd_decompress.c',
      'lib/decompress/zstd_decompress_block.c',

      'zstd_codec.cc',
      'binding.cc',
    ],
  },
  'targets': [
    {
      'target_name': 'UasmUniZstd',
      'type': 'shared_library',
      'include_dirs': ['lib'],
      # -pthread?
      'direct_dependent_settings': {
        'include_dirs': [ 'lib' ]
      },
      'defines': [
        # cxx_library(name='xxhash')
        'XXH_NAMESPACE=ZSTD_',
        # cxx_library(name='threading')
        'ZSTD_MULTITHREAD',
        # TODO: Use deps/zstd/lib/decompress/huf_decompress_amd64.S.
        'ZSTD_DISABLE_ASM',
      ],
      'all_dependent_settings': {
        'defines': [
          'XXH_NAMESPACE=ZSTD_',
          'ZSTD_MULTITHREAD',
          # TODO: Use deps/zstd/lib/decompress/huf_decompress_amd64.S.
          'ZSTD_DISABLE_ASM',
        ],
      },
      "include_dirs": [
        "<!@(node -p \"require('node-addon-api').include\")",
      ],
      "cflags_cc": [ "-std=c++20", "-fno-exceptions" ],
      'conditions': [
        ['OS == "android"', {
          'defines': [
            'GYP_ANDROID=1',
          ],
          'sources': [
            'jni_binding.cc',
          ],
          'ldflags': [
            '-Wl,--unresolved-symbols=ignore-all',
            '-Wl,-exported_symbols_list,<(module-root-dir)/android.exports',
          ],
        }],
        ['OS == "ios"', {
          'type': 'shared_library',
          'mac_bundle': 1,
          'sources': [
            'swift_binding.cc',
            'swift_binding.h',
            'zstd.h',
          ],
          'mac_framework_headers': [
            'swift_binding.h',
            'zstd.h',
          ],
          'defines': [
            'GYP_IOS=1',
          ],
          'xcode_settings': {
            'PRODUCT_BUNDLE_IDENTIFIER': 'uts.sdk.modules.uniZstd',
          },
          'ldflags': [
            '-Wl,-undefined,dynamic_lookup',
            '-Wl,-exported_symbols_list,<(module-root-dir)/ios.exports',
          ],
        }],
        ['OS == "harmony"', {
          "type": "shared_library",
          "defines": [
            "GYP_HARMONY=1",
          ],
          'libraries': [
            '-lace_napi.z',
          ],
          "ldflags": [
            "-Wl,--unresolved-symbols=ignore-all",
            "-Wl,--version-script=<(module-root-dir)/harmony.exports",
          ],
        }],
      ],
      'sources': [
        '<@(zstd_sources)',
      ]
    }
  ]
}

package.json 文件内容, 仅展示需要的配置

{
  "name": "uni-zstd",
  "version": "1.0.0",
  "description": "uni-zstd-plugin",
  "gypfile": true,
  "scripts": {
    "build:uni-zstd:android:release": "uni-gyp -platform=android --depth=. -arch=arm64 --build=Release zstd/binding.gyp && uni-gyp -platform=android --depth=. -arch=x64 --build=Release zstd/binding.gyp",
    "build:uni-zstd:harmony:release": "uni-gyp -platform=harmony --depth=. -arch=arm64 --build=Release zstd/binding.gyp && uni-gyp -platform=harmony --depth=. -arch=x64 --build=Release zstd/binding.gyp",
    "build:uni-zstd:ios:release": "uni-gyp -platform=ios --depth=. -sdk=all -arch=arm64,x64 --build Release zstd/binding.gyp",
    "build:uni-zstd:pack:uni-module:release": "uni-gyp module-pack --target uni-zstd",
    "build:uni-zstd:all": "npm run build:uni-zstd:android:release && npm run build:uni-zstd:harmony:release && npm run build:uni-zstd:ios:release && npm run build:uni-zstd:pack:uni-module:release",
  },
  "dependencies": {
  }
}

一个命令生成 uni-module

// 生成目录为 `uni_modules/uni-zstd`
npm run build:uni-zstd:all

注意:上面的命令包含编译iOS的xcframework,仅在macOS上支持

# 自定义导出说明

# ios.exports 为iOS平台自定义导出符号配置

# android.exports 为Android平台自定义导出符号配置

  • 当前uni-zstd 演示中由主应用提供c/c++的运行库(在默认lib目录下),根据项目的实际环境决定链接行为

# harmony.exports 为HarmonyOS平台自定义导出符号配置

  • 需要链接系统库 ace_napi.z, node api运行时

# 产物排查

# 查看导出符号(符号可见性)

默认情况下,编译器会把所有符号都导出到动态符号表,这既会增加产物体积,也可能与非预期的同名符号发生冲突。 通过 android.exports、harmony.exports 的 version-script 以及 ios.exports 的导出白名单,可以只保留需要对外暴露的符号。

如果怀疑某个符号没有导出、或者导出了不该导出的符号,可以按下述方式检查。

# Android

借助 NDK 自带的 LLVM 工具,工具目录一般为 $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/<host-tag>/bin/(<host-tag> 如 darwin-x86_64、linux-x86_64、windows-x86_64)。

# 仅列出 so 的动态符号表中已定义的导出符号(-D 表示动态符号表,--defined-only 排除未定义引用)
llvm-nm -D --defined-only lib<UasmProduct>.so

# 更详细地查看动态符号表,包含符号类型、绑定方式、可见性
llvm-readelf -sW --dyn-syms lib<UasmProduct>.so

# 查看链接的动态库依赖
llvm-readelf -d lib<UasmProduct>.so

# 查看符号版本信息(version-script 生效后会生成版本节点)
llvm-readelf --version-info lib<UasmProduct>.so
  • 动态符号表中存在、但不应导出的符号:检查 android.exports 是否把该符号放进了 global:,其余符号应通过 local: *; 全部隐藏。
  • 期望导出却没有出现在动态符号表:确认符号被正确使用,且源文件以 -fvisibility=hidden 编译时使用了 __attribute__((visibility("default")));也可以检查是否被 local: 规则误隐藏。
  • 从静态库引入的符号(如 libc++)默认也会被导出,可在 ldflags 中追加 -Wl,--exclude-libs,ALL 隐藏。

# HarmonyOS

工具位于 OpenHarmony NDK 的 llvm/bin/ 下,命令与 Android 基本一致:

$OHOS_NDK_HOME/llvm/bin/llvm-nm -D --defined-only lib<UasmProduct>.so
$OHOS_NDK_HOME/llvm/bin/llvm-readelf -sW --dyn-syms lib<UasmProduct>.so
$OHOS_NDK_HOME/llvm/bin/llvm-readelf --version-info lib<UasmProduct>.so
  • 通过 -Wl,--version-script=<...>/harmony.exports 控制导出;如需在项目内自定义,可参考 HarmonyOS 的 .exports 配置方式。

# iOS

xcframework 由多个架构目录组成,分析前先确认目标架构对应的二进制路径:

# 查看 xcframework 包含的架构
ls <UasmProduct>.xcframework

# 查看指定架构二进制的架构信息
lipo -info <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>

# 列出导出的全局已定义符号,并去掉 Mach-O 的符号前导下划线
nm -gU <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>

# 仅查看导出符号(exports trie,即真正对外可见的符号)
xcrun dyld_info -exports <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>

# 查看动态库依赖
otool -L <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>
  • xcframework 不参与主程序链接,由 uni.loadUasm() / uni.loadUasmSync() 通过 dlopen 加载,因此对外可见的符号必须出现在 ios.exports 白名单中,否则运行期 dlsym 会取不到符号。
  • ios.exports 中的符号名带前导下划线(如 _uasm_test_add),对应 C 函数 uasm_test_add。
  • 检查是否误导出内部符号:对比 nm -gU 的输出与 ios.exports,多出的符号应加入隐藏或收窄编译选项 -fvisibility=hidden。

# 体积过大的排查与优化

按平台查看产物各段/各架构的体积构成,定位是代码、调试信息还是多架构合并导致的。

# 分析体积构成

Android / HarmonyOS:

# 按节区统计体积(text/data/bss 及调试节)
llvm-size -A lib<UasmProduct>.so

# 检查是否残留调试信息
llvm-readelf -S lib<UasmProduct>.so | grep -i debug

# 按符号统计体积,找出占用最大的符号(前 20 个)
llvm-nm -D -S --size-sort lib<UasmProduct>.so | tail -n 20

iOS:

# 按段/节统计大小
size -m <UasmProduct>.framework/<UasmProduct>

# 按符号统计体积(降序)
nm -S --size-sort -gU <UasmProduct>.framework/<UasmProduct>

# 查看 xcframework 内各架构二进制的体积
du -sh <UasmProduct>.xcframework/*

# 查看各架构 / 静态库体积
lipo -info <UasmProduct>.xcframework/ios-arm64/<UasmProduct>.framework/<UasmProduct>

# 常见原因与处理

  • 未剥离调试信息:Release 产物应移除调试符号。Android/HarmonyOS 可在 ldflags 中追加 -Wl,--strip-debug,或使用 llvm-strip --strip-unneeded lib<UasmProduct>.so;iOS 由 Xcode 的 STRIP_INSTALLED_PRODUCT / DEPLOYMENT_POSTPROCESSING 控制,也可手动 strip -x。
  • 导出了过多符号:默认导出全部符号会显著增加动态符号表和哈希表体积,同时影响加载性能。按上文用 version-script / ios.exports 收窄到最小集合,并配合 -Wl,--exclude-libs,ALL 隐藏静态库符号。
  • 未做死代码消除:编译时开启 -ffunction-sections -fdata-sections,链接时开启 -Wl,--gc-sections(Android/HarmonyOS)或 -Wl,-dead_strip(iOS),可剔除未被引用的函数与数据。
  • 默认可见性为 default:源文件统一以 -fvisibility=hidden -fvisibility-inlines-hidden 编译,仅对需要导出的接口加 __attribute__((visibility("default")))。
  • 静态链接了完整运行库:确认是否把 libc++_shared、libc++abi 等静态链接进产物;Android 可参考示例用 -nostdlib++ 并由 App 提供运行库。
  • 多架构重复打包:xcframework 会聚合 ios-arm64、ios-arm64_x86_64-simulator 等目录,最终包体会成倍增大。发布前确认只保留需要的架构(如仅真机包时移除模拟器切片)。
  • 重复包含同一静态库:检查 binding.gyp 的 sources/libraries 是否重复引入相同源码或库文件。
  • 开启 LTO:在确认稳定后可开启链接时优化(Android/HarmonyOS -flto,iOS LLVM_LTO),通常能同时减小体积并提升性能。
  • 资源与多余头文件:mac_framework_headers、resources 等若把非必要文件打进产物,也会增加包体,需按需裁剪。