随着 HarmonyOS NEXT(纯血鸿蒙)生态的快速扩张,越来越多的应用开始尝试把成熟的 C/C++ 三方库引入到鸿蒙工程里——图形渲染、音视频编解码、加密、网络、AI 推理……这些库往往已经在 Linux/Windows 上跑了很多年,重写不现实,最务实的路径就是「交叉编译 + NAPI 封装」

随着 HarmonyOS NEXT(纯血鸿蒙)生态的快速扩张,越来越多的应用开始尝试把成熟的 C/C++ 三方库引入到鸿蒙工程里——图形渲染、音视频编解码、加密、网络、AI 推理……这些库往往已经在 Linux/Windows 上跑了很多年,重写不现实,最务实的路径就是「交叉编译 + NAPI 封装」

但问题来了:网上关于鸿蒙三方库适配的资料要么太碎(只讲某一步),要么太旧(API 8 时代),很少有端到端走通的完整案例。很多同学卡在「编译出来的 .a 链接不上」「NAPI 头文件顺序报错」「GL 上下文创建失败」这些细节里反复折腾。

这篇文章,我就用 NanoVG 这个轻量级 2D 矢量图形库,从零走通 「C 源码 → 交叉编译静态库 → NAPI 适配 → DevEco 工程集成 → ArkTS 调用」 全流程,并把我踩过的每一个坑都标出来。

适合谁读:有 C/C++ 基础、正在做 HarmonyOS 应用/系统开发、想把第三方 C 库移植到鸿蒙的同学。

一、项目背景与目标

1.1 NanoVG 是什么

NanoVG 是一个单头文件实现的轻量级 2D 矢量图形渲染库,主要特点:

  • 📦 单文件实现(nanovg.h),无需复杂构建

  • 🎨 基于 OpenGL 的矢量图形渲染

  • 🔧 支持多种后端:OpenGL 2/3、OpenGL ES 2/3、Metal

  • 💡 非常适合嵌入式设备和图形 UI 应用

1.2 适配目标

把 NanoVG 交叉编译为 HarmonyOS 可用的静态库,并通过 NAPI 封装,集成到 ArkTS 应用中,实现小车控制的状态可视化——小车通过 TCP 接收控制指令,NanoVG 实时绘制小车的位置、朝向、运动状态。

1.3 技术栈

组件

版本

说明

操作系统

Ubuntu 20.04

交叉编译环境

DevEco Studio

5.1.1.840

HarmonyOS 开发 IDE

HarmonyOS SDK

API 12

目标平台 SDK

编译工具链

llvm/clang 12+

HarmonyOS NDK

NanoVG

master 分支

目标三方库

二、整体流程总览

阶段

做什么

关键产物

① 环境搭建

配置 HarmonyOS NDK 交叉编译环境

clang/llvm 工具链就绪

② 交叉编译

把 NanoVG 编译成 libnanovg.a

arm64-v8a / x86_64 静态库

③ NAPI 适配

用 NAPI 把 C 接口暴露给 ArkTS

libentry.so + Index.d.ts

④ 工程集成

DevEco 工程引入库并调用验证

可运行 HAP

一句话主线:C 源码 →(交叉编译)→ .a →(NAPI 封装)→ .so →(import)→ ArkTS

抓住这条主线,后面每一步都不会迷路。

一句话主线:C 源码 →(交叉编译)→ .a →(NAPI 封装)→ .so →(import)→ ArkTS

抓住这条主线,后面每一步都不会迷路。


三、阶段一:环境搭建

3.1 下载 HarmonyOS NDK

访问 HarmonyOS 开发者官网 下载 NDK 工具包,解压到本地:

# 假设解压到 /home/ohpkg/linux
export OHOS_SDK_ROOT=/home/ohpkg/linux

3.2 配置环境变量

# 设置 NDK 路径
export OHOS_NDK=$OHOS_SDK_ROOT/native
export OHOS_SYSROOT=$OHOS_NDK/sysroot
export OHOS_CLANG=$OHOS_NDK/llvm/bin

# 加入 PATH
export PATH=$OHOS_CLANG:$PATH

# 设置交叉编译工具链
export CC=clang
export CXX=clang++
export AR=llvm-ar
export AS=llvm-as
export LD=ld.lld
export RANLIB=llvm-ranlib
export STRIP=llvm-strip

3.3 验证环境

# 检查 clang 是否能识别鸿蒙目标
clang --target=aarch64-linux-ohos --version

预期输出:

clang version 12.0.0 (...)
Target: aarch64-linux-ohos
Thread model: posix

