Golang中如何通过指针调用动态加载的共享库函数?

Golang中如何通过指针调用动态加载的共享库函数? 我尝试从Go中调用一些共享对象函数。我不想为所有函数编写C注释代码来构建CGO接口

我这样编写我的共享对象:

#include <stdio.h>

void greet(char* name) {
    printf("Hello, %s!\n", name);
}

我使用以下命令编译它:gcc -shared -fPIC -o libgreet.so greet.c

我的Go代码用于调用greet函数:

package main

// #cgo LDFLAGS: -ldl
// #include <dlfcn.h>
// #include <stdlib.h>
import "C"
import (
    "unsafe"
    "fmt"
)

func main() {
    so_name := C.CString("./libgreet.so")
    defer C.free(unsafe.Pointer(so_name))
    function_name := C.CString("greet")
    defer C.free(unsafe.Pointer(function_name))

    library := C.dlopen(so_name, C.RTLD_LAZY)
    defer C.dlclose(library)
    function := C.dlsym(library, function_name)

    greet := (*func(*C.char))(unsafe.Pointer(&function))

    fmt.Println("%p %p %p %p\n", greet, function, unsafe.Pointer(function), unsafe.Pointer(&function))
    (*greet)(C.CString("Bob"))
}

当我启动可执行文件时,我遇到了SIGSEGV错误。

我尝试用gdb调试它,打印出来的指针看起来是好的(与gdb中的x greet值相同)。段错误发生在指令0x47dde4 <main.main+548> call r8处,其中$r8包含0x10ec8348e5894855(可能不是一个内存地址)。

你知道我该如何修复这个错误吗?有没有一种解决方案可以在Go中调用共享对象函数,而无需像CGO语法那样编写C注释代码(我没有找到任何文档来做到这一点)?


更多关于Golang中如何通过指针调用动态加载的共享库函数?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

10 回复

如果你成功让示例运行起来,如果可以的话,我想看看代码。

更多关于Golang中如何通过指针调用动态加载的共享库函数?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


谢谢!第一种方案不可行,我必须动态加载库来启动可执行文件,因为该库存在多个路径,并且它不应作为运行可执行文件的先决条件。

我会测试第二个选项。

但我猜这个解决方案并不能解决你的原始问题,即避免为你想要使用的库编写带注释的包装代码。 或者,你使用 dlopen 接口是因为你想要以插件风格使用共享库(在运行时加载和卸载)吗?

感谢您的回复!

您的答案运行良好,但并非我所需……我正在使用一个包含大量函数的共享对象,而 CGO 语法不易阅读和维护,因为它是一个没有语法高亮的注释,并且是从共享对象签名中 复制粘贴 过来的,所以每次更新共享对象时我都必须修改它。

是的,当然。我在此仓库中编写了一个概念验证:https://github.com/mauricelambert/TerminalMessages/blob/main/GoDemonstrationLinux.go

这个库目前还很小,但我希望启动一个更大的项目,在我的库中实现许多功能。

感谢您的回复!

我需要动态加载我的库,因为它就像一个可选的插件。我的项目将提供许多功能,但每个功能都将在一个DLL/SO中实现,用户可以选择他们想要的功能。因此,我的库需要动态加载,以避免成为主可执行文件运行的必要条件。

我不知道什么是 golang 插件,我会去了解更多相关信息。

我不想为我的每个功能都启动另一个进程,所以最后一个解决方案不能在我的项目中使用。

我想到了另外两个可能可行的方案?

  1. 直接使用 LDFLAGS: -lgreet,完全不使用动态加载(而是使用动态链接)。或者这太明显了/我是不是漏掉了什么?
  2. modernc 项目可以将 C 代码转译为 Go 代码。我从未直接尝试过,只试过同一位作者转译的 sqlite 代码。https://gitlab.com/cznic/ccgo

#1: 不同的库路径(在目标系统上)实际上不是什么大问题,这其实相当常见——可以参考 ld.conf、rpath 等。

#2: 使用 dlopen() 及其相关函数时,即使是在 C 语言中,也必须格外小心。你必须确保你的函数指针确实与原型匹配。这看起来可能很简单,但很容易遇到很多陷阱,例如特定架构的调用约定——你的代码需要明确地处理这一点!

#3: 如果确实需要插件形式,你可以尝试将绑定代码放入一个 Go 插件中,该插件直接链接到那个外部库。

