CGO 是一种在 iOS 平台上运行 Go 代码的方案。而 WireGuard 一种 VPN 技术,其中包含了一部分 Go 代码,同时项目提供了一个 Makefile 脚本,使用 CGO 来将相关代码编译成 iOS 静态库。

写这篇文章的契机是,因为希望使用 Mac Catalyst 将内部工具带到 macOS 平台,我又回过头去看一年前(恰巧是22年3月)研究 WireGuard 的过程。

发现虽然当时的研究成功,.xcframewrok 文件还在,但是过程已经模糊不清了。另外还需要针对 Mac Cataglyst 构建出新的静态库,所以又要从头进行梳理不过好在是 “模糊不清”,而不是 “一干二净”,多少还记得一些。

Makefile

我对 Go 语言几乎可以说是一窍不通的。

而对于 Makefile 也只有很少很少的了解。
想要执行 Makefile 文件中的脚本,就在命令行中,在进入对应目录后执行以下命令:

# 前提是说,电脑上要安装 make。一般安装了 Xcode 命令行的话都是有的。
make

这里我将该 Makefile 先贴过来:

# These are generally passed to us by xcode, but we set working defaults for standalone compilation too.
ARCHS ?= x86_64 arm64
PLATFORM_NAME ?= macosx
SDKROOT ?= $(shell xcrun --sdk $(PLATFORM_NAME) --show-sdk-path)
CONFIGURATION_BUILD_DIR ?= $(CURDIR)/out
CONFIGURATION_TEMP_DIR ?= $(CURDIR)/.tmp

export PATH := $(PATH):/usr/local/bin:/opt/homebrew/bin
export CC ?= clang
LIPO ?= lipo
DESTDIR ?= $(CONFIGURATION_BUILD_DIR)
BUILDDIR ?= $(CONFIGURATION_TEMP_DIR)/wireguard-go-bridge

CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -isysroot $(SDKROOT) -arch
GOARCH_arm64 := arm64
GOARCH_x86_64 := amd64
GOOS_macosx := darwin
GOOS_iphoneos := ios

build: $(DESTDIR)/libwg-go.a
version-header: $(DESTDIR)/wireguard-go-version.h

REAL_GOROOT := $(shell go env GOROOT 2>/dev/null)
export GOROOT := $(BUILDDIR)/goroot
$(GOROOT)/.prepared:
	[ -n "$(REAL_GOROOT)" ]
	mkdir -p "$(GOROOT)"
	rsync -a --delete --exclude=pkg/obj/go-build "$(REAL_GOROOT)/" "$(GOROOT)/"
	cat goruntime-*.diff | patch -p1 -f -N -r- -d "$(GOROOT)"
	touch "$@"

define libwg-go-a
$(BUILDDIR)/libwg-go-$(1).a: export CGO_ENABLED := 1
$(BUILDDIR)/libwg-go-$(1).a: export CGO_CFLAGS := $(CFLAGS_PREFIX) $(ARCH)
$(BUILDDIR)/libwg-go-$(1).a: export CGO_LDFLAGS := $(CFLAGS_PREFIX) $(ARCH)
$(BUILDDIR)/libwg-go-$(1).a: export GOOS := $(GOOS_$(PLATFORM_NAME))
$(BUILDDIR)/libwg-go-$(1).a: export GOARCH := $(GOARCH_$(1))
$(BUILDDIR)/libwg-go-$(1).a: $(GOROOT)/.prepared go.mod
	go build -ldflags=-w -trimpath -v -o "$(BUILDDIR)/libwg-go-$(1).a" -buildmode c-archive
	rm -f "$(BUILDDIR)/libwg-go-$(1).h"
endef
$(foreach ARCH,$(ARCHS),$(eval $(call libwg-go-a,$(ARCH))))

$(DESTDIR)/wireguard-go-version.h: go.mod $(GOROOT)/.prepared
	 sed -E -n 's/.*golang\.zx2c4\.com\/wireguard +v[0-9.]+-[0-9]+-([0-9a-f]{8})[0-9a-f]{4}.*/#define WIREGUARD_GO_VERSION "\1"/p' "$<" > "$@"