💡 经验:鸿蒙的目标三元组(triple)和 Android 不一样!

  • arm64-v8aaarch64-linux-ohos(手机/平板/车机)

  • x86_64x86_64-linux-ohos(PC/2in1)

直接套 Android 的 aarch64-linux-android 是编译不出来的。

3.4 目标架构说明

架构

工具链目标三元组

适用设备

arm64-v8a

aarch64-linux-ohos

手机、平板、车机

x86_64

x86_64-linux-ohos

PC、2in1 设备

本文以 arm64-v8a 为例演示,x86_64 只需改一个参数,文末会讲。

四、阶段二:交叉编译打包静态库

4.1 获取 NanoVG 源码

git clone https://github.com/memononen/nanovg.git
cd nanovg-master

# 主要文件
# nanovg.h        # 头文件
# nanovg.c        # 实现文件(可选)
# nanovg_gl.h     # OpenGL 后端头文件

4.2 编写交叉编译 Makefile

在源码根目录创建 Makefile.ohos

# HarmonyOS NanoVG 交叉编译 Makefile

# 工具链配置
CC      = clang
AR      = llvm-ar
RANLIB  = llvm-ranlib
STRIP   = llvm-strip

# 目标架构
TARGET_TRIPLE = aarch64-linux-ohos
SYSROOT       = $(OHOS_NDK)/sysroot

# 编译参数
CFLAGS = --target=$(TARGET_TRIPLE) \
         --sysroot=$(SYSROOT) \
         -fPIC \
         -fno-addrsig \
         -fdata-sections \
         -ffunction-sections \
         -Wall \
         -O2

# 包含路径
INCLUDES = -I$(SYSROOT)/usr/include -I.

# 源文件(NanoVG 使用单文件实现)
SOURCES = nanovg.c

# 输出
OUTPUT_DIR = ../ohos_libs/usr/local/lib
TARGET     = $(OUTPUT_DIR)/libnanovg.a

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(SOURCES)
	@mkdir -p $(OUTPUT_DIR)
	$(CC) $(CFLAGS) $(INCLUDES) -c $(SOURCES) -o nanovg.o
	$(AR) rcs $@ nanovg.o
	$(RANLIB) $@
	@echo "Build complete: $@"
	@file $@

clean:
	rm -f *.o $(TARGET)

4.3 关键编译参数解读

很多同学复制 Makefile 但不理解每个参数,排错时就很被动,这里逐个解释:

参数

作用

为什么需要

--target=

指定 OHOS 目标三元组

最关键,决定产物架构,写错直接编出 PC 版本

--sysroot=

指定 OHOS sysroot

提供系统头文件和库的根目录

-fPIC

生成位置无关代码

后续要链接进 .so,不加会链接报错

-fdata-sections / -ffunction-sections

按函数/变量分 section

配合 --gc-sections 做死代码消除,缩减体积

-O2

优化等级

兼顾性能和体积

-Wall

开启警告

提前暴露问题

4.4 执行编译

make -f Makefile.ohos

预期输出:

clang --target=aarch64-linux-ohos ... -c nanovg.c -o nanovg.o
llvm-ar rcs ../ohos_libs/usr/local/lib/libnanovg.a nanovg.o
llvm-ranlib ../ohos_libs/usr/local/lib/libnanovg.a
Build complete: ../ohos_libs/usr/local/lib/libnanovg.a

4.5 验证产物

# 1. 查看文件类型,应为 ARM aarch64
file libnanovg.a
# 输出: ELF 64-bit LSB relocatable, ARM aarch64, ...

# 2. 查看导出符号,应有 nvgCreateGLES2 / nvgDeleteGLES2
llvm-nm libnanovg.a | grep nvgCreate

# 3. 查看公开函数数量(NanoVG 约 47 个)
llvm-nm libnanovg.a | grep ' T ' | wc -l

📷 [配图占位·4.5] 建议放一张终端里 file + llvm-nm 命令输出截图,尺寸 1200×675。

4.6 编译 x86_64 版本(可选)

只需改两个地方:

TARGET_TRIPLE = x86_64-linux-ohos
OUTPUT_DIR    = ../ohos_libs/usr/local/lib/x86_64

4.7 打包发布

cd ../ohos_libs
tar czf nanovg-ohos-arm64.tar.gz usr/local/lib/libnanovg.a

五、阶段三:NAPI 适配封装(核心环节)

📌 这是整篇文章最关键的部分。前面只是把 C 库编译出来,真正让它能被 ArkTS 调用,靠的是这一步

