Golang中函数指针的使用:Go与C之间的交互及C结构体传递

Golang中函数指针的使用:Go与C之间的交互及C结构体传递 我正在尝试调用一个用C语言编写的语音合成库中的函数,该库通过其DLL在Golang中使用。我无法构建用于cgo的DLL(如果有人知道如何使用dlltool等方法实现,请告诉我)。无论如何,该DLL包含如下函数:

ESPEAK_API void espeak_SetSynthCallback(t_espeak_callback* SynthCallback);

t_espeak_callback类型定义为:

typedef int (t_espeak_callback)(short*, int, espeak_EVENT*);

而espeak_EVENT结构体定义为:

typedef struct {
	espeak_EVENT_TYPE type;
	unsigned int unique_identifier; // 消息标识符(对于键或字符为0)
	int text_position;    // 从文本起始位置的字符数
	int length;           // 单词长度(以字符计,用于espeakEVENT_WORD事件)
	int audio_position;   // 生成的语音输出数据中的时间(毫秒)
	int sample;           // 样本ID(内部使用)
	void* user_data;      // 调用程序提供的指针
	union {
		int number;        // 用于WORD和SENTENCE事件
		const char *name;  // 用于MARK和PLAY事件,UTF8字符串
		char string[8];    // 用于音素名称(UTF8)。除非名称需要完整的8个字节,否则以零字节终止
	} id;
} espeak_EVENT;

那么,我该如何在Go中编写回调函数,并将其传递给C库呢?我不知道如何在Go中实现联合体(或类似结构)。即使使用cgo,这又能如何帮助我呢?

编辑:这似乎适用于我查看的所有语音合成库——它们都有某种回调函数,这正是我主要困惑的地方。结构体问题似乎与ESpeak相关——不过我猜这不会是唯一的问题。


更多关于Golang中函数指针的使用:Go与C之间的交互及C结构体传递的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于Golang中函数指针的使用:Go与C之间的交互及C结构体传递的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


在Go中通过cgo与C库交互时,处理回调函数和包含联合体的结构体是完全可行的。以下是完整的解决方案:

1. 定义C结构体和回调类型

首先在Go文件中定义对应的C类型:

/*
#include <stdint.h>

// 定义事件类型枚举
typedef enum {
    espeakEVENT_WORD = 1,
    espeakEVENT_SENTENCE = 2,
    espeakEVENT_MARK = 3,
    espeakEVENT_PLAY = 4
} espeak_EVENT_TYPE;

// 定义事件结构体
typedef struct {
    espeak_EVENT_TYPE type;
    unsigned int unique_identifier;
    int text_position;
    int length;
    int audio_position;
    int sample;
    void* user_data;
    union {
        int number;
        const char *name;
        char string[8];
    } id;
} espeak_EVENT;

// 定义回调函数类型
typedef int (*t_espeak_callback)(short*, int, espeak_EVENT*);

// 声明外部函数
extern void espeak_SetSynthCallback(t_espeak_callback SynthCallback);
*/
import "C"

2. 实现Go回调函数

package main

import (
    "fmt"
    "unsafe"
)

//export goSynthCallback
func goSynthCallback(wav *C.short, numsamples C.int, events *C.espeak_EVENT) C.int {
    if events == nil {
        return 0
    }
    
    // 处理音频数据
    if wav != nil && numsamples > 0 {
        // 将C short数组转换为Go slice
        samples := (*[1 << 28]C.short)(unsafe.Pointer(wav))[:numsamples:numsamples]
        fmt.Printf("收到 %d 个音频样本\n", len(samples))
    }
    
    // 处理事件
    event := (*C.espeak_EVENT)(events)
    fmt.Printf("事件类型: %d, 位置: %d\n", event.type, event.text_position)
    
    // 根据事件类型处理联合体
    switch event.type {
    case C.espeakEVENT_WORD, C.espeakEVENT_SENTENCE:
        // 使用number字段
        fmt.Printf("事件ID: %d\n", event.id.number)
    case C.espeakEVENT_MARK, C.espeakEVENT_PLAY:
        // 使用name字段
        if event.id.name != nil {
            name := C.GoString(event.id.name)
            fmt.Printf("标记名称: %s\n", name)
        }
    default:
        // 处理string字段或其他情况
        if event.id.string[0] != 0 {
            str := C.GoString(&event.id.string[0])
            fmt.Printf("音素: %s\n", str)
        }
    }
    
    return 0 // 返回0表示成功
}

3. 注册回调函数

func main() {
    // 将Go回调函数注册到C库
    callback := C.t_espeak_callback(C.goSynthCallback)
    C.espeak_SetSynthCallback(callback)
    
    // 后续的库初始化和其他调用...
    fmt.Println("回调函数已注册")
}

4. 处理联合体的替代方法

如果需要更精确地处理联合体,可以使用字节数组来访问原始内存:

//export goSynthCallbackAdvanced
func goSynthCallbackAdvanced(wav *C.short, numsamples C.int, events *C.espeak_EVENT) C.int {
    if events == nil {
        return 0
    }
    
    event := (*C.espeak_EVENT)(events)
    
    // 通过unsafe直接访问联合体内存
    unionPtr := unsafe.Pointer(&event.id)
    
    switch event.type {
    case C.espeakEVENT_WORD:
        // 作为整数访问
        number := *(*C.int)(unionPtr)
        fmt.Printf("单词事件,编号: %d\n", number)
    case C.espeakEVENT_MARK:
        // 作为字符串指针访问
        namePtr := *(**C.char)(unionPtr)
        if namePtr != nil {
            name := C.GoString(namePtr)
            fmt.Printf("标记事件,名称: %s\n", name)
        }
    case 5: // 假设的音素事件类型
        // 作为固定字节数组访问
        var phoneme [8]C.char
        copy(phoneme[:], (*[8]C.char)(unionPtr)[:])
        if phoneme[0] != 0 {
            str := C.GoString(&phoneme[0])
            fmt.Printf("音素: %s\n", str)
        }
    }
    
    return 0
}

5. 编译和链接

确保在Go文件中包含正确的编译指令:

// #cgo LDFLAGS: -L. -lespeak
// #cgo CFLAGS: -I.
import "C"

这个解决方案展示了如何在Go中正确处理C回调函数和包含联合体的复杂结构体。关键在于使用cgo的类型转换和unsafe.Pointer来安全地访问C内存布局。

回到顶部