欢迎加入开源鸿蒙PC社区: https://harmonypc.csdn.net/

目标平台:HarmonyOS PC / 2in1。需求:在鸿蒙 PC 上做一个二维码生成器——
用户输入文本/网址 → 生成二维码 → 预览 → 保存到图库。
主线:用 Go(官方移植版 ohos_golang_go)做二维码核心,ArkTS 做桌面 UI,
中间用 NAPI 搭桥。

本文把环境搭建工程实现集成踩坑最终落地与验证一次性讲透,
看完即可复现。

鸿蒙 PC(2in1)上运行的二维码生成器

最终效果:应用以桌面窗口形态运行在鸿蒙 PC(2in1)上,输入内容即生成二维码。


目录

  1. 为什么是鸿蒙 PC + Go
  2. 环境搭建
  3. 工程搭建
  4. 用 Go 写二维码核心并交叉编译
  5. 集成的两个关键现实(避坑核心)
  6. 务实落地:C++ 写核心 + ArkTS UI
  7. 构建 / 安装 / 运行
  8. 验证:不止能跑,还要正确
  9. 结论与展望

1. 为什么是鸿蒙 PC + Go

鸿蒙 PC(2in1 形态,如 MateBook 系列)与手机共用同一套 OHOS 应用模型:UI 用 ArkTS,
原生能力走 NAPI。把应用声明为 2in1 设备类型即可在 PC 上运行。

选 Go 做核心,是因为 Go 的 skip2/go-qrcode 成熟、生成 PNG 只要几行。剩下的真正难题是:
怎么把 Go 编出来的东西塞进鸿蒙 PC 应用。 这是全文重点。


2. 环境搭建

2.1 系统与工具清单

本文环境为 macOS(Apple Silicon, arm64)+ zsh。实测可用的工具链:

工具 版本 用途
Go(系统版) go1.24.3 darwin/arm64 仅作 ohos_golang_gobootstrap
ohos_golang_go 基于 go1.22.10 交叉编译 GOOS=openharmony 的 Go 工具链
ark-cli (ark) 0.1.2 鸿蒙命令行脚手架(建项目/构建/安装/模拟器)
hvigor (hvigorw) 6.24.2 鸿蒙工程构建系统
ohpm 6.1.2 鸿蒙包管理器
node v23.11.0 hvigor 运行时
OpenHarmony SDK API 24 / 6.1.1.125 ArkTS 编译、NDK
NDK clang OHOS clang 15.0.4 C/C++ 交叉编译(aarch64-unknown-linux-ohos)
hdc 随 SDK 设备/模拟器调试

2.2 SDK 与 NDK 路径(重要坑)

环境变量 OHOS_NDK_HOME / OHOS_SDK 指向的 ~/Library/OpenHarmony/Sdk 实际不存在(失效配置)。
真实 SDK 位于:

~/command-line-tools/sdk/default/openharmony/native            # NDK(clang、sysroot)
~/command-line-tools/sdk/default/openharmony/toolchains/hdc    # hdc

NDK 交叉编译器(按目标架构提供 wrapper):

.../native/llvm/bin/aarch64-unknown-linux-ohos-clang     # arm64
.../native/llvm/bin/x86_64-unknown-linux-ohos-clang      # x86_64

# 验证
aarch64-unknown-linux-ohos-clang --version
# OHOS (dev) clang version 15.0.4   Target: aarch64-unknown-linux-ohos

脚本里应优先用环境变量,失效时回退到 ~/command-line-tools/sdk/default/openharmony/native

2.3 安装 ark-cli 与统一运行时

ark-cli(二进制名 ark)是轻量级命令行脚手架,内嵌 hdc,覆盖建项目/构建/安装/调试/模拟器,
可在无完整 DevEco Studio 时完成开发闭环。

第一步:安装本体(三选一)

# 方式一:克隆仓库安装(推荐,脚本自动编译并装入 PATH、建 ark 软链)
git clone https://atomgit.com/nutpi/ark-cli.git && cd ark-cli && bash scripts/install.sh

