摘要:本文详细介绍了如何将 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 依赖。

为什么需要这个补丁

  1. X11 依赖问题:JDK 的 jawt_md.h 硬编码包含 #include <X11/Xlib.h>
  2. 鸿蒙PC 无 X11:鸿蒙PC 是无头系统(headless),没有图形界面,不提供 X11 库
  3. JAWT 非必需:JNA 的核心功能不需要 JAWT,只有需要 Java GUI 组件才需要
  4. 编译阻断:不修改会导致编译错误 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

补丁修改说明

  1. 第一处修改:在 #include <jni.h> 之前定义 NO_JAWT 宏
    1. 这个宏会告知 JNA 代码禁用 JAWT 相关功能
  2. 第二处修改:将 #include <jni.h> 替换为直接包含 JNI 头文件
    1. 跳过 jni.h 间接包含 jawt_md.h 的链条
    2. 直接包含 com_sun_jna_Native.h 和 com_sun_jna_Function.h
  3. 第三处修改:将 #ifndef NO_JAWT 改为 #ifdef DISABLE_JAWT_COMPLETELY
    1. 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 的作用

  1. 环境验证:检查编译环境是否满足要求
  2. 产物验证:验证生成的库文件是否存在且格式正确
  3. 架构检查:验证 ELF 文件格式和目标架构
  4. 日志记录:记录测试结果到日志文件

四、编译流程与完整示例

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() 四个阶段:

关键步骤说明

  1. prepare() 阶段
    1. 复制源码到 jna-5.14.0/ 目录
    2. 使用 sed 修改 dispatch.c 禁用 JAWT
    3. 运行 ant javah 生成 6 个 JNI 头文件
    4. 复制头文件到 build/native/ 目录
  2. build() 阶段
    1. 配置并交叉编译 libffi 静态库(使用 aarch64-linux-android)
    2. 编译 dispatch.c 和 closures.c
    3. 链接生成 libjnidispatch.so
  3. package() 阶段
    1. 将 libjnidispatch.so 复制到 usr/lib/ 目录
  4. archive() 阶段
    1. 生成 jna_5.14.0.tar.gz 标准压缩包
    2. 生成 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 "============================================================"

脚本分析

该脚本分为六个检查步骤:

  1. 环境检查 —— 确认当前目录包含 libjnidispatch.so
  2. ELF** 格式检验** —— 利用 file 命令验证格式是否为 64 位 ARM64 ELF,防止文件损坏或架构错误
  3. 动态依赖扫描 —— 通过 readelf -d 列出 NEEDED 项,确保没有 X11 等不该出现的依赖,仅依赖 libc.so。
  4. JNI 符号导出验证 —— 统计并展示 Java_ 开头的全局符号,确认桥接层接口完整
  5. 架构详情确认 —— 再次用 readelf -h 展示 ELF 头中的 Class 和 Machine 字段,增强可读性
  6. 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 文件上下文行号不匹配导致的。原因可能是:

  1. JNA 版本不同,dispatch.c 的实际行号有差异
  2. 文件有 CRLF 换行符,导致 patch 解析失败
  3. 之前的修改已经改变了文件内容

解决方案:使用 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 即可。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