带有共享库的 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.so
和 foo.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 互操作中。
我有一个用 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.so
和 foo.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 互操作中。