# 方式二:从源码构建(需 Rust:curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh)
git clone https://atomgit.com/nutpi/ark-cli.git && cd ark-cli
cargo build --release
cp target/release/ark-cli /usr/local/bin/ && ln -sf /usr/local/bin/ark-cli /usr/local/bin/ark

# 方式三:从 Releases 下载对应平台预编译二进制,放入 PATH

第二步:安装统一运行时(关键,否则 build/run/emulator 不可用)

ark runtime install     # 下载 SDK + hvigor/ohpm/node + Emulator 到 ~/.ark-cli/runtime/
ark runtime status      # 各组件状态,自愈失效路径
ark --version           # 0.1.2
ark devices             # 验证 hdc 与设备连接

2.4 构建 ohos_golang_go 移植版(关键)

标准 Go 不认识 GOOS=openharmony,必须用 OpenHarmony-SIG 的移植版:

仓库:https://gitee.com/openharmony-sig/ohos_golang_go(基于 go1.22.10)。
它新增了 GOOS=openharmony(runtime 含 rt0_openharmony_arm64.s 等),当前仅支持 arm64

# 1) 克隆(仓库 ~180MB,浅克隆即可)
cd /Volumes/coder/hmapp/qrcode
git clone --depth 1 https://gitee.com/openharmony-sig/ohos_golang_go.git ohos-go

# 2) 用系统 Go 作 bootstrap,编译工具链(约 1-3 分钟)
cd ohos-go/src
GOROOT_BOOTSTRAP="$(go env GOROOT)" ./make.bash

# 3) 验证:产出 ohos-go/bin/go
../bin/go version       # go version go1.22.10 darwin/arm64

2.5 模拟器准备(鸿蒙 PC / 2in1)

ark emulator list                 # 查看实例与已下载镜像
ark emulator start MateBook Pro   # 启动 2in1(PC)模拟器,API 24
.../toolchains/hdc list targets   # 127.0.0.1:5555  Connected

本机用 ark 内嵌 hdc 偶报“文件不存在”,改用 SDK 自带
~/command-line-tools/sdk/default/openharmony/toolchains/hdc 直连 127.0.0.1:5555 稳定可用。

2.6 环境自检

go version                                            # 系统 Go(bootstrap)
/Volumes/coder/hmapp/qrcode/ohos-go/bin/go version    # 移植版 Go(openharmony)
ark --version                                         # ark-cli
aarch64-unknown-linux-ohos-clang --version            # NDK clang
.../toolchains/hdc list targets                       # 模拟器在线

3. 工程搭建

3.1 创建工程

ark create qrapp --template default --bundle com.example.qrapp

default 模板不含 native,故手动添加 cpp/ 目录与构建配置。

3.2 工程结构

qrapp/
├─ entry/
│  ├─ src/main/
│  │  ├─ ets/
│  │  │  ├─ pages/Index.ets          # UI:输入/生成/预览/保存
│  │  │  └─ utils/QrService.ets      # 业务:原生调用/PixelMap/PNG/存图库
│  │  ├─ cpp/
│  │  │  ├─ napi_init.cpp            # NAPI 包装(C++ 生成二维码 RGBA)
│  │  │  ├─ CMakeLists.txt           # 编译 libentry.so + qrcodegen
│  │  │  ├─ qrcodegen/               # Nayuki qrcodegen (MIT)
│  │  │  └─ types/libentry/          # .so 的 ArkTS 类型声明
│  │  └─ module.json5                # deviceTypes 含 2in1
│  ├─ build-profile.json5            # externalNativeOptions / abiFilters
│  └─ oh-package.json5               # 依赖 libentry.so
├─ gocore/                           # 【归档】Go 二维码核心 + 交叉编译脚本
│  ├─ cmd/qrexec/main.go             # Go 可执行程序(已验证可在 OHOS 运行)
│  ├─ build-ohos.sh                  # 用 ohos_golang_go 交叉编译
│  └─ go.mod / go.sum
└─ docs/

声明目标设备类型为鸿蒙 PC(2in1):

// entry/src/main/module.json5
"deviceTypes": ["2in1", "tablet", "phone"]

4. 用 Go 写二维码核心并交叉编译

核心逻辑(gocore/):