#4: 根据你的实际使用场景,将使用外部库的代码放入一个独立的程序/服务中可能是有意义的。

或许 go doc cmd/cgo 中的这段内容会有帮助?

目前不支持调用C函数指针,但是你可以声明持有C函数指针的Go变量,并在Go和C之间来回传递它们。C代码可以调用从Go接收到的函数指针。例如:

    package main

    // typedef int (*intFunc) ();
    //
    // int
    // bridge_int_func(intFunc f)
    // {
    //		return f();
    // }
    //
    // int fortytwo()
    // {
    //	    return 42;
    // }
    import "C"
    import "fmt"

    func main() {
    	f := C.intFunc(C.fortytwo)
    	fmt.Println(int(C.bridge_int_func(f)))
    	// Output: 42
    }

我正在尝试自己实现一个解决方案,但看起来我需要更新(或补充)我对C语言中函数指针的知识。

在Go中通过指针调用动态加载的共享库函数时,主要问题在于函数指针的类型转换。你的代码中(*func(*C.char))(unsafe.Pointer(&function))这种转换方式是不正确的,因为function本身已经是unsafe.Pointer类型,而&function获取的是指针变量的地址。

以下是修复后的代码示例:

package main

// #cgo LDFLAGS: -ldl
// #include <dlfcn.h>
// #include <stdlib.h>
import "C"
import (
    "unsafe"
    "fmt"
)

func main() {
    // 加载共享库
    soName := C.CString("./libgreet.so")
    defer C.free(unsafe.Pointer(soName))
    lib := C.dlopen(soName, C.RTLD_LAZY)
    if lib == nil {
        fmt.Printf("dlopen failed: %s\n", C.GoString(C.dlerror()))
        return
    }
    defer C.dlclose(lib)
    
    // 获取函数符号
    funcName := C.CString("greet")
    defer C.free(unsafe.Pointer(funcName))
    fn := C.dlsym(lib, funcName)
    if fn == nil {
        fmt.Printf("dlsym failed: %s\n", C.GoString(C.dlerror()))
        return
    }
    
    // 正确的函数指针转换方式
    greet := *(*func(*C.char))(unsafe.Pointer(&fn))
    
    // 调用函数
    name := C.CString("Bob")
    defer C.free(unsafe.Pointer(name))
    greet(name)
}

对于更复杂的函数签名,可以使用类型定义来简化:

package main

// #cgo LDFLAGS: -ldl
// #include <dlfcn.h>
// #include <stdlib.h>
import "C"
import (
    "unsafe"
    "fmt"
)

// 定义函数类型
type GreetFunc func(*C.char)

func main() {
    soName := C.CString("./libgreet.so")
    defer C.free(unsafe.Pointer(soName))
    
    lib := C.dlopen(soName, C.RTLD_LAZY)
    if lib == nil {
        fmt.Printf("dlopen failed: %s\n", C.GoString(C.dlerror()))
        return
    }
    defer C.dlclose(lib)
    
    funcName := C.CString("greet")
    defer C.free(unsafe.Pointer(funcName))
    
    fn := C.dlsym(lib, funcName)
    if fn == nil {
        fmt.Printf("dlsym failed: %s\n", C.GoString(C.dlerror()))
        return
    }
    
    // 使用类型定义进行转换
    var greet GreetFunc
    *(*uintptr)(unsafe.Pointer(&greet)) = uintptr(fn)
    
    name := C.CString("Bob")
    defer C.free(unsafe.Pointer(name))
    greet(name)
}

对于有返回值的函数,可以这样处理:

// C函数:int add(int a, int b);
type AddFunc func(C.int, C.int) C.int

func main() {
    // ... 加载库和获取符号的代码
    
    fn := C.dlsym(lib, C.CString("add"))
    var add AddFunc
    *(*uintptr)(unsafe.Pointer(&add)) = uintptr(fn)
    
    result := add(10, 20)
    fmt.Printf("Result: %d\n", result)
}

关键点:

  1. C.dlsym返回的是unsafe.Pointer,直接使用uintptr(fn)获取函数地址
  2. 通过*(*uintptr)(unsafe.Pointer(&greet)) = uintptr(fn)将函数地址赋值给Go函数变量
  3. 确保函数签名与C函数完全匹配
  4. 使用defer C.dlclose(lib)确保资源释放

这种方法避免了使用cgo的注释语法,直接通过动态加载的方式调用共享库函数。

回到顶部