5.1 工程结构设计

MyApplication2/
├── entry/
│   └── src/
│       └── main/
│           ├── cpp/
│           │   ├── CMakeLists.txt          # CMake 构建配置
│           │   ├── napi_init.cpp           # NAPI 入口文件
│           │   ├── include/
│           │   │   └── nanovg/
│           │   │       ├── nanovg.h
│           │   │       └── nanovg_gl.h
│           │   └── types/
│           │       └── libentry/
│           │           └── Index.d.ts      # TypeScript 类型定义
│           ├── ets/
│           │   ├── pages/
│           │   │   └── Index.ets           # 主页面
│           │   └── service/
│           │       └── ChassisService.ets   # 小车控制服务
│           └── module.json5                # 模块配置
└── entry/
    └── libs/
        ├── arm64-v8a/libnanovg.a
        └── x86_64/libnanovg.a

5.2 CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.5.0)
project(MyNativeProject)

# 原生渲染根路径
set(NATIVERENDER_ROOT_PATH ${CMAKE_CURRENT_SOURCE_DIR})

# HarmonyOS SDK 查找
if(DEFINED PACKAGE_FIND_FILE)
    include(${PACKAGE_FIND_FILE})
endif()

# 静态库存放路径(根据自动检测的 OHOS_ARCH 切换架构)
set(LIBS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH})

# 引入预编译的 NanoVG 静态库
add_library(nanovg STATIC IMPORTED)
set_target_properties(nanovg PROPERTIES
    IMPORTED_LOCATION ${LIBS_DIR}/libnanovg.a
)

# 头文件包含路径
include_directories(
    ${NATIVERENDER_ROOT_PATH}
    ${NATIVERENDER_ROOT_PATH}/include
)

# 生成共享库(NAPI 入口)
add_library(entry SHARED napi_init.cpp)

# 链接依赖库
target_link_libraries(entry PUBLIC
    libace_napi.z.so    # NAPI 运行时
    nanovg              # NanoVG 静态库
    GLESv2              # OpenGL ES 2.0
    EGL                 # EGL 库
)

要点解读:

  • IMPORTED 表示引入一个预编译好的外部库,CMake 不会去编译它,只做链接

  • ${OHOS_ARCH} 是 DevEco 注入的变量,会自动取 arm64-v8ax86_64

  • 最终产物 libentry.so 就是 ArkTS 侧 import 的那个 .so

5.3 拷贝头文件

mkdir -p entry/src/main/cpp/include/nanovg
cp nanovg-master/nanovg.h    entry/src/main/cpp/include/nanovg/
cp nanovg-master/nanovg_gl.h entry/src/main/cpp/include/nanovg/

