📋 目录

  1. 移植概述
  2. 移植方式选择
  3. AKI方式移植(推荐)
  4. Node-API方式移植
  5. 构建配置
  6. 实际案例:HMAC-SHA256库移植
  7. 最佳实践
  8. 常见问题

移植概述

什么是C/C++三方库移植?

C/C++三方库移植是指将现有的C/C++库适配到HarmonyOS平台,使其能够在ArkTS应用中调用。移植的主要目的是:

  • 性能优化: C/C++库通常比JS/TS库运行效率更高
  • 功能复用: 利用成熟的C/C++库,避免重复开发
  • 生态扩展: 丰富HarmonyOS的第三方库生态

移植的基本流程

1. 库分析 → 2. 移植方式选择 → 3. 接口设计 → 4. 代码实现 → 5. 构建配置 → 6. 测试验证

移植方式选择

两种主要方式对比

特性 AKI方式 Node-API方式
代码复杂度
开发效率
学习成本
性能 优秀 优秀
维护成本
推荐度 ⭐⭐⭐⭐⭐ ⭐⭐⭐

选择建议

  • 推荐AKI方式: 适用于大多数场景,开发效率高
  • Node-API方式: 适用于需要精确控制或特殊需求的场景

🚀 AKI方式移植(推荐)

什么是AKI?

AKI (Alpha Kernel Interacting) 是一款边界性编程体验友好的ArkTS FFI开发框架,提供极简语法糖使用方式,一行代码完成ArkTS与C/C++的无障碍跨语言互调。

AKI的优势

  1. 极简使用: 解耦FFI代码与业务代码,友好的边界性编程体验
  2. 完整特性: 提供完整的数据类型转换、函数绑定、对象绑定、线程安全等特性
  3. 所见即所得: 一行代码完成ArkTS与C/C++的无障碍跨语言互调
  4. 无需关心底层: 开发者无需关心Node-API的线程安全问题、Native对象GC问题

AKI是一款专为鸿蒙原生开发设计的FFI(外部函数接口)开发框架。它极大地简化了JS与C/C++之间的跨语言访问,为开发者提供了一种边界性编程体验友好的解决方案。通过AKI,开发者可以使用让代码更易读的语法糖,实现JS与C/C++之间的无障碍跨语言互调,真正做到所“键”即所得。

这一创新框架的出现,正是为了解决开发者在迁移C/C++项目到HarmonyOS NEXT时面临的核心痛点。传统的NAPI接口调用复杂,学习成本高,开发者需要耗费大量精力进行适配和迁移。AKI通过封装复杂的NAPI接口,让开发者无需直接接触繁琐的跨语言调用技术细节,这一设计不仅能有效减少跨语言调用接口90%的代码量,还能将跨语言调用接口和业务代码完全解耦,帮助开发者更加专注于产品创新与功能迭代,而非技术迁移的细节问题,大幅提升开发效率。

据悉,在涉及C/C++/ETS跨越语言调用的鸿蒙化应用中,有超过80%的项目都在使用AKI,如某知名购物应用,使用后减少了项目10%代码量;某知名社交电商平台使用后减少了50%以上跨语言调用接口代码量;某图像处理软件所有C++代码复用通过AKI来实现。使用AKI后这些项目不仅减少了项目代码量,还显著优化了代码复用与迁移流程。

OHPM仓AKI直达地址:https://ohpm.openharmony.cn/#/cn/detail/@ohos/aki@ohos%2Faki

在这里插入图片描述

类型映射关系

在这里插入图片描述

AKI移植步骤

1. 环境准备

# 确保开发环境满足要求
- HarmonyOS SDK 4.0+
- Node.js 16+
- CMake 3.4.1+

2. 安装AKI依赖

# 进入项目entry目录
cd entry

# 安装AKI依赖
ohpm install @ohos/aki

3. 创建C++包装器

// example_aki.cpp
#include <aki/jsbind.h>
#include <aki/array_buffer.h>
#include "your_library.h"  // 你的C/C++库头文件
#include <string>
#include <vector>

// 用户自定义业务函数
std::string YourFunction(const std::string& input) {
    // 调用你的C/C++库函数
    return your_library_function(input.c_str());
}

