当 Golang 遇上鸿蒙

1,486 阅读7分钟

本文我们将简单尝试使用 Golang 开发鸿蒙上面的模块。


2024.09.07 更新:

在与群友的交流中,我们发现可以通过一些绕过的实现来实现 Go 在鸿蒙上的正常使用。这里有两个方案来实现调用:

  1. 直接将 GOOS 修改为android,构建的时候将安卓的 android/log.h 文件和对应的动态链接库添加到构建过程中以完成构建,最终使用dlopen进行调用。

    • 笔者自己没试过这种方法,群里有同学已经跑通 具体请参考:b23.tv/jEI3oiM
    • 群友反馈只能用 dlopen 调用,具体未验证。
  2. 简单修改 go 的源码来实现构建。本质上与第一种方式没有区别,在构建的时候仍然需要将 GOOS 修改为 android,不过我们将对安卓日志的依赖直接移除了。
    本地构建出魔改的版本替代官方版本进行构建即可,此方式构建的动态/静态链接库可以与其他的链接库一样正常使用,支持正常 link 逻辑。

    image.png


这里先给出一个直接的结论:Golang 构建的模块无法在鸿蒙上被正常使用。

本文后续所有内容是认为 Golang 可以正常构建和执行的流程进行分析和实践,不过最后一步执行会出现问题。

本文相关代码在 go-example 可以找到。

背景

最近在社区和交流群中看到许多开发者在询问:如何将使用 Golang 开发的动态/静态链接库在鸿蒙中进行适配?

恰巧最近将 zig 的构建系统尝试实现了对鸿蒙的构建支持,因此我们可以使用 zig 构建系统来提供给 go 实现交叉编译到鸿蒙生态中。因此笔者做了一些简单的尝试,本文将简单讲解尝试过程以及最终的一些结果。

初识 Golang

简介

我们在之前的一些文章中讲过,鸿蒙原生模块开发本质上对于语言没有太大的限制,只要是能够构建出符合标准的动态/静态链接库即可。

比如与鸿蒙非常类似的 Node 生态中,有使用 Rust 进行开发的 swc/oxc 等工具,也有使用 Golang 开发的 esbuild 等工具。那么类似的我们依旧可以在鸿蒙上也使用 go 开发一些能力来提供给上层的 ArkTS 使用。

关于 go 的环境配置、项目初始化等能力这里不做过多的讲解,网上有大把参考资料可供学习。

FFI

这里我们简单讲一些 go 的 ffi 能力,与很多语言相同。go 提供了开箱即用的 ffi 能力,不过仍然需要做一些额外的数据处理以及适配工作。

对于 go 很熟悉的读者可以直接跳过这一部分。

Go 中提供了C包,提供了与 C ABI 桥接的各种能力,包括数据定义等。一个标准的使用 go 暴露 C ABI 接口的代码如下所示:

package main

import "C"

//export Add
func Add(a, b int) int {
	return a + b
}

func main() {}

这里有几点需要特别注意:

  1. 对于需要导出的函数和方法必须使用注释声明,否则将无法导出。
  2. main函数是必须的,即使没有任何任务也必须声明。
  3. export 注释必须遵循规范,在编写代码的时候会有一定的提示,如下所示: 17226515676513.jpg

这样我们就完成了一个简单的 C ABI 声明。

现在我们尝试将其构建为对应平台上动态链接库:

  • Mac: xx.dylib
  • Linux: xx.so
  • Windows: xx.dll

一个标准的使用 CGO 构建的命令如下所示:

CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../go-shared/libadd.dylib -buildmode=c-shared ../go-shared/add.go

简单讲解下这里配置参数:

  • GGO_ENABLED
    允许使用 CGO 进行编译
  • GOOS
    构建的目标平台系统
  • GOARCH
    构建的目标平台系统的架构
  • -o
    最终的输出产物名
  • -buildmode
    构建产物类型:c-shared c-archive 动态/静态链接库
  • xx
    构建的文件

构建完成之后我们会得到两个文件:libadd.dyliblibadd.h 分别为产物以及其头文件。然后我们可以编写一个简单的 C 来验证我们的动态链接库是否可以正常使用。

#include "libadd.h"
#include "stdio.h"

int main() {
    int ret = Add(1,2);
    printf("%d",ret);
}

然后构建并且尝试运行即可:

# 构建时需要指定链接的路径以及动态链接库
gcc ../go-shared/main.c -o main -L../go-shared -ladd