5.4 NAPI 入口实现(napi_init.cpp

头文件包含顺序是这里的第一个大坑

// 1. OpenGL ES 2.0 头文件 —— 必须最先包含!
//    因为 nanovg_gl.h 用到了 GLuint 等 GL 类型,
//    如果不先包含 gl2.h,就会报 "unknown type name 'GLuint'"
#include <GLES2/gl2.h>

// 2. NAPI 头文件
#include <napi/native_api.h>

// 3. NanoVG 头文件
#include "include/nanovg/nanovg.h"
#include "include/nanovg/nanovg_gl.h"

// 4. 标准库
#include <string>
#include <cstdio>

// 全局 NanoVG 上下文
static NVGcontext* vg = nullptr;

下面是几个典型的 NAPI 接口封装。

① 获取版本信息:

static napi_value GetVersion(napi_env env, napi_callback_info info) {
    const char *version = "NanoVG 1.0 (GLES2 backend)";
    napi_value result;
    napi_create_string_utf8(env, version, strlen(version), &result);
    return result;
}

② 创建 / 销毁上下文:

// 创建 NanoVG 上下文
static napi_value CreateNVGContext(napi_env env, napi_callback_info info) {
    napi_value result;
    if (vg != nullptr) {
        napi_create_string_utf8(env, "Context already exists", NAPI_AUTO_LENGTH, &result);
        return result;
    }
    // 调用 NanoVG 的 C 接口创建 GLES2 后端上下文
    vg = nvgCreateGLES2(NVG_ANTIALIAS | NVG_STENCIL_STROKES);
    if (vg != nullptr) {
        napi_create_string_utf8(env, "Context created successfully", NAPI_AUTO_LENGTH, &result);
    } else {
        napi_create_string_utf8(env,
            "Context creation failed - requires active GL surface",
            NAPI_AUTO_LENGTH, &result);
    }
    return result;
}

// 销毁上下文
static napi_value DestroyNVGContext(napi_env env, napi_callback_info info) {
    if (vg != nullptr) {
        nvgDeleteGLES2(vg);
        vg = nullptr;
    }
    napi_value result;
    napi_create_string_utf8(env, "Context destroyed", NAPI_AUTO_LENGTH, &result);
    return result;
}

③ 业务接口——绘制小车可视化:

// 参数:x, y, size, angle, state
static napi_value DrawCarVisualization(napi_env env, napi_callback_info info) {
    size_t argc = 5;
    napi_value args[5];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    float x = 200.0f, y = 200.0f, size = 100.0f, angle = 0.0f;
    int state = 0;

    if (argc >= 5) {
        double tmp;
        napi_get_value_double(env, args[0], &tmp); x = (float)tmp;
        napi_get_value_double(env, args[1], &tmp); y = (float)tmp;
        napi_get_value_double(env, args[2], &tmp); size = (float)tmp;
        napi_get_value_double(env, args[3], &tmp); angle = (float)tmp;
        napi_get_value_int32 (env, args[4], &state);
    }

    const char* stateNames[] = {"stop", "forward", "backward", "left", "right"};
    const char* stateName = (state >= 0 && state < 5) ? stateNames[state] : "unknown";

    char buf[512];
    snprintf(buf, sizeof(buf),
        "Car at (%.0f,%.0f), size %.0f, angle %.0f°, state: %s",
        x, y, size, angle, stateName);

    napi_value result;
    napi_create_string_utf8(env, buf, strlen(buf), &result);
    return result;
}

④ 模块导出——把上面的函数暴露给 ArkTS:

static napi_value Init(napi_env env, napi_value exports) {
    // desc 数组里的每一项 = 一个暴露给 JS 侧的方法
    napi_property_descriptor desc[] = {
        {"getVersion",           nullptr, GetVersion,           nullptr, nullptr, nullptr, napi_default, nullptr},
        {"createNVGContext",     nullptr, CreateNVGContext,     nullptr, nullptr, nullptr, napi_default, nullptr},
        {"destroyNVGContext",    nullptr, DestroyNVGContext,    nullptr, nullptr, nullptr, napi_default, nullptr},
        {"drawCarVisualization", nullptr, DrawCarVisualization, nullptr, nullptr, nullptr, napi_default, nullptr},
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}

// NAPI 模块声明,"entry" 必须和 CMake 里的 target 名一致
NAPI_MODULE(entry, Init)

5.5 TypeScript 类型定义(Index.d.ts

这一步常被忽略,但它决定了 ArkTS 侧有没有类型提示:

/**
 * NanoVG NAPI 接口类型定义
 */

// 获取 NanoVG 版本信息
export const getVersion: () => string;

// 创建 NanoVG 上下文
export const createNVGContext: () => string;

// 销毁 NanoVG 上下文
export const destroyNVGContext: () => string;

/**
 * 绘制小车可视化
 * @param x     中心点 X 坐标
 * @param y     中心点 Y 坐标
 * @param size  小车尺寸
 * @param angle 旋转角度(度)
 * @param state 状态 (0:停止, 1:前进, 2:后退, 3:左转, 4:右转)
 */
export const drawCarVisualization: (
    x: number,
    y: number,
    size: number,
    angle: number,
    state: number
) => string;

⚠️ 关于宏定义的实测差异

很多教程会告诉你:在 napi_init.cpp 顶部要写 #define NANOVG_GLES2

但我在实际工程里实测:

一个更重要、必须遵守的规则是——绝对不要写 #define NANOVG_GLES2_IMPLEMENTATION,否则会在链接时报 duplicate symbol: nvgCreateGLES2(实现代码重复)。记住:实现交给 .a,源码里只放声明

六、阶段四:DevEco 工程集成与验证

6.1 创建项目

打开 DevEco Studio 5.1.1.840 → Create Project:

项目模板:Native C++ (HarmonyOS)
API Level:API 12
设备类型:Phone + Tablet + 2in1
项目名称:MyApplication2

6.2 拷贝预编译库

cp nanovg-ohos-arm64/libnanovg.a  MyApplication2/entry/libs/arm64-v8a/
cp nanovg-ohos-x86_64/libnanovg.a MyApplication2/entry/libs/x86_64/

📷 [配图占位·6.2] 建议放一张 DevEco 工程目录树截图,标红 libs 和 cpp 目录,尺寸 1200×675。

6.3 配置模块权限

编辑 entry/src/main/module.json5,给小车 TCP 通信加网络权限:

{
  "module": {
    "name": "entry",
    "type": "entry",
    "mainElement": "EntryAbility",
    "deviceTypes": ["phone", "tablet", "2in1", "car", "wearable", "tv"],
    "pages": "$profile:main_pages",
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "exported": true
      }
    ],
    // ⬇️ 添加网络权限(小车 TCP 通信需要)
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
      }
    ]
  }
}