// 支持ArrayBuffer的函数
aki::ArrayBuffer YourBufferFunction(const aki::ArrayBuffer& input) {
    // 处理二进制数据
    std::vector<uint8_t> result = process_buffer(input.GetData(), input.GetLength());
    return aki::ArrayBuffer(result.data(), result.size());
}

// 注册AKI插件
JSBIND_ADDON(your_library_aki)

// 注册全局函数
JSBIND_GLOBAL() {
    JSBIND_FUNCTION(YourFunction);
    JSBIND_FUNCTION(YourBufferFunction);
}

4. 创建CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)
project(your_library_aki)

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 设置AKI根路径
set(AKI_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../../../oh_modules/@ohos/aki)
set(CMAKE_MODULE_PATH ${AKI_ROOT_PATH})
find_package(Aki REQUIRED)

# 源文件
set(SOURCES
    your_library.c          # 你的C/C++库源文件
    your_library_wrapper.c  # 包装器源文件
    example_aki.cpp         # AKI包装器
)

# 创建共享库
add_library(your_library_aki SHARED ${SOURCES})

# 链接AKI库
target_link_libraries(your_library_aki PUBLIC Aki::libjsbind)

# 设置输出目录
set_target_properties(your_library_aki PROPERTIES
    LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../lib
    RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/../lib
)

5. 创建TypeScript类型定义

// types/libyour_library_aki/index.d.ts

/**
 * 你的库函数
 * @param input - 输入字符串
 * @returns 处理结果
 */
export function YourFunction(input: string): string;

/**
 * 处理ArrayBuffer的函数
 * @param input - 输入ArrayBuffer
 * @returns 处理结果ArrayBuffer
 */
export function YourBufferFunction(input: ArrayBuffer): ArrayBuffer;

6. 在ArkTS中使用

// 在你的ArkTS文件中
import aki from 'libyour_library_aki.so'

@Entry
@Component
struct MyPage {
  @State result: string = '';

  build() {
    Column() {
      Button('调用C++库')
        .onClick(() => {
          // 一行代码调用C++函数!
          this.result = aki.YourFunction('Hello from ArkTS!');
        })
      
      Text(this.result)
    }
  }
}

🔧 Node-API方式移植

Node-API方式特点

HarmonyOS Node-API是基于Node.js 12.x LTS的Node-API规范扩展开发的机制,为开发者提供了ArkTS/JS与C/C++模块之间的交互能力。它提供了一组稳定的、跨平台的API,可以在不同的操作系统上使用。

  • 精确控制: 可以精确控制每个细节
  • 灵活性高: 支持复杂的参数处理和错误处理
  • 学习成本高: 需要深入理解Node-API机制

开发者使用NAPI过程中还会发现:为了做跨线程任务,需要做线程管理,需要关心环境上下文;为了使用结构体对象,需要关注napi_value生命周期如何管理;巴拉巴拉等等与自己业务无关的逻辑。搞了半天,发现业务代码一行没写,还在写NAPI的跨语言调用实现。拥有洁癖的开发者还会发现,很难做到隔离NAPI代码与业务代码,我们讨厌 毫无边界性的编程。

在这里插入图片描述

Node-API移植步骤

1. 创建Native C++工程

在DevEco Studio中New > Create Project,选择Native C++模板,创建新工程。

2. 设置模块注册信息

// entry/src/main/cpp/napi_init.cpp

// 准备模块加载相关信息
static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "entry",  // 模块名称,对应so库名称
    .nm_priv = ((void*)0),
    .reserved = {0},
};

// 加载so时,该函数会自动被调用,将模块注册到系统中
extern "C" __attribute__((constructor)) void RegisterDemoModule() { 
    napi_module_register(&demoModule);
}

3. 模块初始化

