带有共享库的 C# 字符串

C# string with shared library

我有一个用 Go 构建的共享库和一个 C# 程序,我想调用共享库中的函数。但是打印一个空行。

这是 C# 代码:

using System.Runtime.InteropServices;

class Program
{
    [DllImport("./main.so", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    static extern void Println(string s);

    static void Main(string[] args)
    {
        Println("hello world");
    }
}

这是 Go 代码:

package main

import "C"
import "fmt"

//export Println
func Println(s string){
    fmt.Println(s)
}

func main(){}

如何将 C# 字符串作为 Go 字符串传递?

问题

问题是,虽然 -buildmode=c-shared(通常 -buildmode=c-whatever)确实使 Go 符号可从 C 调用,但在字符串方面却有一个转折点。

在 C 语言中,确实没有字符串这样的东西,但存在一个约定,即字符串是指向其第一个字节的指针,并且字符串的长度是 隐含的由该字符串中代码为 0 (ASCII NUL) 的字节定义。

在 Go 中,字符串是 struct 两个字段:指向包含字符串内容的内存块的指针和该块中的字节数。

正如你所见,当Go工具集编译一个标记为"exported to C"的Go函数时,它基本上有两个选择:

  • 使函数可调用"as is"—要求调用者传递描述字符串的 Go 风格 struct 值。
  • 人为地 "wrap" 将导出的函数放入一个代码块中,该代码块将采用 "C-style" NUL 终止的字符串,计算其中的字节数,制作 Go 风格 struct值并最终调用原始函数。

对于非 Go 程序员来说,第二种方法可以说更容易处理,但有两个反对使用它的论点:

  • 这种方法会为每次调用带来隐性成本:扫描字符串的字节——即使调用者知道它。
  • 如果需要的话,可以在 Go 代码中创建这样的包装器,正如@Selvin 在他们对问题的评论中所建议的那样。

因此,重申一下,当 Go 工具集以 c-whatever 模式编译标记为 "exported to C" 的 Go 函数时,它遵循 Go 约定,其工作结果是:

  • 对于每个导出函数的 Go 类型 string 的每个参数,已编译的库期望接收一个 struct 类型的值,该值由指针和大小组成(如上所述)。
  • 已生成支持的 C 头文件,其中包含 struct 类型的定义——它将被称为 GoString,以及所有导出函数的声明,使用 GoString 为 Go 端 string 的参数键入。

用更简单的话来说,如果您要创建一个包含

的文件 foo.go
package main

import "C"

import "fmt"

//export PrintIt
func PrintIt(string s) {
    fmt.Println(s)
}

然后使用 go build -buildmode=c-shared -o foo.so foo.go 编译它, Go 工具集将创建 foo.sofoo.h,其中包含诸如:

typedef struct { const char *p; ptrdiff_t n; } GoString;

extern void PrintIt(GoString p0);

如您所见,要调用 PrintIt,您应该将 GoString 的实例传递给它,而不是 const char *.

类型的值

一个解决方案

一个合适的解决方案是像

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential, Pack=0)]
struct GoString {
    byte *p;
    int  n;
}

然后

[DllImport("./main.so", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
static extern void Println(GoString gs);

请注意,我不太确定普通的 int 是否可以在每种情况下用于 ptrdiff_t — 请自己研究一下应该正确使用什么在 .NET 互操作中。