前言 #
CGO是Go语言中的一种机制,用于调用C代码或集成C库,它可以让我们通过Go程序直接使用C语言写的库文件(如.dll
文件)。这种功能在需要利用已有的C库时非常有用,尤其是在Windows环境下的DLL文件调用。
本文将围绕如何在CGO中集成和调用DLL文件,介绍相关步骤和注意事项。
本文不会涉及到C动态库的内部实现,重点是使用CGO调用动态库的方法介绍
不同类型C库的分发方式 #
要使用C库,首先需要对不同类型库的相关文件有一个基本认识,c库通常有以下两种类型
- 静态库
- 头文件(*.h)
- 库文件(.lib/.a)
- 动态库
- 头文件(*.h)
- 库文件(.lib/.a)(这里的lib库是导入库类型,包含函数所在的DLL文件和文件中函数位置的信息,并没有具体的实现)
- 动态库(.dll/.so)
区分静态库和动态库 #
因为正常链接过程都是使用.lib或者.a的文件,在没有明确告知库类型的情况下,可能会不知道自己使用的是静态库还是动态库,因此需要一个方法来区分,以MSVC环境的为例
使用lib命令行工具
lib /list *库文件*
如果输出都是.obj目标文件,那么就是静态库,相反如果是.dll动态库,那么这个.lib文件就是动态库的导入库
lib /list avp.lib
CGO调用DLL动态库函数的方法 #
要使用CGO,需要确保Go编译器开启了cgo,可以用
go env
来检查,确保环境变量CGO_ENABLED=1
(现在默认都是开启的,后面编译阶段如果出现问题可以排查一下是否是这个原因导致的);以及需要配置好MingW的环境,这步不再赘述
编译时链接动态库 #
编译时链接动态库是指在编译时通过#cgo LDFLAGS
指定动态库,生成的可执行文件在运行时会自动依赖这些库。Go程序启动时会自动加载这些动态库,无需手动管理动态库的加载和函数的绑定。
和静态库的链接方法一样,在LDFLAGS
中指定库目录以及链接的库即可
/*
#cgo LDFLAGS: -L${SRCDIR}/../lib -lavp
*/
import "C"
后续使用就引入头文件,直接调用库函数即可
/*
#include <stdlib.h>
#include "c_avp.h"
*/
import "C"
func (meTool *MediaTool) cGetVideoInfo(cInputVideoPath *C.char, needDecodeData C.bool) (result string) {
// c_get_video_properties接口 在c_avp.h中声明
result = C.GoString(C.c_get_video_properties(cInputImagePath, needDecodeData))
return
}
这种方案的优缺点:
- 优点
- 简单方便:直接在编译时指定动态库路径和名称,使用时像调用普通C函数一样调用库中的函数。
- 自动管理:Go程序启动时自动加载动态库,无需手动加载和绑定函数。
- 代码清晰:使用CGO调用C库函数时无需额外的处理逻辑。
- 缺点
- 动态库路径依赖:运行时需要保证动态库文件存在于指定路径,否则会导致程序启动失败。
- 灵活性较低:动态库在程序启动时就被加载,无法在运行时动态选择不同的库版本。
如果不想让每个使用动态库的可执行程序都带上这个依赖项,可以尝试下面更灵活的动态加载库方案
运行时动态加载库 #
以Windows平台为例,动态加载动态库需要手动管理整个DLL的生命周期,获取更加灵活的特性需要付出一些代码复杂度上的代价。
例如:调用avp.dll中获取视频属性的接口,提取视频的时长可以用下面的方式实现
/*
#include <stdlib.h>
#ifdef _WIN32
// 使用windows.h内的动态库管理接口
#include <windows.h>
#include <stdio.h>
#include <stdbool.h>
// 调用接口的函数签名,需要与头文件中的声明保持一致
typedef char* (*GetVideoInfoFunction)(const char*, bool);
// 动态库句柄
HMODULE avp_dll;
GetVideoInfoFunction _c_get_video_properties;
void free_avp() {
if (avp_dll) {
// 释放资源
FreeLibrary(avp_dll);
}
}
int init_avp(const char* dll_path) {
// 动态载入动态库
avp_dll = LoadLibrary(dll_path);
if (!avp_dll) {
return 1;
}
// 找到对应函数指针的位置
_c_get_video_properties = (GetVideoInfoFunction)GetProcAddress(avp_dll, "c_get_video_properties");
if (!_c_get_video_properties) {
free_avp();
return 2;
}
return 0;
}
char* c_get_video_properties(const char* filename, bool need_decode_data) {
return _c_get_video_properties(filename, need_decode_data);
}
#else
#include "c_avp.h"
#endif
*/
import "C"
func (ffTool *FFmpegAvp) GetVideoDuration(inputVideoPath string, needDecodeData bool) (duration float64, err error) {
cInputVideoPath := C.CString(inputVideoPath)
cNeedDecodeData := C.bool(needDecodeData)
defer func() {
C.free(unsafe.Pointer(cInputVideoPath))
}()
result := make(map[string]interface{})
strResult := C.c_get_video_properties(cInputVideoPath, cNeedDecodeData)
err = json.Unmarshal([]byte(C.GoString(strResult)), &result)
if err != nil {
err = errors.New("视频属性解析失败")
return
}
C.c_avp_free((*unsafe.Pointer)(unsafe.Pointer(&strResult)))
duration = result["duration"].(float64) / 1000
return
}
上面的方法和在C代码中使用动态加载库的方法一致,是手动使用C库方法来控制动态库,属于是在go中写c代码,当然也可以采用下面go已经封装好的方法来操作,相比上面的方式代码执行效率可能会低一些,但是项目结构性和可读性会强不少,而且都是go代码调试起来会很方便
使用syscall
包来管理和操作DLL动态库
- 加载 DLL:通过
syscall.LoadDLL
加载一个动态库,得到库的句柄。 - 获取函数指针:通过
FindProc
获取指定函数的入口地址(函数指针)。 - 调用函数:通过
Call
方法,调用函数指针来执行 DLL 中的函数。 - 回调函数:通过
syscall.NewCallbackCDecl
获取适配C的函数指针,回调函数需要用//export
注释导出 - 释放库资源(可选):如果库不再需要,调用
Release()
来释放 DLL。
加载动态库
syscall.LoadDLL(dyLibPath)
获取动态库接口函数
// dLib 的类型是 *syscall.DLL,返回的函数指针类型是 *syscall.Proc
dLib.libHandle.FindProc(funcName)
函数执行
// args ...uintptr
proc.Call(args...)
这里的函数返回值:(uintptr, uintptr, error) → (r1, r2, err)
-
r1
:主返回值对于大多数函数调用,
r1
包含的就是函数的返回结果。比如,如果动态库函数返回一个整型值(如状态码或处理结果),这个值会存储在r1
中。对于返回指针的函数,r1
是指向内存的指针值,指向结果数据的首地址。 -
r2
:次返回值r2
是辅助返回值,一般只在一些特定情况下使用。例如,当函数返回的是一个 64 位的值而当前系统架构是 32 位时,可能会将返回值分成两部分,通过r1
和r2
返回。对于大多数常见的 32 位或单一返回值的函数,r2
通常为 0 或无用。 -
err
:错误码err
是syscall.Errno
类型的错误码。可以通过err.Error()
或直接检查err
来获取错误描述。
返回值需要根据具体情况进行类型转换
e.g. 返回值是字符串的情况处理
ret, _, _ := meTool.RunProc(meTool.videoInfoProc, uintptr(unsafe.Pointer(cInputVideoPath)), uintptr(intNeedDecodeData))
cRet := (*C.char)(unsafe.Pointer(ret))
result = C.GoString(cRet)
defer meTool.Free(unsafe.Pointer(&cRet))
其中c返回的字符串指针,需要注意是否需要在go中释放,如果需要在go端释放就需要显式调用c的释放内存接口,避免出现内存泄漏问题。
两种方式的对比总结 #
特性 | 编译时链接动态库 | 运行时动态加载库 |
---|---|---|
灵活性 | 较低,编译时确定库文件,编译的可执行程序直接依赖动态库 | 高,运行时决定加载哪个库,编译的可执行程序无需直接依赖动态库 |
使用复杂度 | 简单,直接调用库函数 | 复杂,需要手动加载库和绑定函数 |
库文件管理 | 程序启动时自动加载 | 手动加载和释放库文件 |
性能 | 启动时加载库,启动较快 | 运行时加载库,稍微影响启动时间 |
典型应用场景 | 常规程序依赖的第三方库 | 插件系统、动态库切换需求 |
这两种方式各有其优缺点,具体选择哪种方式取决于程序的需求。如果需要灵活地管理动态库并在运行时决定加载哪个库,运行时动态加载是更好的选择;而对于普通的库依赖,编译时链接动态库更简单方便。
小结 #
在使用 CGO 和动态库时,这些技巧和方法能够帮助开发者更加灵活地管理跨语言的代码交互,同时提供强大的可扩展性,但是也需要注意跨语言交互过程的内存安全等问题