【问题标题】:How to pass an array of strings and get an array of strings in Ruby using Go shared library?如何使用 Go 共享库在 Ruby 中传递字符串数组并获取字符串数组?
【发布时间】:2019-11-14 21:41:43
【问题描述】:

我正在尝试从 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

鲁比:

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项目中有一个方法可以并行处理这样的数组)

【问题讨论】:

  • ['array', 'of', 'strings'].map {|e| GNparser.ParseToJSON(e) }
  • @marmeladze 在我的例子中我想发送一个 ['array', 'of', 'strings'] 到 Go,昂贵的算法将被并行化
  • 你看到这个答案stackoverflow.com/questions/29389334/…了吗?
  • 当将数组传递给 C 中的函数(以及 FFI)时,您会传递一个 指针,通常需要某种方式来指定长度(即另一种范围)。同样,你不能真正返回一个数组。您需要返回一个指针和(通常)长度。通常这是通过将指针传递给函数将填充的函数来完成的。与此相关的是内存管理——谁分配内存以及谁负责释放它。
  • 请注意 CGo 文档说“不支持 Go 数组类型;使用 C 指针”,大概是出于这些原因。这里的答案可能取决于从 Go 导出的实际函数签名。

标签: ruby go ffi


【解决方案1】:

这里的主要问题是流程中有两个不同的运行时,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 中,然后到(或从)Go 中,但我认为如果没有运行时(尤其是垃圾收集器)相互绊倒。可以将数据直接存储在某个共享区域并对其进行操作,而无需使用 Ruby 中的 FFI::Pointers 和 Go 中的 unsafe 进行复制,但这首先会破坏使用这些语言的目的。

【讨论】:

    【解决方案2】:

    我不确定这是不是正确的做法,但在这个解决方案中,要传递的参数是 json 编码为 ruby​​,然后 json 解码为 go。

    该解决方案可能效率低下,但它是安全的。

    我把 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
    

    【讨论】:

    • 我考虑过这种解决方法,但在我的情况下返回的 JSON 字符串太大,在 Ruby 中解组它是不切实际的。所以我仍然需要发送一个字符串数组并返回一个字符串数组。
    • 解组 100 000 个小的 JSON 字符串与解组具有 100 000 个元素的巨大 JSON 有很大的不同。
    • 是的,但我不知道如何在 ruby​​ 中处理指针。我能想到的其他替代方案都需要某种编组,所以它们并不是更好。我会尝试使用 ruby​​ c 类型 (mvolkmann.github.io/NFJS/RubyCExtensions.pdf)、RArray 来传递指针。
    • 你在 ffi 方面检查过这个吗? github.com/ffi/ffi/wiki/Examples#array-of-strings
    • 是的,我还不太明白如何正确连接Ruby数组、FFI、转换为Go slice、从Go slice转换为FF和Ruby数组
    猜你喜欢
    • 2016-05-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-20
    • 1970-01-01
    • 2017-04-28
    • 2016-10-25
    • 1970-01-01
    相关资源
    最近更新 更多