// entry/src/main/cpp/napi_init.cpp
EXTERN_C_START
// 模块初始化
static napi_value Init(napi_env env, napi_value exports) {
    // ArkTS接口与C++接口的绑定和映射
    napi_property_descriptor desc[] = {
        {"yourFunction", nullptr, YourFunction, nullptr, nullptr, nullptr, napi_default, nullptr},
        {"yourBufferFunction", nullptr, YourBufferFunction, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    // 在exports对象上挂载Native方法
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

4. 实现Native侧函数

// entry/src/main/cpp/napi_init.cpp
static napi_value YourFunction(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    // 获取传入的参数
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 参数验证
    if (argc < 1) {
        napi_throw_error(env, nullptr, "需要至少1个参数");
        return nullptr;
    }

    // 获取字符串参数
    char input[256];
    size_t input_len;
    napi_get_value_string_utf8(env, args[0], input, sizeof(input), &input_len);

    // 调用你的C/C++库函数
    std::string result = your_library_function(input);

    // 返回结果
    napi_value result_value;
    napi_create_string_utf8(env, result.c_str(), NAPI_AUTO_LENGTH, &result_value);
    return result_value;
}

static napi_value YourBufferFunction(napi_env env, napi_callback_info info)
{
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    // 获取传入的参数
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 获取ArrayBuffer参数
    void* buffer_data;
    size_t buffer_length;
    napi_get_arraybuffer_info(env, args[0], &buffer_data, &buffer_length);

    // 处理二进制数据
    std::vector<uint8_t> result = process_buffer(buffer_data, buffer_length);

    // 返回ArrayBuffer结果
    napi_value result_buffer;
    napi_create_arraybuffer(env, result.size(), &buffer_data, &result_buffer);
    memcpy(buffer_data, result.data(), result.size());

    return result_buffer;
}

5. 创建TypeScript类型定义

// entry/src/main/cpp/types/libentry/index.d.ts
export const yourFunction: (input: string) => string;
export const yourBufferFunction: (input: ArrayBuffer) => ArrayBuffer;

6. 配置oh-package.json5

// entry/src/main/cpp/types/libentry/oh-package.json5
{
  "name": "libentry.so",
  "types": "./index.d.ts",
  "version": "",
  "description": "Please describe the basic information."
}

7. 配置CMakeLists.txt

# entry/src/main/cpp/CMakeLists.txt
cmake_minimum_required(VERSION 3.4.1)
project(YourLibrary)

set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

include_directories(${NATIVERENDER_ROOT_PATH}
                    ${NATIVERENDER_ROOT_PATH}/include)

# 添加名为entry的库
add_library(entry SHARED napi_init.cpp your_library.c)

# 构建此可执行文件需要链接的库
target_link_libraries(entry PUBLIC libace_napi.z.so)

8. 在ArkTS中使用

// entry/src/main/ets/pages/Index.ets
// 通过import的方式,引入Native能力
import nativeModule from 'libentry.so'

@Entry
@Component
struct Index {
  @State result: string = '';

  build() {
    Row() {
      Column() {
        Text('调用C++库')
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick(() => {
            this.result = nativeModule.yourFunction('Hello from ArkTS!');
          })
        
        Text(this.result)
          .fontSize(30)
      }
      .width('100%')
    }
    .height('100%')
  }
}

Node-API的约束限制

SO命名规则

导入使用的模块名和注册时的模块名大小写保持一致,如模块名为entry,则so的名字为libentry.so,napi_module中nm_modname字段应为entry,ArkTS侧使用时写作:import xxx from 'libentry.so'。

注册建议

  • nm_register_func对应的函数需要加上static,防止与其他so里的符号冲突
  • 模块注册的入口函数名需要确保不与其它模块重复

多线程限制

  • Node-API接口只能在JS线程使用
  • Native接口入参env与特定JS线程绑定只能在创建时的线程使用
  • 使用Node-API接口创建的数据需在env完全销毁前进行释放,避免内存泄漏

🏗️ 构建配置

项目结构

your_library_harmonyos/
├── CMakeLists.txt                    # CMake构建配置
├── your_library.c                    # 原始C/C++库源文件
├── your_library.h                    # 原始C/C++库头文件
├── example_aki.cpp                   # AKI包装器(推荐)
├── napi_init.cpp                     # Node-API包装器
├── package.json                      # 包配置
├── types/
│   └── libyour_library_aki/
│       └── index.d.ts                # TypeScript类型定义
├── examples/
│   └── arkts_example.ets             # ArkTS使用示例
└── README.md                         # 说明文档

package.json配置

{
  "name": "your-library-harmonyos",
  "version": "1.0.0",
  "description": "Your C/C++ library for HarmonyOS",
  "main": "lib/index.js",
  "types": "types/libyour_library_aki/index.d.ts",
  "scripts": {
    "build": "cmake -B build && cmake --build build",
    "clean": "rm -rf build lib",
    "test": "node test/test.js"
  },
  "keywords": [
    "harmonyos",
    "arkts",
    "aki",
    "napi"
  ],
  "dependencies": {
    "@ohos/aki": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^18.0.0"
  }
}

📚 实际案例:HMAC-SHA256库移植

原始仓库地址https://github.com/h5p9sl/hmac_sha256

原始C库分析

// hmac_sha256.h
size_t hmac_sha256(
    const void* key, const size_t keylen,
    const void* data, const size_t datalen,
    void* out, const size_t outlen
);

AKI方式实现

// hmac_sha256_aki.cpp
#include <aki/jsbind.h>
#include <aki/array_buffer.h>
#include "../hmac_sha256.h"
#include <string>
#include <vector>

// 字符串输入版本
std::vector<uint8_t> HmacSha256Hash(const std::string& key, const std::string& data) {
    std::vector<uint8_t> output(32);
    size_t result_len = hmac_sha256(key.c_str(), key.length(), data.c_str(), data.length(), output.data(), output.size());
    output.resize(result_len);
    return output;
}

// ArrayBuffer输入版本
aki::ArrayBuffer HmacSha256HashBuffer(const aki::ArrayBuffer& key, const aki::ArrayBuffer& data) {
    std::vector<uint8_t> output(32);
    size_t result_len = hmac_sha256(key.GetData(), key.GetLength(), data.GetData(), data.GetLength(), output.data(), output.size());
    aki::ArrayBuffer result(output.data(), result_len);
    return result;
}

// 十六进制输出版本
std::string HmacSha256Hex(const std::string& key, const std::string& data) {
    auto hash = HmacSha256Hash(key, data);
    aki::ArrayBuffer buffer(hash.data(), hash.size());
    return ArrayBufferToHex(buffer);
}

// 注册AKI插件
JSBIND_ADDON(hmac_sha256_aki)

// 注册全局函数
JSBIND_GLOBAL() {
    JSBIND_FUNCTION(HmacSha256Hash);
    JSBIND_FUNCTION(HmacSha256HashBuffer);
    JSBIND_FUNCTION(HmacSha256Hex);
}

Node-API方式实现

// napi_init.cpp
#include <napi.h>
#include "../hmac_sha256.h"
#include <string>
#include <vector>

static napi_value HmacSha256Hash(napi_env env, napi_callback_info info)
{
    size_t argc = 2;
    napi_value args[2] = {nullptr};

    // 获取参数
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 获取key和data字符串
    char key[256], data[1024];
    size_t key_len, data_len;
    napi_get_value_string_utf8(env, args[0], key, sizeof(key), &key_len);
    napi_get_value_string_utf8(env, args[1], data, sizeof(data), &data_len);

    // 计算HMAC-SHA256
    std::vector<uint8_t> output(32);
    size_t result_len = hmac_sha256(key, key_len, data, data_len, output.data(), output.size());

    // 返回Buffer
    void* buffer_data;
    napi_value result_buffer;
    napi_create_arraybuffer(env, result_len, &buffer_data, &result_buffer);
    memcpy(buffer_data, output.data(), result_len);

    return result_buffer;
}

static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        {"hmacSha256Hash", nullptr, HmacSha256Hash, nullptr, nullptr, nullptr, napi_default, nullptr}
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

static napi_module hmacModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "hmac_sha256",
    .nm_priv = ((void*)0),
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterHmacModule() { 
    napi_module_register(&hmacModule);
}

在ArkTS中使用

// AKI方式
import aki from 'libhmac_sha256_aki.so'

@Entry
@Component
struct HmacExample {
  @State result: string = '';

  build() {
    Column() {
      Button('计算 HMAC-SHA256')
        .onClick(() => {
          // 一行代码完成HMAC计算!
          this.result = aki.HmacSha256Hex('my-key', 'Hello World!');
        })
      
      Text(this.result)
    }
  }
}

// Node-API方式
import nativeModule from 'libhmac_sha256.so'

@Entry
@Component
struct HmacExample {
  @State result: string = '';

  build() {
    Column() {
      Button('计算 HMAC-SHA256')
        .onClick(() => {
          const hash = nativeModule.hmacSha256Hash('my-key', 'Hello World!');
          this.result = Array.from(new Uint8Array(hash))
            .map(b => b.toString(16).padStart(2, '0'))
            .join('');
        })
      
      Text(this.result)
    }
  }
}

✅ 最佳实践

1. 移植前准备

  • 库分析: 仔细分析原始库的API和依赖关系
  • 功能规划: 确定需要移植的核心功能
  • 接口设计: 设计ArkTS友好的接口

2. 代码组织

  • 模块化: 将不同功能模块分离
  • 错误处理: 提供完善的错误处理机制
  • 类型安全: 使用TypeScript提供类型安全

3. 性能优化

  • 内存管理: 注意内存泄漏问题
  • 数据转换: 减少不必要的数据转换
  • 批量处理: 支持批量操作提高效率

4. 测试验证

  • 单元测试: 编写完整的单元测试
  • 集成测试: 在真实环境中测试
  • 性能测试: 验证性能表现

❓ 常见问题

Q1: 如何选择移植方式?

A: 推荐使用AKI方式,除非有特殊需求。AKI方式代码量少、开发效率高、维护成本低。

Q2: Node-API和Node.js的Node-API有什么区别?

A: HarmonyOS Node-API是基于Node.js 12.x LTS的Node-API规范扩展开发的机制,提供了ArkTS/JS与C/C++模块之间的交互能力,但有一些HarmonyOS特有的扩展接口。

Q3: 如何处理复杂的C++类?

A: 使用AKI的JSBIND_CLASS语法糖绑定C++类:

class MyClass {
public:
    MyClass(int value) : value_(value) {}
    int getValue() const { return value_; }
    void setValue(int value) { value_ = value; }
private:
    int value_;
};

JSBIND_CLASS(MyClass) {
    JSBIND_CONSTRUCTOR<int>();
    JSBIND_METHOD(getValue);
    JSBIND_METHOD(setValue);
}

Q4: 如何处理异步操作?

A: 使用Node-API的异步工作接口:

// 创建异步工作
napi_async_work work;
napi_create_async_work(env, nullptr, resource_name, execute_callback, complete_callback, data, &work);
napi_queue_async_work(env, work);

Q5: 构建失败怎么办?

A: 检查以下几点:

  1. 确保安装了AKI依赖:ohpm install @ohos/aki
  2. 检查CMake版本:cmake --version
  3. 检查HarmonyOS SDK路径配置
  4. 查看构建日志定位具体错误

🎉 总结

C/C++三方库移植到HarmonyOS平台是一个系统性的工程,选择合适的移植方式至关重要:

  • AKI方式: 推荐用于大多数场景,开发效率高
  • Node-API方式: 适用于需要精确控制的特殊场景

通过本教程的学习,您应该能够:

  1. 理解移植的基本概念和流程。
  2. 选择合适的移植方式。
  3. 使用AKI或Node-API实现库的移植。
  4. 配置构建环境并生成可用的库。
  5. 在ArkTS应用中正确使用移植的库。

如果还有其他问题,欢迎关注交流。
作者:csdn猫哥,个人博客:blog.csdn.net/yyz_1987,转载请注明出处。

参考链接

https://gitcode.com/openharmony-tpc/docs/blob/master/contribute/adapter-guide/c_c++%E7%A7%BB%E6%A4%8D%E9%80%82%E9%85%8D%E6%8C%87%E5%AF%BC.md

https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-dynamic-link-library#section166546365376

https://gitcode.com/openharmony-sig/aki

https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-ndk-33

https://blog.itpub.net/70011554/viewspace-2968815/

https://www.woshipm.com/share/6192541.html

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