$(DESTDIR)/libwg-go.a: $(foreach ARCH,$(ARCHS),$(BUILDDIR)/libwg-go-$(ARCH).a)
	@mkdir -vp "$(DESTDIR)"
	$(LIPO) -create -output "$@" $^

clean:
	rm -rf "$(BUILDDIR)" "$(DESTDIR)/libwg-go.a" "$(DESTDIR)/wireguard-go-version.h"

install: build

.PHONY: clean build version-header install

这个脚本分为以下几个部分:

  • 定义变量:最开始的几行都是定义变量,其中 ?= 语法则是设置默认值,意思是可以从外部设置该值。
  • 定义目标:此后脚本定义了4个目标,分别是 buildversion-headercleaninstall。其中 version-header 目标内还定义了一个函数:libwg-go-a
  • 目标的执行顺序:最后的 .PHONY 用来定义目标执行顺序,脚本将按照该值的顺序去执行目标。

看完大块我们来看细节。

定义变量

在此不解释每个变量的作用,先重点关注下面几个:

ARCHS ?= x86_64 arm64
PLATFORM_NAME ?= macosx
SDKROOT ?= $(shell xcrun --sdk $(PLATFORM_NAME) --show-sdk-path)

GOARCH_arm64 := arm64
GOARCH_x86_64 := amd64
GOOS_macosx := darwin
GOOS_iphoneos := ios

上面已经提到了,这个 Makefile 是用来构建 iOS 静态库的。

那么 ARCHS 这个变量就代表着这个库所支持的架构。默认是 x86_64arm64 两个。

PLATFORM_NAMESDKROOT 要一起看,根据指定的平台获取对应 SDK 的路径,即用何种 SDK 来构建静态库。

PLATFORM_NAME 可选的值包括:

含义
iphoneos iOS
macosx macOS
iphonesimulator iOS 模拟器

后面几个变量则是定义了一些 CGO 的参数,注意这里是没有 iphonesimulator 对应的参数的,只有 iOS 和 macOS。

libwg-go-a 函数

前几行是使用 export 来定义一些环境变量,CGO_CFLAGSCGO_LDFLAGS 算是比较重要的两个 CGO 参数,用来指定 C 编译器和链接器的选项,后面还会提及。

函数内的 $(1) 指的是该方法的第一个参数。在 libwg-go-a 函数的下一行是一个 foreach 语句,它的作用是遍历 ARCHS,再将每一个 ARCH 作为 libwg-go-a 函数的参数传入,来调用 libwg-go-a 函数。所以这里的 $(1) 就是我们在最上面定义的 ARCHS 里的内容。

我们还可以注意到 $(GOOS_$(PLATFORM_NAME)) 这里,代码借助 PLATFORM_NAME 变量套了一层,来拼接获取 GOOS_macosx 或者 GOOS_iphoneos 的值。

其中比较关键的 go build 命令,就是真正负责构建静态库的命令。最后生成的静态库文件名则是 libwg-go-$(1).a

小结

到这里 Makefile 大致就算是说完了,弄明白了这个文件的大致结构与各部分的作用,后面才好动手对文件进行修改。

通过修改 ARCHSPLATFORM_NAME 的值,如果一切顺利的话,我们可以得到 iOS 和 macOS 平台的静态库,大体上工作也就结束了。

但是往往还会有些额外工作要处理。

编译支持模拟器的静态库

虽然 WireGuard VPN 本身不支持模拟器,但是跳出这个话题,上面有提到,这个脚本现在是不支持 iphonesimulator 的,如果想要支持模拟器该怎么办呢?

其实不支持的原因很简单:因为不存在 GOOS_iphonesimulator 变量(想一想 $(GOOS_$(PLATFORM_NAME)))。

所以我们可以手动定义该变量。通过查阅资料,模拟器对应的 GOOS 也为 ios,所以变量定义如下:

