Go API 包装与 macOS 客户端通信
本页说明 extra/go-api/wssocks_client_wrapper.go 提供的 Go/C ABI 包装层,以及它如何把 Go 客户端能力暴露给 macOS 原生客户端。包装层通过 cgo 导出创建、启动、等待和停止客户端的函数,并以 C 字符串返回错误信息。
Purpose and Scope
本页聚焦 Go API 包装层与 macOS 客户端之间的边界:
- 导出的 C ABI 函数:
NewClientHandles、StartClientWrapper、WaitClientWrapper和StopClientWrapper。 - C 字符串、布尔值、
uintptr句柄与 Go 配置对象之间的转换。 extra.TaskHandles、client.Options、vpn.UstbVpn以及密码认证配置在包装层中的组装关系。- 句柄的保活策略,以及启动、等待、停止调用的错误返回约定。
macOS SwiftUI 应用、Xcode 工程设置和具体的 Go 客户端后台实现不在本页展开;当前源代码证据只确认 Swift 模块通过 ../extra/go-api/libwssocks_go_api.h 引入导出头文件,未读取到 Swift 调用点和 TaskHandles 的实现。关于 macOS UI 行为与发布构建,请参阅仓库中的对应客户端文档。
Overview
该集成采用“Go 核心 + C ABI + macOS 原生调用方”的边界设计。Go 包装文件使用 import "C",并通过 //export 将 Go 函数导出为可由 C/Swift 调用的符号。macOS 侧无需直接理解 Go 的复合配置类型,而是传入地址、远端地址、认证字段和若干布尔开关;包装层负责将它们转换为 Go 字符串和配置结构。
调用生命周期由一个不透明的整数句柄串联:
NewClientHandles分配extra.TaskHandles,把其指针转换为uintptr,并保存到全局handleInstances映射中。StartClientWrapper将 C 参数转换为extra.Options,再调用该句柄的StartWssocks。WaitClientWrapper调用同一句柄的Wait,把运行期间或等待期间的错误转换为 C 字符串。StopClientWrapper调用NotifyCloseWrapper发出关闭通知。
成功时三个操作函数均返回空 C 字符串;发生错误时返回 C.CString(err.Error())。源文件没有提供对应的释放字符串函数,也没有导出销毁句柄的函数,因此调用方的内存释放和句柄生命周期管理需要结合生成的 C 头文件及未读取的 macOS 调用代码进一步确认。
Architecture
图中的 WssocksGoApi 和头文件路径来自 Swift 模块映射;四个导出函数、handleInstances 以及对 TaskHandles 方法的调用均来自 Go 包装实现。TaskHandles 的内部线程模型和关闭语义未在已读取源码中展开,因此图中只表示包装层确认存在的调用关系。
Source: wssocks_client_wrapper.go
设计边界与关键职责
C ABI 是稳定边界,而不是业务层
StartClientWrapper 接受扁平化参数,而不是直接暴露 Go 结构体。这种形态适合 Swift/C 调用:地址、认证字段使用 *C.char,开关使用 C._Bool,运行实例使用 uintptr。包装器承担类型转换和结构体组装,业务启动仍委托给 TaskHandles.StartWssocks,避免在 ABI 层重复实现客户端逻辑。
句柄映射用于阻止 Go 对象被回收
文件中的全局 handleInstances map[uintptr]*extra.TaskHandles 保存句柄到 Go 指针的引用。NewClientHandles 先创建 TaskHandles,再保存映射,最后返回指针值。注释明确指出该映射用于防止对象被垃圾回收;后续函数只接收 uintptr,再通过 unsafe.Pointer 恢复为 *extra.TaskHandles。
这是一个明确的跨语言生命周期策略,但当前文件没有并发保护,也没有删除映射项的 API。因而不能从现有证据推断它是否支持并发创建实例、重复停止或实例回收;调用方应把这些行为视为需要进一步核对的边界。
Core Flow
Source: wssocks_client_wrapper.go
该顺序反映包装文件中实际可见的调用:启动前必须先取得句柄;启动、等待和停止都通过同一个 handlesPtr 恢复 *extra.TaskHandles。启动和等待会检查错误,停止函数则直接通知关闭并返回空字符串。
配置组装与数据流
StartClientWrapper 将参数分成三组:
| 输入组 | 目标字段 | 转换方式 |
|---|---|---|
localAddr、httpLocalAddr、remoteAddr | client.Options.LocalSocks5Addr、LocalHttpAddr、extra.Options.RemoteAddr | C.GoString |
httpEnable、skipTSLVerify | client.Options.HttpEnabled、SkipTLSVerify | bool(C._Bool) |
vpnEnable、vpnForceLogout、vpnHostEncrypt、vpnHostInput | vpn.UstbVpn 的开关和目标主机 | 布尔转换与 C.GoString |
vpnUsername、vpnPassword | passwd.UstbVpnPasswdAuth.Username、Password | C.GoString |
VPN 认证方法在包装层固定设置为 vpn.VpnAuthMethodPasswd,因此当前 ABI 没有把认证方法作为独立参数暴露。密码结构由用户名和密码两个 C 字符串组成。包装层还把 RemoteAddr 放在外层 extra.Options,而本地 SOCKS5、HTTP 代理和 TLS 校验选项放在嵌套的 client.Options 中。
实现 walkthrough
1. 创建并保活句柄
1// so it would not be destroyed by garbage collection
2var handleInstances map[uintptr]*extra.TaskHandles
3
4//export NewClientHandles
5func NewClientHandles() uintptr {
6 hd := new(extra.TaskHandles)
7 ptr := uintptr(unsafe.Pointer(hd))
8 if handleInstances == nil {
9 handleInstances = make(map[uintptr]*extra.TaskHandles)
10 }
11 handleInstances[ptr] = hd
12 return ptr
13}uintptr 只是 ABI 传递的句柄表示;实际对象仍是 *extra.TaskHandles。映射中的 value 保留 Go 指针,使该对象仍被 Go 运行时引用。源代码没有给出从 handleInstances 删除条目的逻辑,因此不能宣称句柄在停止后自动释放。
Source: wssocks_client_wrapper.go
2. 组装启动选项并启动
1//export StartClientWrapper
2func StartClientWrapper(handlesPtr uintptr, localAddr, remoteAddr, httpLocalAddr *C.char,
3 httpEnable, skipTSLVerify, vpnEnable, vpnForceLogout, vpnHostEncrypt C._Bool,
4 vpnHostInput, vpnUsername, vpnPassword *C.char) *C.char {
5 options := extra.Options{
6 Options: client.Options{
7 LocalSocks5Addr: C.GoString(localAddr),
8 HttpEnabled: bool(httpEnable),
9 LocalHttpAddr: C.GoString(httpLocalAddr),
10 SkipTLSVerify: bool(skipTSLVerify),
11 },
12 UstbVpn: vpn.UstbVpn{
13 Enable: bool(vpnEnable),
14 ForceLogout: bool(vpnForceLogout),
15 HostEncrypt: bool(vpnHostEncrypt),
16 TargetVpn: C.GoString(vpnHostInput),
17 AuthMethod: vpn.VpnAuthMethodPasswd,
18 PasswdAuth: passwd.UstbVpnPasswdAuth{
19 Username: C.GoString(vpnUsername),
20 Password: C.GoString(vpnPassword),
21 },
22 },
23 RemoteAddr: C.GoString(remoteAddr),
24 }
25 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
26 if err := hp.StartWssocks(options); err != nil {
27 return C.CString(err.Error())
28 }
29 return C.CString("")
30}实现先完整构造 extra.Options,再从 handlesPtr 恢复句柄并调用 StartWssocks。这意味着参数转换发生在真正启动之前;启动失败不会 panic,而是把 Go error 文本复制成新的 C 字符串返回给调用方。源代码没有定义空指针检查,因此 nil C 字符串是否安全取决于 cgo 运行时和调用约定,不能从本包装文件推断额外的校验行为。
Source: wssocks_client_wrapper.go
3. 等待与停止
1//export WaitClientWrapper
2func WaitClientWrapper(handlesPtr uintptr) *C.char {
3 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
4 if err := hp.Wait(); err != nil {
5 return C.CString(err.Error())
6 }
7 return C.CString("")
8}
9
10//export StopClientWrapper
11func StopClientWrapper(handlesPtr uintptr) *C.char {
12 var hp = (*extra.TaskHandles)(unsafe.Pointer(handlesPtr))
13 hp.NotifyCloseWrapper()
14 return C.CString("")
15}WaitClientWrapper 与启动使用相同的错误编码约定。StopClientWrapper 不检查 NotifyCloseWrapper 的返回值(源代码中也没有接收返回值),因此其 ABI 结果始终是空字符串。停止函数的语义是“发出关闭通知”,并非本文件可证明的同步等待;若需要确认关闭完成,应查看 TaskHandles 的实现以及调用方是否随后调用 WaitClientWrapper。
Source: wssocks_client_wrapper.go
API Reference
NewClientHandles() uintptr
创建 extra.TaskHandles 实例,并返回其指针值转换后的 uintptr。该返回值必须作为后续三个导出函数的 handlesPtr 使用。函数自身没有错误返回值。
StartClientWrapper(handlesPtr uintptr, localAddr, remoteAddr, httpLocalAddr *C.char, httpEnable, skipTSLVerify, vpnEnable, vpnForceLogout, vpnHostEncrypt C._Bool, vpnHostInput, vpnUsername, vpnPassword *C.char) *C.char
把 C ABI 参数转换为 extra.Options 并调用 TaskHandles.StartWssocks。
handlesPtr:由NewClientHandles返回的句柄。localAddr:写入client.Options.LocalSocks5Addr。remoteAddr:写入extra.Options.RemoteAddr。httpLocalAddr:写入client.Options.LocalHttpAddr。httpEnable:写入client.Options.HttpEnabled。skipTSLVerify:写入client.Options.SkipTLSVerify。参数名中的TSL拼写与源代码保持一致,目标字段是SkipTLSVerify。vpnEnable、vpnForceLogout、vpnHostEncrypt:写入vpn.UstbVpn的对应开关。vpnHostInput:写入vpn.UstbVpn.TargetVpn。vpnUsername、vpnPassword:写入密码认证结构。
返回空 C 字符串表示 StartWssocks 没有返回错误;否则返回 err.Error() 的 C 字符串。源文件未声明显式的异常抛出机制。
WaitClientWrapper(handlesPtr uintptr) *C.char
恢复 *extra.TaskHandles 并调用 Wait。成功返回空 C 字符串,失败返回 err.Error() 的 C 字符串。
StopClientWrapper(handlesPtr uintptr) *C.char
恢复 *extra.TaskHandles 并调用 NotifyCloseWrapper。该包装函数不读取或转换关闭通知的返回值,始终返回空 C 字符串。
配置与调用约定
本包装层没有配置文件或环境变量读取逻辑;全部运行参数由 StartClientWrapper 的调用参数提供。可以确认的默认/固定行为如下:
| 项目 | 类型 | 默认或固定行为 | 说明 |
|---|---|---|---|
| VPN 认证方法 | vpn.VpnAuthMethod | 固定为 vpn.VpnAuthMethodPasswd | ABI 不提供认证方法选择参数 |
| 启动成功返回值 | *C.char | C.CString("") | 空字符串表示包装层未收到错误 |
| 启动失败返回值 | *C.char | C.CString(err.Error()) | 返回 Go 错误文本 |
| 等待失败返回值 | *C.char | C.CString(err.Error()) | 返回 Wait 错误文本 |
| 停止返回值 | *C.char | C.CString("") | NotifyCloseWrapper 的结果未被检查 |
| 句柄存储 | map[uintptr]*extra.TaskHandles | 首次创建句柄时初始化 | 用于保持 Go 对象引用 |
Swift 模块映射声明模块名为 WssocksGoApi,并将头文件指定为 ../extra/go-api/libwssocks_go_api.h。这说明 macOS 工程的调用边界是生成的 C 头文件,而不是直接导入 Go 源文件。
Source: module.modulemap
Failure Modes、边界与并发
Source: wssocks_client_wrapper.go
从源码可确认的失败处理只有两类:StartWssocks 和 Wait 的错误字符串化。以下风险则是接口边界上必须关注、但当前已读源码没有实现细节的部分:
- 无句柄校验:三个操作都直接把
uintptr转成*extra.TaskHandles,没有检查零值、失效值或是否存在于handleInstances。 - 无参数校验:包装层没有验证地址格式、空字符串、用户名或密码,也没有检查 C 指针是否为空。
- 返回字符串的释放责任未定义:所有返回值都通过
C.CString创建。当前文件没有导出对应的释放函数,因此调用方如何释放这些字符串需要查看生成头文件、构建脚本或 Swift 桥接代码;本页不对此做假设。 - 句柄回收未实现:
handleInstances只有写入,没有删除操作。停止并不等同于从映射中释放对象。 - 并发访问未保护:全局 map 的访问没有 mutex 或其他同步机制。源文件未说明多个线程同时创建句柄或并发调用同一句柄是否受支持,不能据此宣称线程安全。
- 停止是否同步未知:
StopClientWrapper只调用通知方法并立即返回;关闭完成时机应由Wait或TaskHandles实现决定。
性能与运维注意事项
包装层本身主要执行字符串转换、结构体组装和一次方法转发,没有看到重试、缓存、超时或连接池逻辑。真正的网络连接、VPN 登录和任务调度发生在 extra.TaskHandles.StartWssocks 及其下游实现中;由于该实现未能在本页的源读取预算内读取,启动耗时、重连策略和资源占用均应到对应实现页核对。
运维上,调用方应保留启动和等待返回的错误文本,并把句柄生命周期与客户端生命周期绑定。尤其不能仅依据 StopClientWrapper 返回空字符串就断言后台任务已经完全退出,因为该函数只报告包装调用本身,没有等待关闭完成。
Extension Points
当前可见的扩展点是 StartClientWrapper 的选项组装:新增可传输配置需要同时考虑 C ABI 参数、Go 结构体字段和生成头文件的兼容性。认证方式目前是硬编码的密码认证;若要支持其他认证方式,需要修改包装层的 AuthMethod 和相应认证字段,而不是仅在 macOS UI 中增加开关。
由于 handleInstances、返回字符串释放和并发访问都没有公开管理 API,扩展句柄生命周期前应先补充明确的释放/销毁协议与同步策略;这些能力在当前源文件中不存在。