如何使用 Go 共享库传递字符串数组并获取 Ruby 中的字符串数组?
How to pass an array of strings and get an array of strings in Ruby using Go shared library?
我正在尝试从 Ruby 调用一个 Go 项目。当我传递一个字符串并取回一个字符串时它确实工作得很好:
开始:
package main
import "C"
import (
"fmt"
"gitlab.com/gogna/gnparser"
)
//export ParseToJSON
func ParseToJSON(name *C.char) *C.char {
goname := C.GoString(name)
gnp := gnparser.NewGNparser()
parsed, err := gnp.ParseAndFormat(goname)
if err != nil {
fmt.Println(err)
return C.CString("")
}
return C.CString(parsed)
}
func main() {}
我用
编译它
go build -buildmode=c-shared -o libgnparser.so main.go
Ruby:
require 'ffi'
# test
module GNparser
extend FFI::Library
if Gem.platforms[1].os == 'darwin'
ffi_lib './clib/mac/libgnparser.so'
else
ffi_lib './clib/linux/libgnparser.so'
end
attach_function :ParseToJSON, [:string], :string
end
puts GNparser.ParseToJSON("Homo sapiens L.")
如何将 Ruby 字符串数组传递给 Go 并为此类示例取回字符串数组? (Go项目中有一种方法可以并行处理这样的数组)
我不确定这是正确的方法,但在这个解决方案中,要传递的参数是 json 在 ruby 中编码,然后 json 在 json 中解码去吧。
该解决方案可能效率低下,但它是安全的。
我将 ruby 程序稍微更改为
require 'ffi'
require 'json'
# test
module GNparser
extend FFI::Library
ffi_lib './libgnparser.so'
attach_function :ParseToJSON, [:string], :string
end
puts GNparser.ParseToJSON(["Homo","sapiens","L."].to_json)
Go 程序
package main
import "C"
import (
"encoding/json"
"fmt"
)
// "gitlab.com/gogna/gnparser"
// ParseToJSON exports ParseToJSON
//export ParseToJSON
func ParseToJSON(name *C.char) *C.char {
goname := C.GoString(name)
dec := []string{}
json.Unmarshal([]byte(goname), &dec)
// gnp := gnparser.NewGNparser()
// parsed, err := gnp.ParseAndFormat(goname)
// if err != nil {
// fmt.Println(err)
// return C.CString("")
// }
goname = fmt.Sprint(len(dec))
return C.CString(goname)
}
func main() {}
注意添加// export comment
,否则符号不会导出,ruby程序无法访问。
[mh-cbon@Host-001 rubycgo] $ go build -buildmode=c-shared -o libgnparser.so main.go
[mh-cbon@Host-001 rubycgo] $ objdump -TC libgnparser.so | grep Parse
000000000012fb40 g DF .text 0000000000000042 Base ParseToJSON
000000000012f780 g DF .text 0000000000000051 Base _cgoexp_fcc5458c4ebb_ParseToJSON
[mh-cbon@Host-001 rubycgo] $ ruby main.rb
3
[mh-cbon@Host-001 rubycgo] $ ll
total 3008
-rw-rw-r-- 1 mh-cbon mh-cbon 1639 17 nov. 13:12 libgnparser.h
-rw-rw-r-- 1 mh-cbon mh-cbon 3063856 17 nov. 13:12 libgnparser.so
-rw-rw-r-- 1 mh-cbon mh-cbon 504 17 nov. 13:12 main.go
-rw-rw-r-- 1 mh-cbon mh-cbon 219 17 nov. 13:03 main.rb
这里的主要问题是进程中有两个不同的运行时,Ruby 和 Go,它们都不像其他人在内部探索。因此,为了从 Ruby 调用 Go,您必须首先从 Ruby 中获取数据,然后进入 Go,然后从 Go 中获取结果,然后进入 Ruby ].实际上,即使没有实际的 C 代码,您也必须通过 C 从 Ruby 转到 Go。
从 Go 端开始,假设您要使用的函数具有如下签名:
func TheFunc(in []string) []string
您可以将它导出到您的共享库中,这将给出一个 C 签名:
extern GoSlice TheFunc(GoSlice p0);
其中 GoSlice
是
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
虽然这可能有效,但它可以直接访问 Go 数据,尤其是 return 值,因此并不安全。
一个解决方案是提供一个包装函数,它接受一个指向 C 字符串数组(即 **char
)的指针和数组的长度。然后该函数可以解压缩这些数据并将其转换为 Go 数组(或切片)并将其传递给执行工作的实际函数。这个包装函数还需要一种方法来获得结果。一种方法是传入一个指向字符串数组指针的指针(即 ***char
),函数可以分配数组,用结果字符串填充它,并将其地址写入该指针指向的位置到.
这个解决方案的缺点是在 Go 中分配内存并依赖调用代码来释放它。
这有点乱,但它看起来像这样:
// #include <stdlib.h>
import "C"
import "unsafe"
//export CTheFunc
func CTheFunc(in **C.char, len C.int, out ***C.char) {
inSlice := make([]string, int(len))
// We need to do some pointer arithmetic.
start := unsafe.Pointer(in)
pointerSize := unsafe.Sizeof(in)
for i := 0; i< int(len); i++ {
// Copy each input string into a Go string and add it to the slice.
pointer := (**C.char)(unsafe.Pointer(uintptr(start) + uintptr(i)*pointerSize))
inSlice[i] = C.GoString(*pointer)
}
// Call the real function.
resultSlice := TheFunc(inSlice)
// Allocate an array for the string pointers.
outArray := (C.malloc(C.ulong(len) * C.ulong(pointerSize)))
// Make the output variable point to this array.
*out = (**C.char)(outArray)
// Note this is assuming the input and output arrays are the same size.
for i := 0; i< int(len); i++ {
// Find where to store the address of the next string.
pointer := (**C.char)(unsafe.Pointer(uintptr(outArray) + uintptr(i)*pointerSize))
// Copy each output string to a C string, and add it to the array.
// C.CString uses malloc to allocate memory.
*pointer = C.CString(resultSlice[i])
}
}
这提供了一个具有以下签名的 C 函数,我们可以使用 FFI 从 Ruby 访问它。
extern void CDouble(char** p0, int p1, char*** p2);
事物的 Ruby 方面非常相似,但相反。我们需要将数据复制到 C 数组中,并分配一些内存,我们可以将其地址传递给接收结果,然后将数组、它的长度和输出指针传递给 Go 函数。当它 returns 时,我们需要将 C 数据复制回 Ruby 字符串和数组并释放内存。它可能看起来像这样:
require 'ffi'
# We need this to be able to use free to tidy up.
class CLib
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :free, [:pointer], :void
end
class GoCaller
extend FFI::Library
ffi_lib "myamazinggolibrary.so"
POINTER_SIZE = FFI.type_size(:pointer)
attach_function :CTheFunc, [:pointer, :int, :pointer], :void
# Wrapper method that prepares the data and calls the Go function.
def self.the_func(ary)
# Allocate a buffer to hold the input pointers.
in_ptr = FFI::MemoryPointer.new(:pointer, ary.length)
# Copy the input strings to C strings, and write the pointers to in_ptr.
in_ptr.write_array_of_pointer(ary.map {|s| FFI::MemoryPointer.from_string(s)})
# Allocate some memory to receive the address of the output array.
out_var = FFI::MemoryPointer.new(:pointer)
# Call the actual function.
CTheFunc(in_ptr, ary.length, out_var)
# Follow the pointer in out_var, and convert to an array of Ruby strings.
# This is the return value.
out_var.read_pointer.get_array_of_string(0, ary.length)
ensure
# Free the memory allocated in the Go code. We don’t need to free
# the memory in the MemoryPointers, it is done automatically.
out_var.read_pointer.get_array_of_pointer(0, ary.length).each {|p| CLib.free(p)}
CLib.free(out_var.read_pointer)
end
end
这确实涉及在每个方向上复制数据两次,从 Ruby 出(或入)然后入(或出)Go,但我认为在其他任何地方都不可能这样做没有运行时(尤其是垃圾收集器)相互绊倒的方式。可以将数据直接存储在某个共享区域并对其进行操作,而无需使用 Go 中的 Ruby 中的 FFI::Pointer
和 Go 中的 unsafe
进行复制,但这会破坏使用这些的目的语言排在首位。
我正在尝试从 Ruby 调用一个 Go 项目。当我传递一个字符串并取回一个字符串时它确实工作得很好:
开始:
package main
import "C"
import (
"fmt"
"gitlab.com/gogna/gnparser"
)
//export ParseToJSON
func ParseToJSON(name *C.char) *C.char {
goname := C.GoString(name)
gnp := gnparser.NewGNparser()
parsed, err := gnp.ParseAndFormat(goname)
if err != nil {
fmt.Println(err)
return C.CString("")
}
return C.CString(parsed)
}
func main() {}
我用
编译它go build -buildmode=c-shared -o libgnparser.so main.go
Ruby:
require 'ffi'
# test
module GNparser
extend FFI::Library
if Gem.platforms[1].os == 'darwin'
ffi_lib './clib/mac/libgnparser.so'
else
ffi_lib './clib/linux/libgnparser.so'
end
attach_function :ParseToJSON, [:string], :string
end
puts GNparser.ParseToJSON("Homo sapiens L.")
如何将 Ruby 字符串数组传递给 Go 并为此类示例取回字符串数组? (Go项目中有一种方法可以并行处理这样的数组)
我不确定这是正确的方法,但在这个解决方案中,要传递的参数是 json 在 ruby 中编码,然后 json 在 json 中解码去吧。
该解决方案可能效率低下,但它是安全的。
我将 ruby 程序稍微更改为
require 'ffi'
require 'json'
# test
module GNparser
extend FFI::Library
ffi_lib './libgnparser.so'
attach_function :ParseToJSON, [:string], :string
end
puts GNparser.ParseToJSON(["Homo","sapiens","L."].to_json)
Go 程序
package main
import "C"
import (
"encoding/json"
"fmt"
)
// "gitlab.com/gogna/gnparser"
// ParseToJSON exports ParseToJSON
//export ParseToJSON
func ParseToJSON(name *C.char) *C.char {
goname := C.GoString(name)
dec := []string{}
json.Unmarshal([]byte(goname), &dec)
// gnp := gnparser.NewGNparser()
// parsed, err := gnp.ParseAndFormat(goname)
// if err != nil {
// fmt.Println(err)
// return C.CString("")
// }
goname = fmt.Sprint(len(dec))
return C.CString(goname)
}
func main() {}
注意添加// export comment
,否则符号不会导出,ruby程序无法访问。
[mh-cbon@Host-001 rubycgo] $ go build -buildmode=c-shared -o libgnparser.so main.go
[mh-cbon@Host-001 rubycgo] $ objdump -TC libgnparser.so | grep Parse
000000000012fb40 g DF .text 0000000000000042 Base ParseToJSON
000000000012f780 g DF .text 0000000000000051 Base _cgoexp_fcc5458c4ebb_ParseToJSON
[mh-cbon@Host-001 rubycgo] $ ruby main.rb
3
[mh-cbon@Host-001 rubycgo] $ ll
total 3008
-rw-rw-r-- 1 mh-cbon mh-cbon 1639 17 nov. 13:12 libgnparser.h
-rw-rw-r-- 1 mh-cbon mh-cbon 3063856 17 nov. 13:12 libgnparser.so
-rw-rw-r-- 1 mh-cbon mh-cbon 504 17 nov. 13:12 main.go
-rw-rw-r-- 1 mh-cbon mh-cbon 219 17 nov. 13:03 main.rb
这里的主要问题是进程中有两个不同的运行时,Ruby 和 Go,它们都不像其他人在内部探索。因此,为了从 Ruby 调用 Go,您必须首先从 Ruby 中获取数据,然后进入 Go,然后从 Go 中获取结果,然后进入 Ruby ].实际上,即使没有实际的 C 代码,您也必须通过 C 从 Ruby 转到 Go。
从 Go 端开始,假设您要使用的函数具有如下签名:
func TheFunc(in []string) []string
您可以将它导出到您的共享库中,这将给出一个 C 签名:
extern GoSlice TheFunc(GoSlice p0);
其中 GoSlice
是
typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;
虽然这可能有效,但它可以直接访问 Go 数据,尤其是 return 值,因此并不安全。
一个解决方案是提供一个包装函数,它接受一个指向 C 字符串数组(即 **char
)的指针和数组的长度。然后该函数可以解压缩这些数据并将其转换为 Go 数组(或切片)并将其传递给执行工作的实际函数。这个包装函数还需要一种方法来获得结果。一种方法是传入一个指向字符串数组指针的指针(即 ***char
),函数可以分配数组,用结果字符串填充它,并将其地址写入该指针指向的位置到.
这个解决方案的缺点是在 Go 中分配内存并依赖调用代码来释放它。
这有点乱,但它看起来像这样:
// #include <stdlib.h>
import "C"
import "unsafe"
//export CTheFunc
func CTheFunc(in **C.char, len C.int, out ***C.char) {
inSlice := make([]string, int(len))
// We need to do some pointer arithmetic.
start := unsafe.Pointer(in)
pointerSize := unsafe.Sizeof(in)
for i := 0; i< int(len); i++ {
// Copy each input string into a Go string and add it to the slice.
pointer := (**C.char)(unsafe.Pointer(uintptr(start) + uintptr(i)*pointerSize))
inSlice[i] = C.GoString(*pointer)
}
// Call the real function.
resultSlice := TheFunc(inSlice)
// Allocate an array for the string pointers.
outArray := (C.malloc(C.ulong(len) * C.ulong(pointerSize)))
// Make the output variable point to this array.
*out = (**C.char)(outArray)
// Note this is assuming the input and output arrays are the same size.
for i := 0; i< int(len); i++ {
// Find where to store the address of the next string.
pointer := (**C.char)(unsafe.Pointer(uintptr(outArray) + uintptr(i)*pointerSize))
// Copy each output string to a C string, and add it to the array.
// C.CString uses malloc to allocate memory.
*pointer = C.CString(resultSlice[i])
}
}
这提供了一个具有以下签名的 C 函数,我们可以使用 FFI 从 Ruby 访问它。
extern void CDouble(char** p0, int p1, char*** p2);
事物的 Ruby 方面非常相似,但相反。我们需要将数据复制到 C 数组中,并分配一些内存,我们可以将其地址传递给接收结果,然后将数组、它的长度和输出指针传递给 Go 函数。当它 returns 时,我们需要将 C 数据复制回 Ruby 字符串和数组并释放内存。它可能看起来像这样:
require 'ffi'
# We need this to be able to use free to tidy up.
class CLib
extend FFI::Library
ffi_lib FFI::Library::LIBC
attach_function :free, [:pointer], :void
end
class GoCaller
extend FFI::Library
ffi_lib "myamazinggolibrary.so"
POINTER_SIZE = FFI.type_size(:pointer)
attach_function :CTheFunc, [:pointer, :int, :pointer], :void
# Wrapper method that prepares the data and calls the Go function.
def self.the_func(ary)
# Allocate a buffer to hold the input pointers.
in_ptr = FFI::MemoryPointer.new(:pointer, ary.length)
# Copy the input strings to C strings, and write the pointers to in_ptr.
in_ptr.write_array_of_pointer(ary.map {|s| FFI::MemoryPointer.from_string(s)})
# Allocate some memory to receive the address of the output array.
out_var = FFI::MemoryPointer.new(:pointer)
# Call the actual function.
CTheFunc(in_ptr, ary.length, out_var)
# Follow the pointer in out_var, and convert to an array of Ruby strings.
# This is the return value.
out_var.read_pointer.get_array_of_string(0, ary.length)
ensure
# Free the memory allocated in the Go code. We don’t need to free
# the memory in the MemoryPointers, it is done automatically.
out_var.read_pointer.get_array_of_pointer(0, ary.length).each {|p| CLib.free(p)}
CLib.free(out_var.read_pointer)
end
end
这确实涉及在每个方向上复制数据两次,从 Ruby 出(或入)然后入(或出)Go,但我认为在其他任何地方都不可能这样做没有运行时(尤其是垃圾收集器)相互绊倒的方式。可以将数据直接存储在某个共享区域并对其进行操作,而无需使用 Go 中的 Ruby 中的 FFI::Pointer
和 Go 中的 unsafe
进行复制,但这会破坏使用这些的目的语言排在首位。