cp ../go-shared/libadd.dylib ./

./main

然后我们可以看到已经能够正常的使用并且输出了对应的结果

17232548510439.jpg

这就是使用 CGO 的一个基本流程,其他的内容这里就不再赘述。

Zig with go

接下来回到正题,我们讲过 zig 提供了一个非常健全和便捷的构建系统。

这个构建系统不仅能够在 zig 本身使用,还能够让我们在其他语言中体验到相当便利的构建能力,比如我们在之前的文章中使用 zig 来构建 rust 的代码使其运行在鸿蒙系统上面。同样的,我们也能够使用 zig 在 golang 上面替代其默认的构建系统从而体验到 zig 带来的便利构建能力。

初试

我们直接使用刚才的例子来尝试使用 zig 作为 CGO 的构建系统能力。只需要为构建命令新增CCCXX环境变量即可,那么我们的命令就应该如下所示:

CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 CC="zig cc -target aarch64-macos" CXX="zig c++ -target aarch64-macos" go build -o ../go-shared/libadd.dylib -buildmode=c-shared ../go-shared/add.go

不过这里没有办法给大家演示其正确性,因为 zig 在 0.12 以及 0.13 的版本中引入了一些 bug 会导致无法构建。

具体问题可以参考 issue

构建 go 到鸿蒙

与使用 zig 构建其他平台的能力类似,我们只需要将 zig 的 target 修改成为鸿蒙的即可。同时我们的构建产物应该输出为.so,因此我们的构建命令如下所示:

CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC="zig cc -target aarch64-linux-ohos" CXX="zig c++ -target aarch64-linux-ohos" go build -o ../go-shared/libadd.so -buildmode=c-shared ../go-shared/add.go

注意:这里的 zig 不能使用官方提供的版本,官方提供的版本无法对鸿蒙使用 zig cc 能力,具体可以参考之前的文章 当zig-lang构建系统遇上鸿蒙

尝试构建运行,我们发现可以正常得到鸿蒙的产物。

17232566722950.jpg

验证

接下来我们使用 ohos-rs 来简单编写一个包来验证我们的能力是否完整。

我们在lib.rs中声明我们将要通过 ffi 调用的函数能力。

use std::ffi::c_int;

use napi_derive_ohos::napi;

extern "C" {
    pub fn Add(left: c_int, right: c_int) -> c_int;
}

#[napi]
pub fn add(left: i32, right: i32) -> i32 {
    unsafe { Add(left, right) }
}

然后在build.rs中声明我们需要链接的动态库即可。

use std::env;

fn main() {
  napi_build_ohos::setup();

  let dir = env::current_dir().unwrap();
  let binding = dir.parent().unwrap().join("go-shared");

  println!("cargo:rustc-link-search={}",binding.to_str().unwrap());
  println!("cargo:rustc-link-lib=dylib=add");
}

注意:我们现在应该只构建 arm64 架构的产物,因为我们只提供了 arm64 架构下的libadd.so文件。

因此,我们构建命令如下所示:

ohrs build --arch aarch

17232576706837.jpg

现在我们只需要将构建产物在鸿蒙中正常调用即可。

import { add } from 'libhello.so';

@Entry
@Component
struct Index {

  build() {
    Row() {
      Column() {
        Button('add').onClick(() => {
          const ret = add(1,2);
          console.log(`ret is:${ret}`);
        })
      }
      .width('100%')
    }
    .height('100%')
  }
}

当然不出意外的话,应该是出意外了~ 😭 当我们尝试运行的时候不出意外的应该是报错了,应用直接 crash了,报错信息如下所示:

17232580803508.jpg

看起来应该是 native 模块有问题导致的,我们在 hilog 中查询相关日志会发现这样一个错误:

17232581467518.jpg

到这里,已成艺术(划掉。这里就回到了文章的最开头讲的内容:Go 目前无法在鸿蒙上正常运行。

这是因为 go 对于 musl 基础库的兼容性不是特别好,而鸿蒙本身的 C 基础库就是基于 musl 实现的,因此运行时会出现一些问题,具体的问题可以参考 issue

到这里我们就讲完了 go 目前在鸿蒙上的原生模块的一些尝试,有相关小伙伴想要尝试使用 go 开发鸿蒙能力的基本上可以放弃了,当然这是我个人目前得出的结论,如果有什么不对的或者说遗漏导致结论错误,希望你能给予指正。

希望本文对你有所帮助~