import qrcode "github.com/skip2/go-qrcode"
// 生成中等纠错的 PNG 字节流
png, err := qrcode.Encode(content, qrcode.Medium, size)

用移植版交叉编译为 OHOS 产物(注意两个关键开关):

export GOROOT=/Volumes/coder/hmapp/qrcode/ohos-go
export GOTOOLCHAIN=local          # 坑1:否则 go.mod 的 go 指令会触发下载标准版 Go,前功尽弃
export GOPROXY=https://goproxy.cn,direct   # 坑2:默认 proxy 当时一直 Bad Gateway

CGO_ENABLED=1 GOOS=openharmony GOARCH=arm64 \
  CC=.../native/llvm/bin/aarch64-unknown-linux-ohos-clang \
  $GOROOT/bin/go build -trimpath -o libqrcore.so .

坑位提醒:

  • go.modgo 1.24 会让工具链尝试下载标准版 go1.24.0(不含 openharmony),
    需把 go.mod 降到 go 1.22 并设 GOTOOLCHAIN=local
  • 依赖下载用 GOPROXY=https://goproxy.cn,direct

到这里 Go 一切顺利:产物是合法的 AArch64 ELF、导出符号正常。
真正的考验在「怎么让鸿蒙 PC 应用用上它」。


5. 集成的两个关键现实(避坑核心)

把 Go 接进 OHOS 应用,直觉上有两条路。我都走通到最后一步,再分别被平台拦下——
无论手机还是 PC,结论一致(同一套 OHOS / musl)。

5.1 路线 A:Go 编成 .so,经 NAPI dlopen 加载 —— 被 musl TLS 拒绝

-buildmode=c-shared 编出 libqrcore.so,C++ 写 NAPI 包装层链接它,HAP 打包、装到
鸿蒙 PC、启动——全部成功。HAP 内三个库齐全:

libs/arm64-v8a/libentry.so      # NAPI 包装
libs/arm64-v8a/libqrcore.so     # Go 核心
libs/arm64-v8a/libc++_shared.so

但点「生成」就报错(hilog):

W C03f04/MMG: Error relocating .../libqrcore.so:
    initial-exec TLS resolves to dynamic definition in .../libqrcore.so
    load module default/entry failed.. load failed
E C03f00/ArkCompiler: export objects of native so is undefined, so name is ...libentry.so

根因:Go runtime 需要随时拿到当前 goroutine 指针 g,它通过 TLS 访问,用的是
initial-exec(IE) 模型——该模型假设符号所在模块在程序启动时就已加载。
.so 重定位果然有一条 IE 的 R_AARCH64_TLS_TPREL64

而鸿蒙的 libc 是 musl。glibc 为「后期 dlopen 进来又用 IE-TLS」的库预留了 static TLS
surplus 兜底,musl 没有这个机制——重定位时直接判定该 TLS 符号属于 dlopen 模块
tls_id > static_tls_cnt),IE 无法满足,报错拒绝。

决定性实验:写最小 C 程序在全新进程里直接 dlopen 这个 .so,照样失败——
排除「surplus 被占满」,坐实是 musl 的根本限制:

void* h = dlopen("/data/local/tmp/libqrcore.so", RTLD_NOW);
// DLOPEN_FAIL: ... initial-exec TLS resolves to dynamic definition

真正的修复是 Go 上游 PR #75048(给 arm64/amd64
引入 TLSDESC / global-dynamic,musl 对这两种支持 dlopen),但它尚未合并,移植版也没跟进。

