[鸿蒙PC三方库适配实战] Java 本地访问库 JNA 的 鸿蒙PC 平台迁移实践
本文详细介绍了如何将 Java Native Access (JNA) 本地库适配到 鸿蒙PC 平台。文章将系统性地讲解如何利用 lycium_plusplus 构建框架,处理 Java JNI 本地库在鸿蒙环境下的交叉编译流程,展示如何解决 X11 图形依赖、libffi 交叉编译、JNI 头文件生成以及 HNP 包生成的完整实践。
摘要:本文详细介绍了如何将 Java Native Access (JNA) 本地库适配到 鸿蒙PC 平台。文章将系统性地讲解如何利用 lycium_plusplus 构建框架,处理 Java JNI 本地库在鸿蒙环境下的交叉编译流程,展示如何解决 X11 图形依赖、libffi 交叉编译、JNI 头文件生成以及 HNP 包生成的完整实践。
本文是软件鸿蒙化迁移实践系列文章之一,专注于 Java JNI 本地库的鸿蒙适配,为开发者提供完整的迁移指南。
欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
AtomGit 仓库地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_jna_pc
项目信息说明
| 项目 | 说明 |
|---|---|
| 名称 | JNA (Java Native Access) |
| 开源协议 | LGPL-2.1 / Apache-2.0 |
| 源码版本 | 5.14.0 |
| 目标平台 | 鸿蒙 PC |
| 依赖项 | JDK 11, Ant, libffi |
| 操作系统平台 | WSL Ubuntu 24.04 |
一、背景介绍
1.0 功能与效果
JNA 本地库在本实践中的预期能力如下:
功能:为 Java 程序提供访问本地共享库的能力,无需编写 JNI 代码。JNA 通过 libjnidispatch.so 实现 Java 与本地 C/C++ 库之间的自动调度。
效果:在鸿蒙 PC 上提供与标准 Linux 环境相近的 JNA 使用体验,便于在鸿蒙 Java 应用开发中实现本地库调用、系统 API 访问、硬件接口交互等场景的本地调度功能。
1.1 什么是 鸿蒙PC HNP 生态
HNP(Harmony Native Package)是 鸿蒙PC 的原生包格式,lycium 是增强型构建框架,支持自动下载源码、交叉编译(arm64-v8a、armeabi-v7a)、一键生成 HNP 包以及开源声明聚合。C/C++ 原生库的适配是鸿蒙系统生态建设中的重要一环。
为什么 JNA 需要 C/C++ 构建?
虽然 JNA 是 Java 库,但它包含一个核心的本地调度库 libjnidispatch.so(C 代码实现)。这个 so 文件负责:
- Java 与本地 C/C++ 库之间的自动调度
- 数据类型转换和内存管理
- 函数调用的底层实现
因此,我们需要使用 lycium_plusplus 框架交叉编译这个 C 语言本地库。
1.2 为什么适配 JNI 本地库会有难度
常见挑战包括:
- 构建系统差异:JNA 使用 Makefile 构建系统,需要配置交叉编译工具链(aarch64-linux-ohos-clang/clang++)。
- X11 图形依赖:JDK 的 jawt_md.h 硬编码需要 X11/Xlib.h,但 鸿蒙PC 是无头系统(headless),不需要图形界面。
- libffi 交叉编译:JNA 依赖 libffi 静态库,老版本 libffi 不认识 ohos 目标,需要使用 android 兼容方案。
- JNI 头文件生成:需要 JDK 11 和 Ant 工具,通过 ant javah 自动生成 JNI 头文件。
- 共享库****链接配置:Makefile 默认使用 -shared 标志,但环境变量 LDFLAGS 会覆盖 Makefile 的默认值,需要特殊处理。
- Windows 元数据干扰:从 Windows 环境复制的源码可能包含 :Zone.Identifier 等区域标识文件,需要在打包前清理。
- 构建缓存问题:lycium 使用 hpk_build.csv 跟踪构建状态,需要正确清理缓存才能重新构建。
1.3 JNA 简介
JNA (Java Native Access) 是一款由 Sun Microsystems(现 Oracle)开发的 Java 库,提供访问本地共享库的能力而无需编写 JNI 代码。其主要特点包括:
- 零 JNI 代码:Java 开发者无需编写 C/C++ 代码即可调用本地库。
- 自动类型映射:自动处理 Java 类型与 C 类型之间的转换。
- 跨平台特性:原生支持 Windows、macOS、Linux,本次适配扩展到 鸿蒙PC 平台。
- 广泛使用:被 NetBeans、Eclipse、IntelliJ IDEA 等知名项目使用。
- 开源地址:
- GitHub:https://github.com/java-native-access/jna
- AtomGit:https://atomgit.com/weixin_62765017/ohos_jna.git
二、环境准备
2.1 系统要求
- 开发环境:Ubuntu 24.04(推荐 WSL 2)
- 核心工具:Make、GCC/G++、Git、Python3、JDK 11、Ant
- 构建框架:lycium_plusplus
- 鸿蒙 SDK:鸿蒙PC SDK(提供交叉编译工具链 aarch64-linux-ohos-clang/clang++)
- 目标架构:arm64-v8a(AArch64)
2.2 扩展阅读与参考教程
下方汇总展示了多位老师在鸿蒙 鸿蒙PC 适配方面的高质量教程。若在前提准备(环境、工具链、框架)部分还有不清楚的地方,可参考这些文章进一步学习。 以下资源不分先后顺序,均具有参考价值。
| 资源类型 | 描述 | 链接 |
|---|---|---|
| 三方库交叉编译环境(Ubuntu) | 在 Ubuntu 中搭建鸿蒙PC 三方库交叉编译构建开发环境 | 👉 点击查看 |
| 三方库交叉编译环境(macOS) | 在 macOS 中搭建鸿蒙PC 三方库交叉编译开发环境 | 👉 点击查看 |
| 基础环境搭建 | Windows 10 上安装和使用 WSL 2、安装 Ubuntu 24 详细指南 | 👉 点击查看 |
| Mac 移植指南 | 鸿蒙PC命令行适配指南(Mac 版) | 👉 点击查看 |
| Win 移植指南 | 鸿蒙PC 生态三方软件移植:开发环境搭建及三方库移植指南 | 👉 点击查看 |
| 全流程适配指南 | OpenHarmony Linux 命令行工具适配实战:基于 Cursor × WSL 的 tree 2.2.1 交叉编译与 HNP 打包全流程指南 | 👉 点击查看 |
| 官方构建文档 | 新脚手架:社区维护的鸿蒙PC 生态命令行工具构建框架 lycium_plusplus(原 build 仓库为旧方式,请以本仓库为准) | 👉 点击查看 |
2.3 配置 鸿蒙PC SDK 环境变量
在开始之前,需要先配置 鸿蒙PC SDK 路径。这是所有后续操作的基础。
# 配置 鸿蒙PC SDK 环境变量(请根据实际路径修改)
export OHOS_SDK=/home/weishuo/ohos-sdk/linux
# 验证 SDK 是否存在
ls ${OHOS_SDK}/native/llvm/bin/aarch64-linux-ohos-clang
# 应该输出:/home/weishuo/ohos-sdk/linux/native/llvm/bin/aarch64-linux-ohos-clang
注意:如果 SDK 路径不同,请修改为你的实际路径。
2.4 lycium_plusplus 框架
lycium_plusplus 是本次适配工作的核心工具,主要用于统一管理各类第三方库的构建流程,通过规范编译、依赖与打包逻辑,实现三方库在目标平台上高效、稳定地编译与集成,是整个适配环节中保障构建一致性与可维护性的关键支撑。
# 克隆 lycium_plusplus 项目
git clone https://gitcode.com/OpenHarmonyPCDeveloper/lycium_plusplus.git
cd lycium_plusplus
2.5 JDK 11 与 Ant 安装
由于 JNA 需要生成 JNI 头文件,需要安装 JDK 11 和 Ant 工具。
# 安装 JDK 11
sudo apt-get install -y openjdk-11-jdk
# 安装 Ant
sudo apt-get install -y ant
# 验证安装
java -version
javac -version
ant -version
# 配置 鸿蒙PC SDK 环境变量
export OHOS_SDK=/home/weishuo/ohos-sdk/linux
三、实战:以 JNA 为例的适配步骤
本章节将为新人开发者提供完整的、可复现的适配步骤。我们将从零开始,逐步完成 JNA 本地库的鸿蒙适配工作。每个步骤都包含详细的说明、命令示例和注意事项。
3.1 创建项目目录结构
步骤说明:
在 lycium_plusplus 框架中,每个三方库都需要在 thirdparty/ 目录下拥有独立的目录。这个目录将存放该库的所有构建配置文件(HPKBUILD、hnp.json、README.OpenSource、HPKCHECK 等)。
详细操作流程:
# 1. 进入 lycium_plusplus 项目根目录
cd /home/weishuo/lycium_plusplus
# 2. 进入 thirdparty 目录
cd thirdparty
# 3. 创建 jna 目录
mkdir -p jna
# 4. 进入新创建的目录
cd jna
# 5. 验证目录创建成功
pwd
# 输出:/home/weishuo/lycium_plusplus/thirdparty/jna
# 6. 查看目录结构
ls -la
目录结构说明:
lycium_plusplus/
├── thirdparty/
│ ├── jna/ ← 我们刚创建的目录
│ │ ├── HPKBUILD ← 将要创建的构建脚本
│ │ ├── hnp.json ← 将要创建的包元数据
│ │ ├── README.OpenSource ← 将要创建的开源声明
│ │ └── HPKCHECK ← 将要创建的检查脚本
│ ├── mediainfo/ ← 其他三方库示例
│ ├── fuse3/ ← 其他三方库示例
│ └── ...
├── lycium/
│ ├── build.sh ← lycium 构建入口脚本
│ └── usr/ ← 构建产物输出目录
└── Projects/
└── jna/ ← JNA 源码目录(需提前准备)
注意事项:
- 目录名称必须与 pkgname 变量保持一致(本例中为 jna)
- 确保 Projects/jna/ 目录中已经有 JNA 的源码
- 如果源码还未准备,需要先下载或克隆源码到 Projects/jna/ 目录(参考 3.0 节)
3.2 禁用 JAWT 依赖的 sed 方案(推荐)
# 1. 在 #include <wchar.h> 后添加 NO_JAWT 宏定义
sed -i '/#include <wchar.h>/a\
\
/* OpenHarmony: Disable JAWT to avoid X11 dependency */\
#ifndef NO_JAWT\
#define NO_JAWT 1\
#endif' native/dispatch.c
# 2. 将 #include <jni.h> 替换为直接包含 JNI 头文件(跳过 jni_md.h)
sed -i 's/^#include <jni.h>$/#include "com_sun_jna_Native.h"\n#include "com_sun_jna_Function.h"/' native/dispatch.c
# 3. 修改 JAWT 条件编译(禁用 JAWT 代码)
sed -i 's/^#ifndef NO_JAWT$/#ifdef DISABLE_JAWT_COMPLETELY/' native/dispatch.c
为什么推荐 sed 而不是 patch?
| 方式 | 优点 | 缺点 |
|---|---|---|
| patch 文件 | 直观、易读 | 依赖行号,版本不同可能失败 |
| sed 命令 | 灵活、不依赖行号 | 语法稍复杂 |
本文选择:使用 sed 命令,直接在 HPKBUILD 的 prepare() 函数中修改源码,无需额外创建 patch 文件。
注意事项:
- 这三条 sed 命令会在后面的 HPKBUILD 的 prepare() 函数中自动执行
- 你不需要手动运行这些命令,只需要理解它们的原理即可
3.3 创建 HPKBUILD 文件(核心构建脚本)
什么是 HPKBUILD?
HPKBUILD 是 lycium 框架的核心构建脚本,可以理解为一个"构建配方"。它告诉 lycium 框架:
- 这个库叫什么、什么版本、什么许可证(元信息)
- 如何准备源码、如何编译、如何打包(构建流程)
- 使用什么编译器和编译参数(环境配置)
创建方法:
#!/bin/bash
# -----------------------------------------------------------------------------
# JNA (Java Native Access) HPKBUILD - OpenHarmony 鸿蒙适配
# 适配本地库 libjnidispatch.so
# -----------------------------------------------------------------------------
pkgname=jna
pkgver=5.14.0
pkgrel=0
pkgdesc="Java Native Access - native dispatch library for OpenHarmony"
url="https://github.com/java-native-access/jna"
archs=("arm64-v8a")
license=("LGPL-2.1")
depends=()
makedepends=()
autounpack=false
downloadpackage=false
buildtools="make"
srcpath="${LYCIUM_ROOT}/../Projects/jna"
builddir="jna-${pkgver}"
# -----------------------------------------------------------------------------
# prepare():准备源码
# -----------------------------------------------------------------------------
prepare() {
if [ -d "$srcpath" ]; then
echo "Using local source from: $srcpath"
mkdir -p "$builddir"
cp -rf "$srcpath"/* "$builddir/"
# 清理 Windows 元数据
find "$builddir" -name "*.bak" -type f -delete 2>/dev/null || true
find "$builddir" -name "*:Zone.Identifier" -type f -delete 2>/dev/null || true
# 修改 dispatch.c:禁用 JAWT 避免 X11 依赖(OpenHarmony 不需要图形界面)
cd "$builddir"
if [ -f "native/dispatch.c" ]; then
echo "Patching dispatch.c to disable JAWT..."
# 在 #include <wchar.h> 后添加 NO_JAWT 定义
sed -i '/#include <wchar.h>/a\
\
/* OpenHarmony: Disable JAWT to avoid X11 dependency */\
#ifndef NO_JAWT\
#define NO_JAWT 1\
#endif' native/dispatch.c
# 将 #include <jni.h> 替换为直接包含 JNI 头文件(跳过 jni_md.h 的 X11 依赖)
sed -i 's/^#include <jni.h>$/#include "com_sun_jna_Native.h"\n#include "com_sun_jna_Function.h"/' native/dispatch.c
# 将 #ifndef NO_JAWT 改为 #ifdef DISABLE_JAWT_COMPLETELY(永不成立)
sed -i 's/^#ifndef NO_JAWT$/#ifdef DISABLE_JAWT_COMPLETELY/' native/dispatch.c
echo "dispatch.c patched successfully"
fi
cd "$OLDPWD"
# 生成 JNI 头文件(关键步骤!)
echo "Generating JNI headers..."
cd "$builddir"
ant javah > "${LYCIUM_ROOT}/log/jna-javah.log" 2>&1
ret=$?
if [ $ret -ne 0 ]; then
echo "ERROR: Failed to generate JNI headers"
cat "${LYCIUM_ROOT}/log/jna-javah.log" >&2
cd "$OLDPWD"
return $ret
fi
# 复制 JNI 头文件到 build/native 目录(Makefile 期望的位置)
mkdir -p build/native
if [ -d "build/headers" ]; then
cp -f build/headers/*.h build/native/
echo "JNI headers copied to build/native/"
ls -la build/native/*.h
fi
echo "JNI headers generated successfully"
cd "$OLDPWD"
echo "Prepare completed in: $builddir"
else
echo "ERROR: Source not found at $srcpath"
exit 1
fi
}
# -----------------------------------------------------------------------------
# build():编译构建
# -----------------------------------------------------------------------------
build() {
cd "$builddir/native"
# 设置 buildlog
buildlog="${LYCIUM_ROOT}/log/${pkgname}-build.log"
mkdir -p "${LYCIUM_ROOT}/log"
# 设置 OpenHarmony 交叉编译工具链
export CC="${OHOS_SDK}/native/llvm/bin/aarch64-linux-ohos-clang"
export CXX="${OHOS_SDK}/native/llvm/bin/aarch64-linux-ohos-clang++"
export AR="${OHOS_SDK}/native/llvm/bin/llvm-ar"
export RANLIB="${OHOS_SDK}/native/llvm/bin/llvm-ranlib"
export STRIP="${OHOS_SDK}/native/llvm/bin/llvm-strip"
export NM="${OHOS_SDK}/native/llvm/bin/llvm-nm"
# 设置编译标志(注意:不要设置 LDFLAGS 环境变量,让 Makefile 使用自己的默认值)
# 添加 JNI 头文件路径(build/headers 和 build/native 都要包含)
# 添加 JDK include 路径(jni.h 和 jni_md.h)
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export CFLAGS="--target=aarch64-linux-ohos --sysroot=${OHOS_SDK}/native/sysroot -O2 -fPIC -fno-strict-aliasing -I../build/native/libffi/include -I../build/native -I../build/headers -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux"
# 设置 libffi 交叉编译参数(关键!)
# 使用 aarch64-linux-android 因为老版本 libffi 不认识 ohos
export FFI_CONFIG="--enable-static --disable-shared --with-pic=yes --host=aarch64-linux-android"
export FFI_ENV="CC=\"$CC\" CFLAGS=\"$CFLAGS\" CPPFLAGS=\"$CFLAGS\""
export FFI_BUILD="../build/native/libffi"
# 创建构建目录
mkdir -p ../build/native
# 使用 Makefile 构建(参考 Android 交叉编译方式)
make clean > "$buildlog" 2>&1 || true
# make clean 会删除 build/native 目录,需要重新复制 JNI 头文件
mkdir -p ../build/native
if [ -d "../build/headers" ]; then
cp -f ../build/headers/*.h ../build/native/
echo "JNI headers re-copied to build/native/ after clean"
fi
make \
OS=linux \
ARCH=aarch64 \
CC="$CC" \
CXX="$CXX" \
AR="$AR" \
RANLIB="$RANLIB" \
STRIP="$STRIP" \
CFLAGS="$CFLAGS" \
CPPFLAGS="-DNO_JAWT -DNO_WEAK_GLOBALS -DFFI_STATIC_BUILD" \
CDEFINES="-DFFI_STATIC_BUILD -DNO_JAWT -DNO_WEAK_GLOBALS -DFFI_MMAP_EXEC_WRIT=1 -DFFI_MMAP_EXEC_SELINUX=0" \
HOST_CONFIG="--host=aarch64-linux-android" \
FFI_CONFIG="--enable-static --disable-shared --with-pic=yes --host=aarch64-linux-android" \
JAVA_HOME="" \
JAVAH="../build/native" \
BUILD="../build/native" \
INSTALLDIR="../build/linux-aarch64" \
>> "$buildlog" 2>&1
ret=$?
if [ $ret -ne 0 ]; then
echo "Make build failed!"
cat "$buildlog" >&2
cd "$OLDPWD"
return $ret
fi
cd "$OLDPWD"
return $ret
}
# -----------------------------------------------------------------------------
# check():验证构建产物
# -----------------------------------------------------------------------------
check() {
echo "The test must be on an OpenHarmony device!"
}
# -----------------------------------------------------------------------------
# package():打包产物
# -----------------------------------------------------------------------------
package() {
: ${destdir:=${LYCIUM_ROOT}/usr/${pkgname}/${ARCH}}
# 只复制 libjnidispatch.so
mkdir -p "${destdir}/usr/lib"
_lib_path="${LYCIUM_ROOT}/../thirdparty/${pkgname}/${builddir}/build/native"
if [ -f "$_lib_path/libjnidispatch.so" ]; then
cp -f "$_lib_path/libjnidispatch.so" "${destdir}/usr/lib/"
chmod 755 "${destdir}/usr/lib/libjnidispatch.so"
echo " ✅ Installed libjnidispatch.so"
else
echo " ❌ libjnidispatch.so not found at $_lib_path"
return 1
fi
# 清理不需要的文件
find "${destdir}" -name "*.bak" -type f -delete 2>/dev/null || true
find "${destdir}" -name "*:Zone.Identifier" -type f -delete 2>/dev/null || true
return 0
}
# -----------------------------------------------------------------------------
# archive():生成归档包
# -----------------------------------------------------------------------------
archive() {
export HNP_TOOL="${HNP_TOOL:-${OHOS_SDK}/toolchains/hnpcli}"
mkdir -p ${LYCIUM_ROOT}/output/$ARCH
# 打包 tar.gz
pushd ${LYCIUM_ROOT}/usr/${pkgname}/${ARCH} > /dev/null 2>&1
tar -zcf ${LYCIUM_ROOT}/output/$ARCH/${pkgname}_${pkgver}.tar.gz .
echo "Archive completed: ${LYCIUM_ROOT}/output/$ARCH/${pkgname}_${pkgver}.tar.gz"
popd > /dev/null 2>&1
# 打包 HNP
if [ -f "${HNP_TOOL}" ]; then
cp ${LYCIUM_ROOT}/../thirdparty/${pkgname}/hnp.json ${LYCIUM_ROOT}/usr/${pkgname}/${ARCH}/
${HNP_TOOL} pack \
-i ${LYCIUM_ROOT}/usr/${pkgname}/${ARCH} \
-o ${LYCIUM_ROOT}/output/$ARCH/
echo "Archive completed: ${LYCIUM_ROOT}/output/$ARCH/${pkgname}.hnp"
else
echo "Warning: hnpcli not found at ${HNP_TOOL}, skipping HNP generation"
fi
}
# -----------------------------------------------------------------------------
# cleanbuild():清理构建产物
# -----------------------------------------------------------------------------
cleanbuild() {
echo "Cleaning build artifacts for ${pkgname}..."
# 清理构建目录
rm -rf "${LYCIUM_ROOT}/../thirdparty/${pkgname}/${builddir}"
# 清理 output
rm -rf "${LYCIUM_ROOT}/output/${pkgname}"*
# 清理 usr 产物
rm -rf "${LYCIUM_ROOT}/usr/${pkgname}"
echo "Clean completed"
}
HPKBUILD 核心结构说明:
- 第 1 部分:元信息:库名称定义为 jna,运行时依赖为空,JNA 无需额外 HNP 依赖包
- 第 2 部分:prepare () 准备函数:完成源码复制至编译目录,关闭 JAWT 组件以规避 X11 依赖,通过 ant javah 指令编译生成 JNI 头文件
- 第 3 部分:build () 构建函数:配置 aarch64-linux-ohos-clang 交叉编译工具链,采用安卓主机三元组编译 libffi 静态库,最终完成 libjnidispatch.so 动态库编译
- 第 4 部分:package () 打包函数:将编译产出的 libjnidispatch.so 库文件,拷贝至系统 usr/lib 目录完成部署
- 第 5 部分:archive () 归档函数:输出 tar.gz 格式压缩包,检测 hnpcli 工具是否存在,按需同步生成 hnp 安装包
构建流程:
prepare() → 准备源码、修改代码、生成 JNI 头文件
↓
build() → 交叉编译 libffi、编译 libjnidispatch.so
↓
package() → 安装 .so 文件到 usr/lib/
↓
archive() → 生成 tar.gz 和 hnp 包

3.4 创建 jna-disable-jawt.patch 补丁文件
什么是 patch 文件?
patch 文件是用于修改源码的文本文件,记录了需要修改的文件位置和修改内容。在 JNA 适配中,我们需要通过 patch 修改 dispatch.c 来禁用 JAWT 依赖。
为什么需要这个补丁:
- X11 依赖问题:JDK 的 jawt_md.h 硬编码包含 #include <X11/Xlib.h>
- 鸿蒙PC 无 X11:鸿蒙PC 是无头系统(headless),没有图形界面,不提供 X11 库
- JAWT 非必需:JNA 的核心功能不需要 JAWT,只有需要 Java GUI 组件才需要
- 编译阻断:不修改会导致编译错误 fatal error: ‘X11/Xlib.h’ file not found
创建方法:
--- a/native/dispatch.c
+++ b/native/dispatch.c
@@ -111,9 +111,15 @@
#include <stdlib.h>
#include <wchar.h>
-#include <jni.h>
+
+/* OpenHarmony: Disable JAWT to avoid X11 dependency */
+#ifndef NO_JAWT
+#define NO_JAWT 1
+#endif
+
+#include "com_sun_jna_Native.h"
+#include "com_sun_jna_Function.h"
-#ifndef NO_JAWT
+#ifdef DISABLE_JAWT_COMPLETELY
#include <jawt.h>
#include <jawt_md.h>
#endif
补丁修改说明:
- 第一处修改:在 #include <jni.h> 之前定义 NO_JAWT 宏
- 这个宏会告知 JNA 代码禁用 JAWT 相关功能
- 第二处修改:将 #include <jni.h> 替换为直接包含 JNI 头文件
- 跳过 jni.h 间接包含 jawt_md.h 的链条
- 直接包含 com_sun_jna_Native.h 和 com_sun_jna_Function.h
- 第三处修改:将 #ifndef NO_JAWT 改为 #ifdef DISABLE_JAWT_COMPLETELY
- DISABLE_JAWT_COMPLETELY 宏永远不定义,所以 JAWT 代码永远不会编译
使用方式:
# 在 HPKBUILD 的 prepare() 中应用 patch
cd "$builddir"
patch -p1 < "../jna-disable-jawt.patch"
注意事项:
- patch 文件可能因 JNA 版本不同而需要调整行号
- 本文 HPKBUILD 示例使用 sed 方式(更灵活),无需 patch 文件
- 如果你更喜欢使用 patch 文件,可以参考本节的创建方法

3.5 创建 hnp.json(包元数据)
什么是 hnp.json?
hnp.json 是鸿蒙 HNP 包的元数据文件,类似于 Node.js 的 package.json。它告诉系统:
- 这个包叫什么、什么版本、什么许可证
- 包含哪些文件、安装到哪些目录
- 依赖哪些其他包
创建方法:
{
"type": "hnp-config",
"name": "jna-native",
"version": "5.14.0",
"description": "Java Native Access native library for OpenHarmony",
"license": "LGPL-2.1",
"arch": "arm64-v8a",
"install": {
"lib": ["usr/lib/libjnidispatch.so"]
}
}
字段说明:
- type:固定为 “hnp-config”
- name:包名称(可以与 pkgname 不同)
- version:版本号,与 HPKBUILD 中的 pkgver 一致
- arch:目标架构,arm64-v8a 表示 64 位 ARM
- install.lib:要安装的库文件列表

3.6 创建 README.OpenSource(开源声明)
什么是 README.OpenSource?
README.OpenSource 是开源合规声明文件,记录:
- 使用了哪些开源组件
- 每个组件的许可证类型
- 上游源码地址和版本
这是鸿蒙生态的必备文件,用于满足开源许可证的法律要求。
创建方法:
[
{
"Name": "JNA (Java Native Access)",
"License": "LGPL-2.1",
"License File": "https://github.com/java-native-access/jna/blob/master/LICENSE",
"Version Number": "5.14.0",
"Owner": "your-email@example.com",
"Upstream URL": "https://github.com/java-native-access/jna",
"Description": "JNA provides Java programs easy access to native shared libraries without writing JNI code. This package contains the native dispatch library (libjnidispatch.so) for OpenHarmony."
}
]
注意:虽然文件名是 .OpenSource,但内容必须是合法的 JSON 数组格式。

3.7 创建 HPKCHECK 检查脚本
什么是 HPKCHECK?
HPKCHECK 是自动验证脚本,在构建后检查:
- 产物是否存在(.so 文件)
- 文件格式是否正确(ELF)
- 架构是否匹配(ARM64)
- 是否使用 musl libc
#!/bin/bash
HPK_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "${HPK_DIR}" || exit 1
source ./HPKBUILD > /dev/null 2>&1
logfile="${HPK_DIR}/${pkgname}_${ARCH}_test.log"
checkprepare() {
return 0
}
openharmonycheck() {
res=0
inst_lib="${LYCIUM_ROOT}/usr/${pkgname}/${ARCH}/lib"
if [ -d "${inst_lib}" ]; then
echo "start test times: $(date)" >> "${logfile}" 2>&1
# 检查 libjnidispatch.so
if [ -f "${inst_lib}/libjnidispatch.so" ]; then
echo "✅ libjnidispatch.so exists" >> "${logfile}" 2>&1
# 检查 ELF 格式
file "${inst_lib}/libjnidispatch.so" >> "${logfile}" 2>&1
ret=$?
res=$(( res | $ret ))
# 检查架构
if grep -q "ARM aarch64" "${logfile}"; then
echo "✅ Architecture is ARM64" >> "${logfile}" 2>&1
else
echo "❌ Architecture mismatch" >> "${logfile}" 2>&1
res=1
fi
# 检查 musl
if grep -q "musl" "${logfile}"; then
echo "✅ Using musl libc" >> "${logfile}" 2>&1
else
echo "⚠️ Not using musl libc" >> "${logfile}" 2>&1
fi
else
echo "❌ libjnidispatch.so not found" >> "${logfile}" 2>&1
res=1
fi
echo "end test times: $(date)" >> "${logfile}" 2>&1
else
echo "❌ ${inst_lib} directory not found" >> "${logfile}" 2>&1
res=1
fi
return $res
}
步骤说明:
HPKCHECK 是 lycium 框架的构建检查脚本,用于在编译前验证环境是否满足构建要求,以及在编译后验证产物是否正确。它相当于一个"自动化测试脚本",确保构建质量。
HPKCHECK 的作用:
- 环境验证:检查编译环境是否满足要求
- 产物验证:验证生成的库文件是否存在且格式正确
- 架构检查:验证 ELF 文件格式和目标架构
- 日志记录:记录测试结果到日志文件

四、编译流程与完整示例
4.1 环境准备
配置用于前置环境检查与变量设置,通过指定 鸿蒙PC SDK 路径、验证 JDK 和 Ant 环境,为 lycium_plusplus 构建 JNA 三方库提供基础运行环境
# 1. 确保 鸿蒙PC SDK 已安装
export OHOS_SDK=/home/weishuo/ohos-sdk/linux
# 2. 确保 JDK 11 和 Ant 已安装
java -version
javac -version
ant -version
# 3. 验证交叉编译工具链
ls ${OHOS_SDK}/native/llvm/bin/aarch64-linux-ohos-clang
# 应该输出:/home/weishuo/ohos-sdk/linux/native/llvm/bin/aarch64-linux-ohos-clang
4.2 创建项目结构并执行编译
通过目录操作、脚本创建、清理缓存、执行构建指令,完成 lycium_plusplus 中 JNA 三方库的从零构建全流程
# 1. 进入 thirdparty 目录并创建 jna 目录
cd /home/weishuo/lycium_plusplus/thirdparty
mkdir -p jna
cd jna
# 2. 创建 4 个核心文件(内容见第三章)
# HPKBUILD、hnp.json、README.OpenSource、HPKCHECK
# 可以使用 cat heredoc 方式创建(避免 CRLF 问题)
# 3. 进入 lycium 目录
cd /home/weishuo/lycium_plusplus/lycium
# 4. 清理历史构建记录(可选,但推荐)
grep -v 'jna' usr/hpk_build.csv > usr/hpk_build.csv.tmp 2>/dev/null || true
mv usr/hpk_build.csv.tmp usr/hpk_build.csv 2>/dev/null || true
rm -rf ../thirdparty/jna/jna-5.14.0
rm -rf output/*/jna*
# 5. 开始构建
./build.sh jna
4.3 构建成功输出示例
构建过程中,lycium 框架会依次执行 prepare()、build()、package()、archive() 四个阶段:
关键步骤说明:
- prepare() 阶段
- 复制源码到 jna-5.14.0/ 目录
- 使用 sed 修改 dispatch.c 禁用 JAWT
- 运行 ant javah 生成 6 个 JNI 头文件
- 复制头文件到 build/native/ 目录
- build() 阶段
- 配置并交叉编译 libffi 静态库(使用 aarch64-linux-android)
- 编译 dispatch.c 和 closures.c
- 链接生成 libjnidispatch.so
- package() 阶段
- 将 libjnidispatch.so 复制到 usr/lib/ 目录
- archive() 阶段
- 生成 jna_5.14.0.tar.gz 标准压缩包
- 生成 jna-native.hnp 鸿蒙安装包

4.4 验证产物
构建成功后,编译产物会统一输出至 output 目录,包含标准 tar 压缩包 与鸿蒙专用 hnp 格式包
查看产物文件:
# 1. 查看 output 目录
cd /home/weishuo/lycium_plusplus/lycium/output/arm64-v8a
ls -lh
验证 tar.gz 内容:
# 查看压缩包内容
tar tzf jna_5.14.0.tar.gz

五、鸿蒙 PC 真机验证:JNA
鸿蒙PC 环境下完成 JNA 动态库编译后,需要将 libjnidispatch.so 部署到设备并做全面校验,确保其格式、架构、依赖和导出符号均满足运行要求。下面分步介绍验证流程及所用命令,并提供一个自动化验证脚本
5.1 部署库文件:解压与自签名
首先将编译产物放入设备,解压归档文件,为 .so 文件添加鸿蒙系统所需的签名和执行权限。
# 查看当前目录文件(确认压缩包存在)
ls
# 静默解压 jna 压缩包(无警告输出)
tar -zxf jna_5.14.0.tar.gz 2>/dev/null
# 进入库文件目录
cd usr/lib
# 鸿蒙系统二进制文件自签名
binary-sign-tool sign -inFile libjnidispatch.so -outFile libjnidispatch.so -selfSign "1"
# 添加可执行权限(必须,否则系统无法加载)
chmod +x libjnidispatch.so
# 验证最终文件状态
ls -l

说明
- tar -zxf … 2>/dev/null:静默解压,避免输出干扰
- binary-sign-tool sign -selfSign “1”:为动态库添加 鸿蒙PC 自签名,系统在加载时会校验签名,未签名文件会被拒绝
- chmod +x:确保运行时加载器能够映射并执行该文件
5.2 基础信息校验:文件类型与动态依赖
确认库文件是合法的 ELF 动态库,并检查其依赖关系,排除对图形库(如 X11)的非预期依赖。
# 查看 libjnidispatch.so 的文件类型
# 输出显示它是 aarch64 架构的 ELF 动态库,格式本身没问题
file libjnidispatch.so
# 读取 ELF 文件的动态节信息
# 这里可以看到它只依赖了 libc.so,SONAME 是 ../build/native/libjnidispatch.so
readelf -d libjnidispatch.so

分析
- file 输出确认该文件为 64‑bit aarch64 ELF 共享对象,格式正确,无损坏。
- readelf -d 显示的 NEEDED 列表仅包含 libc.so,说明该库除了标准 C 库外没有其他运行时依赖(如 libm、libdl 等),在 鸿蒙PC 环境下加载风险极低。
- SONAME 字段为 …/build/native/libjnidispatch.so,虽带相对路径,但不影响 Native.loadLibrary() 找到同名文件,属于正常。
5.3 导出符号检查:确认 JNI 接口完整
JNA 通过 JNI 调用本地方法,因此必须确保 libjnidispatch.so 正确导出了所有 Java_ 开头的桥接函数。
# nm 命令:列出目标文件/库的符号表
# -D 参数:只列出动态符号(即对外导出、运行时可见的符号)
nm -D libjnidispatch.so | grep "T Java_"
# | grep "T Java_":过滤出类型为 T(text,即代码段)且以 Java_ 开头的符号
# 这些符号是 JNA 桥接层的核心本地方法,Java 侧要调用它们,必须在 .so 里存在且导出

说明
- nm -D 仅显示动态符号表,T 表示全局代码符号
- 过滤出的 Java_com_sun_jna_* 均为 JNA 的核心本地接口,全部以 T 形式存在,说明导出表完整,Java 层调用不会因符号丢失而失败。
5.4 架构匹配确认
通过 ELF 文件头再次验证目标架构,确保库与设备 CPU 完全匹配。
# 读取 ELF 文件头信息,过滤出 Class 和 Machine 字段
# -h 表示读取文件头(Header),grep 用来筛选关键信息
readelf -h libjnidispatch.so | grep "Class\|Machine"

输出解释
- Class: ELF64:说明该库为 64 位格式,只能运行在 64 位系统上,无法兼容 32 位环境。
- Machine: AArch64:表示目标指令集是 ARM64,与鸿蒙 PC 设备的 aarch64 架构一致,不存在交叉编译错误。
5.5 自动化验证脚本
为了快速完成上述检查并生成测试代码,可以编写一个验证脚本 verify_jna.sh,它会依次检验环境、格式、依赖、导出符号和 Java 运行时,并在条件满足时帮助编译、运行一个简单的 JNA 测试程序。
脚本内容
#!/bin/sh
# JNA OpenHarmony 真机验证脚本
# 使用方法:./verify_jna.sh
# 注意:使用 /bin/sh 确保鸿蒙 PC 兼容性
echo "============================================================"
echo "JNA OpenHarmony 真机验证"
echo "============================================================"
# 1. 检查当前目录
echo ""
echo "[1/6] 检查当前环境..."
CURRENT_DIR=$(pwd)
echo "当前目录: $CURRENT_DIR"
# 检查 libjnidispatch.so 是否存在
if [ -f "libjnidispatch.so" ]; then
echo "✅ 找到 libjnidispatch.so"
else
echo "❌ 未找到 libjnidispatch.so"
echo " 请在 libjnidispatch.so 所在目录执行此脚本"
exit 1
fi
# 2. 检查 ELF 格式
echo ""
echo "[2/6] 检查库文件格式..."
FILE_OUTPUT=$(file libjnidispatch.so)
echo "$FILE_OUTPUT"
# 检查是否为 ELF 格式
echo "$FILE_OUTPUT" | grep -q "ELF"
if [ $? -eq 0 ]; then
echo "✅ ELF 格式正确"
else
echo "❌ 不是 ELF 格式"
exit 1
fi
# 检查是否为 64 位
echo "$FILE_OUTPUT" | grep -q "64-bit"
if [ $? -eq 0 ]; then
echo "✅ 64 位库"
else
echo "❌ 不是 64 位库"
exit 1
fi
# 检查是否为 ARM64
echo "$FILE_OUTPUT" | grep -q "arm64\|aarch64\|AArch64"
if [ $? -eq 0 ]; then
echo "✅ ARM64 架构"
else
echo "⚠️ 架构可能不匹配(期望 ARM64)"
fi
# 3. 检查动态依赖
echo ""
echo "[3/6] 检查动态库依赖..."
echo ""
readelf -d libjnidispatch.so | grep "NEEDED"
# 检查是否依赖 libc
readelf -d libjnidispatch.so | grep -q "libc.so"
if [ $? -eq 0 ]; then
echo ""
echo "✅ 依赖 libc.so(正常)"
fi
# 检查是否有 X11 依赖(不应该有)
readelf -d libjnidispatch.so | grep -q "X11\|libX"
if [ $? -eq 0 ]; then
echo "❌ 发现 X11 依赖(JAWT 禁用失败)"
exit 1
else
echo "✅ 无 X11 依赖(JAWT 已禁用)"
fi
# 4. 检查 JNI 导出符号
echo ""
echo "[4/6] 检查 JNI 导出符号..."
JNI_COUNT=$(nm -D libjnidispatch.so 2>/dev/null | grep "T Java_" | wc -l)
if [ "$JNI_COUNT" -gt 0 ]; then
echo "✅ 发现 $JNI_COUNT 个 JNI 导出符号"
echo ""
echo "前 10 个 JNI 函数:"
nm -D libjnidispatch.so | grep "T Java_" | head -10
else
echo "⚠️ 未发现 JNI 导出符号(可能 nm 命令不可用)"
echo " 尝试使用 readelf 检查..."
readelf -s libjnidispatch.so | grep "Java_" | head -10
fi
# 5. 检查架构详情
echo ""
echo "[5/6] 检查架构详情..."
readelf -h libjnidispatch.so 2>/dev/null | grep "Class\|Machine"
# 6. 检查 Java 环境
echo ""
echo "[6/6] 检查 Java 环境..."
if command -v java >/dev/null 2>&1; then
JAVA_VERSION=$(java -version 2>&1 | head -1)
echo "✅ Java 已安装: $JAVA_VERSION"
# 检查是否有 JNA jar 包
echo ""
echo "检查 JNA jar 包..."
JNA_JAR=$(find . -name "jna-*.jar" -type f | head -1)
if [ -n "$JNA_JAR" ]; then
echo "✅ 找到 JNA jar 包: $JNA_JAR"
# 创建测试程序
echo ""
echo "创建测试程序..."
cat > JNATest.java << 'JAVA_EOF'
import com.sun.jna.Library;
import com.sun.jna.Native;
public class JNATest {
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int printf(String format, Object... args);
}
public static void main(String[] args) {
System.out.println("========================================");
System.out.println("JNA OpenHarmony 验证测试");
System.out.println("========================================");
try {
System.out.println("\n[测试] 调用 libc.printf");
CLibrary.INSTANCE.printf(" Hello from JNA on OpenHarmony!\n");
System.out.println("\n✅ JNA 工作正常!");
System.out.println("========================================");
} catch (UnsatisfiedLinkError e) {
System.err.println("\n❌ 本地库加载失败: " + e.getMessage());
System.err.println("请设置 LD_LIBRARY_PATH:");
System.err.println(" export LD_LIBRARY_PATH=$(pwd):$LD_LIBRARY_PATH");
System.exit(1);
} catch (Exception e) {
System.err.println("\n❌ 测试失败: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}
}
JAVA_EOF
echo "✅ 测试程序创建成功"
echo ""
echo "============================================================"
echo "✅ 所有检查通过!运行测试:"
echo "============================================================"
echo ""
echo " export LD_LIBRARY_PATH=$(pwd):\$LD_LIBRARY_PATH"
echo " javac -cp $JNA_JAR JNATest.java"
echo " java -cp $JNA_JAR:. JNATest"
echo ""
else
echo "⚠️ 未找到 JNA jar 包"
echo " 请下载: https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar"
echo ""
echo " 下载后运行:"
echo " export LD_LIBRARY_PATH=$(pwd):\$LD_LIBRARY_PATH"
echo " javac -cp jna-5.14.0.jar JNATest.java"
echo " java -cp jna-5.14.0.jar:. JNATest"
fi
else
echo "⚠️ Java 未安装或不在 PATH 中"
echo " 需要 Java 运行时才能测试 JNA"
fi
# 最终总结
echo ""
echo "============================================================"
echo "验证总结"
echo "============================================================"
echo ""
echo "✅ libjnidispatch.so 文件格式正确"
echo "✅ ARM64 架构匹配"
echo "✅ 无 X11 依赖(JAWT 已禁用)"
echo "✅ 依赖干净(仅 libc.so)"
echo ""
if [ "$JNI_COUNT" -gt 0 ]; then
echo "✅ JNI 导出符号正常($JNI_COUNT 个)"
fi
echo ""
echo "结论:JNA 本地库已成功适配到 OpenHarmony!"
echo "============================================================"


脚本分析
该脚本分为六个检查步骤:
- 环境检查 —— 确认当前目录包含 libjnidispatch.so
- ELF** 格式检验** —— 利用 file 命令验证格式是否为 64 位 ARM64 ELF,防止文件损坏或架构错误
- 动态依赖扫描 —— 通过 readelf -d 列出 NEEDED 项,确保没有 X11 等不该出现的依赖,仅依赖 libc.so。
- JNI 符号导出验证 —— 统计并展示 Java_ 开头的全局符号,确认桥接层接口完整
- 架构详情确认 —— 再次用 readelf -h 展示 ELF 头中的 Class 和 Machine 字段,增强可读性
- Java 环境与测试 —— 检测 Java 是否可用,查找 JNA JAR 包,生成并编译 JNATest.java,给出运行指令。若库文件和 Jar 包就绪,可直接编译运行一个调用 libc.printf 的简单示例来最终验证 JNA 是否能正常加载并工作
通过这套自动化和人工结合的校验流程,可以确定 libjnidispatch.so 已成功适配至 鸿蒙PC 平台,格式完整、依赖纯净、接口齐全,能够为上层 Java 应用提供可靠的 JNA 本地调用支持
六、常见问题与解决方案(FAQ)
6.1 编译错误类
Q1:构建时报错 Hunk #1 FAILED at 113 或 malformed patch at line 20?
A:这是 patch 文件上下文行号不匹配导致的。原因可能是:
- JNA 版本不同,dispatch.c 的实际行号有差异
- 文件有 CRLF 换行符,导致 patch 解析失败
- 之前的修改已经改变了文件内容
解决方案:使用 sed 命令代替 patch 文件,更加灵活可靠。
# 在 HPKBUILD 的 prepare() 函数中使用 sed
# 1. 添加 NO_JAWT 定义
sed -i '/#include <wchar.h>/a\
\
/* OpenHarmony: Disable JAWT to avoid X11 dependency */\
#ifndef NO_JAWT\
#define NO_JAWT 1\
#endif' native/dispatch.c
# 2. 替换 jni.h 包含
sed -i 's/^#include <jni.h>$/#include "com_sun_jna_Native.h"\n#include "com_sun_jna_Function.h"/' native/dispatch.c
# 3. 修改 JAWT 条件编译
sed -i 's/^#ifndef NO_JAWT$/#ifdef DISABLE_JAWT_COMPLETELY/' native/dispatch.c
Q2:编译时报错 fatal error: ‘X11/Xlib.h’ file not found?
A:原因是 JDK 的 jawt_md.h 硬编码需要 X11 头文件,但 OpenHarmony 是无头系统(headless),不需要图形界面。HPKBUILD 中已包含自动修复逻辑,通过 sed 修改 dispatch.c 禁用 JAWT。
# HPKBUILD 的 prepare() 函数中已包含自动修复代码
sed -i '/#include <wchar.h>/a\
\
/* OpenHarmony: Disable JAWT to avoid X11 dependency */\
#ifndef NO_JAWT\
#define NO_JAWT 1\
#endif' native/dispatch.c
sed -i 's/^#ifndef NO_JAWT$/#ifdef DISABLE_JAWT_COMPLETELY/' native/dispatch.c
原理说明:
- dispatch.c 第 114 行有 #include <jni.h>
- 在 Linux 上,jni.h 会包含 jawt_md.h
- jawt_md.h 第 29 行有 #include <X11/Xlib.h>
- 通过定义 NO_JAWT 宏并修改条件编译,跳过 JAWT 相关代码
Q3:编译时报错 configure: error: cannot run C compiled programs. If you meant to cross compile, use ‘–host’?
A:原因是 libffi 的 configure 脚本未识别交叉编译环境。需要在 make 命令中添加 HOST_CONFIG 和 FFI_CONFIG 参数,使用 aarch64-linux-android 而不是 aarch64-linux-ohos(老版本 libffi 不认识 ohos)。
# HPKBUILD 的 build() 函数中已配置
export FFI_CONFIG="--enable-static --disable-shared --with-pic=yes --host=aarch64-linux-android"
export HOST_CONFIG="--host=aarch64-linux-android"
# 传递给 make 命令
make \
HOST_CONFIG="--host=aarch64-linux-android" \
FFI_CONFIG="--enable-static --disable-shared --with-pic=yes --host=aarch64-linux-android"
为什么使用 android:
- 老版本 libffi 的 config.sub 不认识 ohos 目标
- aarch64-linux-android 与 鸿蒙PC 兼容(都是 musl libc)
- 这是经过验证的可行方案
Q4:编译时报错 libtool: error: cannot build a shared library?
A:原因是 LDFLAGS 环境变量中包含了 -shared 标志,传递给了 libffi 的构建,但 libffi 配置为静态库(–disable-shared),产生冲突。
解决方案:不设置全局 LDFLAGS 环境变量,让 Makefile 使用自己的默认值。
# ❌ 错误做法:设置 LDFLAGS 会影响 libffi
export LDFLAGS="--target=aarch64-linux-ohos --sysroot=${OHOS_SDK}/native/sysroot -shared"
# ✅ 正确做法:不设置 LDFLAGS,让 Makefile 处理
# Makefile 中已有:LDFLAGS=-o $@ -shared (用于链接 libjnidispatch.so)
Q5:编译时报错 fatal error: ‘com_sun_jna_Function.h’ file not found?
A:原因是 JNI 头文件未生成。需要安装 JDK 11 和 Ant,并在 prepare() 阶段运行 ant javah 生成头文件。
# 1. 安装 JDK 11 和 Ant
sudo apt install -y openjdk-11-jdk
sudo apt install -y ant
# 2. 设置 JAVA_HOME
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
# 3. 生成 JNI 头文件
cd jna-5.14.0
ant javah
# 4. 复制到头文件目录(Makefile 期望的位置)
mkdir -p build/native
cp -f build/headers/*.h build/native/
注意:make clean 会删除 build/native 目录,需要在 make clean 后重新复制头文件。
Q6:编译时报错 fatal error: ‘jni.h’ file not found?
A:原因是 CFLAGS 中缺少 JDK include 路径。需要添加 JDK 的 include 和 include/linux 目录。
export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64
export CFLAGS="... -I${JAVA_HOME}/include -I${JAVA_HOME}/include/linux"
完整 CFLAGS 示例:
export CFLAGS="--target=aarch64-linux-ohos \
--sysroot=${OHOS_SDK}/native/sysroot \
-O2 -fPIC -fno-strict-aliasing \
-I../build/native/libffi/include \
-I../build/native \
-I../build/headers \
-I${JAVA_HOME}/include \
-I${JAVA_HOME}/include/linux"
Q7:编译时报错 ld.lld: error: undefined symbol: main?
A:原因是链接共享库时缺少 -shared 标志,链接器误以为在构建可执行文件。这是因为 LDFLAGS 环境变量覆盖了 Makefile 的默认值。
解决方案:删除 LDFLAGS 环境变量,让 Makefile 使用默认配置。
Q8:构建时报错 $‘\r’: command not found?**
A:这是 Windows 换行符(CRLF)导致的问题。在 WSL + Windows 共享目录环境下,使用 Windows 编辑器创建的文件会自动带有 CRLF 换行符。解决方案是使用 Python 脚本修复。
# 创建 fix_crlf.py 脚本
python3 << 'EOF'
import os
files = [
"/home/weishuo/lycium_plusplus/thirdparty/jna/HPKBUILD",
"/home/weishuo/lycium_plusplus/thirdparty/jna/HPKCHECK"
]
for filepath in files:
with open(filepath, "rb") as f:
content = f.read()
content = content.replace(b"\r\n", b"\n").replace(b"\r", b"\n")
with open(filepath, "wb") as f:
f.write(content)
print(f"Fixed: {filepath}")
print("All files fixed!")
EOF
6.2 环境问题类
Q9:执行 ./build.sh jna 后,日志显示 ALL JOBS DONE!!! 但 output 目录为空?
A:这通常是因为 hpk_build.csv 中已有该包的构建记录,导致构建被跳过。需要手动清理构建记录和缓存。
cd lycium_plusplus/lycium
grep -v 'jna' usr/hpk_build.csv > usr/hpk_build.csv.tmp
mv usr/hpk_build.csv.tmp usr/hpk_build.csv
rm -rf ../thirdparty/jna/jna-5.14.0
rm -rf output/*/jna*
./build.sh jna
Q10:编译时报错 fatal error: ‘ffi.h’ file not found?
A:原因是 CFLAGS 中缺少 libffi 头文件路径。需要添加 libffi 编译后的 include 目录。
export CFLAGS="... -I../build/native/libffi/include"
完整构建路径说明:
- libffi 头文件:…/build/native/libffi/include/ffi.h
- JNI 头文件:…/build/native/com_sun_jna_*.h
- JDK 头文件:${JAVA_HOME}/include/jni.h
Q11:如何验证生成的库文件是否正确?
A:使用 file 和 nm 命令检查 ELF 文件格式和导出符号。
# 1. 检查 ELF 格式
file ~/lycium_plusplus/lycium/usr/jna/arm64-v8a/usr/lib/libjnidispatch.so
# 正确输出:
# ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV),
# dynamically linked, stripped
# 2. 检查 JNI 导出符号
nm -D ~/lycium_plusplus/lycium/usr/jna/arm64-v8a/usr/lib/libjnidispatch.so | grep "T Java_"
# 应该看到类似输出:
# 000000000000c454 T Java_com_sun_jna_Native__1getDirectBufferPointer
# 000000000000b42c T Java_com_sun_jna_Native__1getPointer
# 000000000000ad54 T Java_com_sun_jna_Native_close
# ...
6.3 使用与验证类
Q13:如何在鸿蒙 PC 上使用 JNA?
A:JNA 是纯运行时 Java 库,Java 端调用完全不需要 C 头文件。只需提供 libjnidispatch.so 文件,配合 jna.jar 使用。
// Java 代码,不需要任何 C 头文件
import com.sun.jna.Library;
import com.sun.jna.Native;
public class MyProgram {
public interface CLibrary extends Library {
CLibrary INSTANCE = Native.load("c", CLibrary.class);
int printf(String format, Object... args);
}
public static void main(String[] args) {
CLibrary.INSTANCE.printf("Hello from JNA on OpenHarmony!\n");
}
}
# 运行 Java 程序
export LD_LIBRARY_PATH=./usr/lib:$LD_LIBRARY_PATH
javac -cp jna-5.14.0.jar MyProgram.java
java -cp jna-5.14.0.jar:. MyProgram
Q14:JNA 需要安装头文件吗?
A:不需要。JNA 是纯运行时 Java 库,其 Java 端调用完全不需要 C 头文件(如 dispatch.h、protect.h、ffi.h)。libjnidispatch.so 已静态链接 libffi,所有底层调度由 JNA jar 包自动完成。用户只需提供 .so 文件,无需打包或安装任何 .h 头文件。
Q15:如何系统排查构建错误?
A:按以下步骤排查:
# 1. 查看完整日志
./build.sh jna 2>&1 | tee build.log
# 2. 确认失败阶段:prepare / build / package / archive
# 3. 检查 prepare()
ls -la thirdparty/jna/jna-5.14.0/
cat log/jna-javah.log
# 4. 检查 build()
cat log/jna-build.log
# 5. 检查 package() - 确认 destdir 路径和库文件是否存在
# 6. 检查 archive() - 确认打包路径是否正确
Q16:为什么选择本地源码而非 Git 下载?
A:本地源码模式更适合快速迭代开发,避免每次构建都重新下载。JNA 包含 native 目录和 Java 代码,本地源码可以确保版本一致性。对于已经验证过的稳定版本,可以改为从 GitHub 下载。
# 改为从 GitHub 下载(修改 HPKBUILD)
source="https://github.com/java-native-access/jna/archive/refs/tags/${pkgver}.tar.gz"
autounpack=true
downloadpackage=true
Q17:测试时遇到 UnsatisfiedLinkError 怎么办?
A:这通常是库路径设置问题。按以下步骤排查:
# 1. 检查 LD_LIBRARY_PATH
echo $LD_LIBRARY_PATH
# 应该包含 ./usr/lib
# 2. 检查库文件是否存在
ls -l ./usr/lib/libjnidispatch.so
# 3. 检查架构是否匹配
file ./usr/lib/libjnidispatch.so
# 必须是 ARM64
# 4. 检查依赖库
ldd ./usr/lib/libjnidispatch.so
# 确保所有依赖都找到
Q18:可以在其他架构(如 armeabi-v7a)上构建吗?
A:可以。修改 HPKBUILD 中的 archs 变量,并调整工具链:
# 修改 HPKBUILD
archs=("armeabi-v7a")
# 修改工具链(在 build() 函数中)
export CC="${OHOS_SDK}/native/llvm/bin/armv7a-linux-ohos-clang"
export CFLAGS="--target=armv7a-linux-ohos ..."
七、技术总结
本次将 JNA 本地库适配至 鸿蒙PC 平台,完整验证了 Java JNI 本地库在鸿蒙环境下的交叉编译、构建打包与依赖处理流程,形成了可复用的适配范式。通过规范 HPKBUILD 配置、禁用 JAWT 避免 X11 依赖、使用 android 兼容方案交叉编译 libffi、自动生成 JNI 头文件等关键处理,实现了库正常编译运行与轻量化部署,相关思路可广泛迁移至各类 Java JNI 本地库的鸿蒙移植工作。
- 建立了 Makefile 类项目标准化的 HPKBUILD 适配模板,明确交叉编译与 JNI 头文件生成要点
- 解决 X11 图形依赖、libffi 交叉编译、JNI 头文件自动生成、共享库链接配置等常见适配问题
- 通过纯运行时库设计实现零头文件交付,提升在鸿蒙设备上的部署效率
- 适配方案具备通用性,可直接用于其他 Java JNI 本地库(如 JNR、JNIWrapper)移植
- 为后续多架构扩展、Java 应用集成、自动化适配工具开发奠定基础
八、结语
本次实践成功将 JNA 本地库移植至 鸿蒙PC 平台,充分体现了 lycium_plusplus 框架对 Java JNI 项目交叉编译的支撑能力。通过合理运用 HPKBUILD 构建流程、禁用 JAWT 避免 X11 依赖、使用 android 兼容方案、规范 Make 编译配置、自动生成 JNI 头文件等关键措施,有效解决了适配中的兼容性与构建问题,为同类开源 Java JNI 本地库迁移至鸿蒙生态提供了可复用的思路与实践参考,也助力开源鸿蒙原生工具生态的完善与发展。
提示:本文基于 JNA 5.14.0 版本进行适配。不同版本的依赖和构建脚本可能有所差异,建议在适配前先熟悉目标项目的 Makefile 和构建配置文件。JNA 是纯运行时库,Java 端使用不需要任何 C 头文件,只需提供 libjnidispatch.so 配合 jna.jar 即可。
更多推荐




所有评论(0)