如何使用Go共享库在Ruby中传递字符串数组并获得字符串数组



我正在尝试从Ruby调用Go项目。当我传递一个字符串并返回一个字符串时,它确实工作得很好:

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并返回字符串数组?(围棋项目中有一种方法可以并行处理这样的数组(

这里的主要问题是,在这个过程中有两个不同的运行时,Ruby和Go,它们都不像其他运行时那样窥探它们的内部。因此,为了从Ruby调用Go,您必须首先将数据从Ruby中取出,然后输入Go,然后将结果从Go中取出,再输入Ruby。实际上,您将不得不通过C从Ruby转到go,即使没有实际的C代码。

从Go端开始,假设你想要使用的函数有这样的签名:

func TheFunc(in []string) []string

你可以在你的共享库中导出它,它会给出一个C签名:

extern GoSlice TheFunc(GoSlice p0);

其中GoSlice

typedef struct { void *data; GoInt len; GoInt cap; } GoSlice;

虽然这可能有效,但这提供了对Go数据的直接访问,尤其是返回值,因此并不安全。

一种解决方案是提供一个包装器函数,该函数接受指向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函数。当它返回时,我们需要将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中复制(或复制到Ruby中(,然后从Go中复制(或者复制到Go中(,但我认为如果运行时(尤其是垃圾收集器(不相互绊倒,就不可能以任何其他方式复制。可以将数据直接存储在某个共享区域中,并在不复制的情况下对其进行操作,而无需在Ruby中使用FFI::Pointer和在Go中使用unsafe,但这将首先破坏使用这些语言的目的。

我不确定这是正确的方法,但在这个解决方案中,要传递的参数是用ruby编码的json,然后用go解码的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

最新更新