这坑不是鸿蒙独有:Alpine(也是 musl)上 Go 的 c-shared + dlopen 同样炸,见
golang/go#54805;sing-box 在鸿蒙上踩过同款
(issue #3681)。

5.2 路线 B:Go 编成可执行程序,fork/exec 子进程 —— 被 W^X 拒绝

IE-TLS 在「进程启动时加载」是合法的(那时模块就在静态 TLS 集合里)。于是把 Go 编成
可执行程序,由 C++ NAPI fork/execve 当子进程调用。

先验证 Go 程序能在鸿蒙上跑——把 Go 改成命令行程序,hdc shell 直接运行:

$ ./qrexec 'https://harmonyos.dev/go' out.png 384
605
$ head -c 8 out.png | xxd
00000000: 8950 4e47 0d0a 1a0a   .PNG....

605 字节、PNG 魔数正确——证明 Go 核心在 OHOS 上完全正确,唯一问题是「应用内加载」

把可执行文件命名为 libqrexec.so 混进 libs/(Android 经典技巧),C++ 用 dladdr 定位、
execv 调用。启动、点生成——失败:

E QRGO: exec not X_OK errno=13 path=.../libs/arm64/libqrexec.so   # bundle 文件无执行权限
I QRGO: qrexec exit code=113                                       # 113 = 100 + errno(13=EACCES)

复制到沙箱 cache、chmod 0755 再 execve——仍 EACCES根因:HarmonyOS 执行
W^X(Write XOR eXecute) 安全模型,应用进程无权 execve 沙箱内任何二进制(只能 dlopen .so)。
这是平台设计,鸿蒙 PC 同样如此。

5.3 小结

集成方式 结果 原因
NAPI dlopen Go .so musl 不支持 dlopen 含 IE-TLS 的库
execve Go 可执行程序 OHOS W^X 禁止应用执行沙箱内二进制

当前(HarmonyOS 6.1.1 / API 24)Go 无法嵌入应用运行时。 但 Go 交叉编译这条链路是通的、
Go 程序在 OHOS 上也能跑——这部分成果完整保留,等 PR #75048 落地即可复活纯 Go 方案。


6. 务实落地:C++ 写核心 + ArkTS UI

撞墙后的正确姿势是务实交付:运行时改用 C++(NAPI)实现二维码,Go 交叉编译成果归档

6.1 原生层(C++ NAPI + qrcodegen)

核心库用 Nayuki qrcodegen
(单文件 C,MIT,久经考验)。C++ 生成矩阵、渲染 RGBA 返回 ArkTS:

// generateQrCode(content, size) -> { data: ArrayBuffer(RGBA), width }
bool ok = qrcodegen_encodeText(content.c_str(), tmp.data(), qr.data(),
            qrcodegen_Ecc_MEDIUM, qrcodegen_VERSION_MIN,
            qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true);
int n = qrcodegen_getSize(qr.data());     // 模块边数
// 加 4 模块静默区,按 size 整数倍放大,白底黑码填进 RGBA8888 缓冲...

模块注册(名 entry,对应导入名 libentry.so):

static napi_module qrModule = { .nm_register_func = Init, .nm_modname = "entry", ... };
extern "C" __attribute__((constructor)) void RegisterEntryModule(void) {
    napi_module_register(&qrModule);
}

CMakeLists.txt

add_library(entry SHARED napi_init.cpp qrcodegen/qrcodegen.c)
target_link_libraries(entry PUBLIC libace_napi.z.so)

entry/build-profile.json5 启用原生构建:

"buildOption": {
  "externalNativeOptions": {
    "path": "./src/main/cpp/CMakeLists.txt",
    "abiFilters": ["arm64-v8a", "x86_64"]
  }
}

entry/oh-package.json5 声明 .so 依赖:

"dependencies": { "libentry.so": "file:./src/main/cpp/types/libentry" }

6.2 业务层(ArkTS)

QrService.ets

  • createQr():调原生 → image.createPixelMap(rgba, {pixelFormat: RGBA_8888, size})
  • packToPng()image.createImagePacker().packing(pixelMap, {format:'image/png'})
  • saveToAlbum()photoAccessHelper.createAsset(IMAGE,'png') + fileIo 写入。
const qr = generateQrCode(text, 512);                 // 原生
this.pixelMap = await image.createPixelMap(qr.data,
  { pixelFormat: image.PixelMapFormat.RGBA_8888,
    size: { width: qr.width, height: qr.width } });

// SaveButton 回调里
const png = await image.createImagePacker().packing(this.pixelMap,
  { format: 'image/png', quality: 100 });
const uri = await helper.createAsset(photoAccessHelper.PhotoType.IMAGE, 'png');  // + fileIo 写入

6.3 UI 层(ArkTS)

Index.etsTextInput 输入 → Button 生成 → Image(pixelMap) 预览 →
安全控件 SaveButton 保存(点击即获临时授权,无需申请持久化相册权限——桌面端体验尤其顺手)。

SaveButton({ text: SaveDescription.SAVE_IMAGE })
  .onClick((event, result) => {
    if (result === SaveButtonOnClickResult.SUCCESS) { this.onSave(); }
  });

界面状态(下图为模拟器,UI 在手机/PC 形态一致):生成前的空预览区 → 生成后的二维码。

初始界面 生成二维码
初始界面 生成二维码

7. 构建 / 安装 / 运行

# (可选,归档) 交叉编译 Go,验证 ohos_golang_go 链路
bash gocore/build-ohos.sh

# 构建 HAP
cd qrapp && ark build --mode debug
# 产物: entry/build/default/outputs/default/entry-default-unsigned.hap

# 安装到鸿蒙 PC 模拟器并启动
HDC=~/command-line-tools/sdk/default/openharmony/toolchains/hdc
$HDC install -r entry/build/default/outputs/default/entry-default-unsigned.hap
$HDC shell aa start -b com.example.qrapp -a EntryAbility

本模拟器接受 unsigned HAP 直接 hdc install;真机/部分模拟器需配置签名
build-profile.json5signingConfigs,或用 DevEco 自动签名)。