GOARCH_arm64 := arm64
GOARCH_x86_64 := amd64
GOOS_macosx := darwin
GOOS_iphoneos := ios
+GOOS_iphonesimulator := ios

之后再执行 make 命令,就可以得到支持 iOS 模拟器的静态库了。

编译支持 Mac Cataglyst 的静态库

回归业务,本次我的工作是要编译支持 Mac Cataglyst 的静态库,那么该怎么做呢?

x86_64-apple-ios13.0-macabi

通过查找资料,我了解到 -target x86_64-apple-ios13.0-macabi 这个用于描述特定操作系统和处理器架构的标识符:

  • x86_64 表示处理器架构为 64 位的 Intel 或 AMD 处理器;
  • apple 表示运行在苹果操作系统上;
  • ios13.0 表示操作系统版本为 iOS 13.0;
  • macab 表示使用的是 Mac 上的应用程序二进制接口(Mac Application Binary Interface),即我们想要使用的 Mac Cataglyst。

通过使用这个标识符,我们就可以构建出支持 Mac Cataglyst 的静态库。

接着通过这个回复得知,该值需要赋值给 CGO_CFLAGS 变量。回到脚本中我们可以发现,CGO_CFLAGS 上使用 CFLAGS_PREFIX 进行封装。

在这一步我遇到了2个问题:

  1. 不要直接将 -target x86_64-apple-ios13.0-macabi 写在 CFLAGS_PREFIX 的末尾。

省略一些代码后,把两行相关代码放在一起看:

CFLAGS_PREFIX := -isysroot $(SDKROOT) -arch
$(BUILDDIR)/libwg-go-$(1).a: export CGO_CFLAGS := $(CFLAGS_PREFIX) $(ARCH)

可以看到,实际上 export 的内容是 -isysroot $(SDKROOT) -arch x86_64
如果我们直接在 CFLAGS_PREFIX 的末尾添加,那么就会把 -arch x86_64 隔开,继而报错。所以添加到 -arch 前即可。

  1. 找不到 x86_64-apple-ios13.0-macabi

相关的问题在 golang/go 的 github 上是可以搜到的,可惜这个问题是针对 go-mobile 的,但是可以注意到回复中的,-iosversion=14 字样。

于是将其改为 x86_64-apple-ios14.0-macabi,果断解决问题。

编译

Mac Cataglyst 虽然操作的是 iOS App,但是最终还是运行在 macOS 上,所以还是要使用 macosx 的 sdk 进行构建。

修改代码如下:

ARCHS ?= x86_64 arm64
PLATFORM_NAME ?= macosx
SDKROOT ?= $(shell xcrun --sdk $(PLATFORM_NAME) --show-sdk-path)

-CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -isysroot $(SDKROOT) -arch
+CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -isysroot $(SDKROOT) -target x86_64-apple-ios14.0-macabi -arch

但是运行代码后会发现 arm64 架构的包打不出。

Warning
其实到这里,有关 WireGuard 的相关工作已经 “结束” 了。因为虽然最后我打出了包,但是截止到本文发布,我还未能成功将项目运行起来,所以并不知道接下来的操作是否正确。

通过将 GOOS_macosx 对应的值修改为 ios 可以解决这个问题。所以最终修改为:

ARCHS ?= x86_64 arm64
PLATFORM_NAME ?= macosx
SDKROOT ?= $(shell xcrun --sdk $(PLATFORM_NAME) --show-sdk-path)

-CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -isysroot $(SDKROOT) -arch
+CFLAGS_PREFIX := $(if $(DEPLOYMENT_TARGET_CLANG_FLAG_NAME),-$(DEPLOYMENT_TARGET_CLANG_FLAG_NAME)=$($(DEPLOYMENT_TARGET_CLANG_ENV_NAME)),) -isysroot $(SDKROOT) -target x86_64-apple-ios14.0-macabi -arch

GOARCH_arm64 := arm64
GOARCH_x86_64 := amd64
-GOOS_macosx := darwin
+GOOS_macosx := ios
GOOS_iphoneos := ios
GOOS_maccatalyst := ios
GOOS_iphonesimulator := ios

参考