6.4 ArkTS 调用 NAPI

Index.ets 里 import 并调用:

import { getVersion, createNVGContext, drawCarVisualization } from 'libentry.so';

@Entry
@Component
struct Index {
  @State version: string = '';
  @State contextStatus: string = '';
  @State carVisual: string = '';

  aboutToAppear() {
    // 获取版本信息
    this.version = getVersion();
    // 创建 NanoVG 上下文
    this.contextStatus = createNVGContext();
    // 绘制小车(生成渲染指令描述)
    this.carVisual = drawCarVisualization(200, 200, 60, 45, 1);
  }

  build() {
    Column() {
      Text('NanoVG 三方库集成示例')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      Text(`版本: ${this.version}`)
        .fontSize(16)
        .margin({ bottom: 10 })

      Text(`状态: ${this.contextStatus}`)
        .fontSize(16)
        .fontColor(this.contextStatus.includes('success') ? '#4CAF50' : '#FF9800')
        .margin({ bottom: 10 })

      Text(this.carVisual)
        .fontSize(14)
        .fontColor('#666666')
        .width('80%')
        .padding(10)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)
    .padding(20)
  }
}

业务侧再配合 ChassisService.ets 做小车底盘的 TCP 控制,把实时状态喂给 NanoVG 渲染。

6.5 编译与运行

DevEco 顶部工具栏:

Build Target → Debug
Build Mode   → Hap
Module       → entry
Product      → default

执行 Build → Make Module 'entry'(快捷键 Ctrl + B)。

预期构建输出:

> hvigor Finished :entry:default@BuildJS...
> hvigor Finished :entry:default@BuildNativeWithNinja...
> hvigor Finished :entry:default@BuildHap...
> hvigor BUILD SUCCESSFUL in 2s 737 ms

看到 BuildNativeWithNinjaBuildHap 都成功,就说明 NAPI + 静态库全链路打通了。

6.6 验证功能

应用启动后应显示

版本: NanoVG 1.0 (GLES2 backend)
状态: Context created successfully / requires active GL surface

七、踩坑总结(建议收藏)

这是我实际踩过、且大家大概率会遇到的 4 个坑,按出现频率排序:

#

问题

现象

根因

解法

1

类型未定义

nanovg_gl.h: error: unknown type name 'GLuint'

nanovg_gl.h 用了 GL 类型但没引头

#include <GLES2/gl2.h> 必须在 nanovg_gl.h 之前

2

重复符号

ld.lld: error: duplicate symbol: nvgCreateGLES2

同时定义了 NANOVG_GLES2_IMPLEMENTATION 宏又链接了 .a

只用 #define NANOVG_GLES2(仅声明),实现交给预编译库

3

库找不到

ld.lld: error: library 'libnanovg.a' not found

架构目录不匹配

OHOS_ARCH 放到 entry/libs/<arch>/

4

上下文创建失败

Context creation failed - requires active GL surface

NAPI 回调里没有活跃 GL 表面

XComponent 提供 surface,在其 onLoad 里创建上下文

坑 4 的解法展开——需要真实渲染时,用 XComponent 提供 GL 表面:

XComponent({
  id: 'nanovg_component',
  type: 'surface'
})
.onLoad(() => {
  // 在 surface 就绪后再创建 NanoVG 上下文
  createNVGContext();
})

九、扩展方向

跑通基础流程后,可以往这些方向扩展:

  • XComponent 实现真实 OpenGL ES 渲染(而非只返回描述字符串)

  • 添加更多 NanoVG 绘图接口(路径、渐变、文字)

  • 实现实时小车状态可视化 + 轨迹回放

  • 把这套流程套到其他 C 库(如 cJSON / libpng / openssl),形成通用适配模板

写在最后

三方库适配本质上是「工具链问题 + 接口桥接问题」的组合:前者靠交叉编译解决,后者靠 NAPI 解决。一旦把这条链路跑通一次,后面再适配任何 C 库都是套模板的事——换源码、换 Makefile 里的库名、换 NAPI 里暴露的函数,仅此而已。

Logo

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

更多推荐