8. 验证:不止能跑,还要正确

NAPI 模块加载 —— 无 export objects undefined 错误(C++ 方案不含 Go TLS 依赖)。

生成正确性 —— 把界面截图喂 OpenCV 解码:

import cv2
data, _, _ = cv2.QRCodeDetector().detectAndDecode(cv2.imread('screen.png'))
print(data)   # https://harmonyos.dev/go-qrcode  ← 与输入完全一致

保存到图库 —— 点保存,安全控件弹出系统授权框(仅当次点击有效,无需持久化权限):

保存授权弹窗

授权后,hilog 显示媒体库新建资源、缩略图生成:

MediaLibrary: [update] ... fileId_: 1, ownerAlbumId_: 12 ... thumbnailVisible_: 0 -> 1
MediaLibrary: Thumbnail PostProcess GenRes 1

整个 UI 测试是脚本驱动的:hdc shell uinput -K -t <文本> 输入、uinput -T -c x y 点击、
snapshot_display 截图,再用 OpenCV 校验——全程没点一下手。

结果
HAP 打包 libentry.so(含 qrcodegen) + libc++_shared.so,arm64-v8a / x86_64
NAPI 模块加载 ✅ 无 export objects undefined
UI 渲染 ✅ 标题/输入框/按钮/预览区/保存按钮正常
生成二维码 ✅ 预览区显示
二维码正确性 ✅ OpenCV 解码 = 输入文本
保存到图库 ✅ MediaLibrary 新建资源 + 缩略图

9. 结论与展望

  1. 先验证最危险的假设。 本项目最大风险从来不是写代码,而是「Go 能不能在 OHOS 应用里跑」。
    早点做最小 dlopen 实验、hdc 直跑实验,能极大缩短试错链路。
  2. 报错要追到根。 export objects undefined 只是表象,往下是
    initial-exec TLS resolves to dynamic definition,再往下是 musl 与 glibc 的 TLS 机制差异。
  3. 平台安全模型是硬约束。 W^X 不是 bug、不是配置,是设计;与它较劲不如绕开。
  4. 鸿蒙 PC 应用的原生逻辑,目前请用 C/C++(NAPI),UI 用 ArkTS;Go 暂时进不了应用运行时
    (dlopen 被 musl IE-TLS 限制、execve 被 W^X 限制,PC 与手机一致)。
  5. 但 Go 交叉编译链路是通的ohos_golang_go 能把 Go 编成 OHOS 程序并实跑,适合放在
    与设备解耦的服务端/工具链里;待 Go PR #75048
    (TLSDESC)合并并被移植版跟进,纯 Go + NAPI 的鸿蒙 PC 应用即可成真。
  6. 务实交付:本项目最终是一个完整可运行的鸿蒙 PC(2in1)二维码应用(C++ + ArkTS),
    并完整保留、文档化了 Go 的交叉编译成果与平台限制分析。
Logo

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

更多推荐