使用 C++,Go 来为 Ruby 写动态库

March 23, 2017 12:43


瞎折腾

之前在简单的提到使用 C 编写动态库,然后使用 FFI 调用,使用 C++ 编写时则略有不同,但他们在 Ruby 中使用都是一样的,因为最终编译出来的动态库在使用时,都是调用 C 接口。

首先说一下 C++,如果我们还是按照 C 的写法:

// hello.cpp
#include <cstdio>

void hello(char *name) {
  printf("Hello %s", name);
}

然后编译动态库

$ g++ -Wall -c hello.cpp
$ g++ -shared -Wall -o libhello.so hello.o

然后 Ruby 代码

require 'FFI'

module M
   extend FFI::Library

  ffi_lib File.expand_path('../libhello.so', __FILE__)

  attach_function :hello, [:string], :void
end

M.hello('World')

运行时定会报出 Functin 'hello' not found,因为这个编译出来的是 C++ 样式的动态库,FFI 使用 C 的方式去调用时,肯定会找不到啦。我们需要指定导出 C 样式的接口:

#include <cstdio>

extern "C" {
  void hello(char *name) {
    printf("Hello %s", name);
  }
}

重新编译再运行 Ruby 代码,应该就输出了 Hello World 了。

接下来说说使用 Go 来折腾,其实也是非常简单的,如下 Go 代码:

package main

import "C"
import "fmt"

//export hello
func hello(name *C.char) {
  fmt.Printf("hello %s", C.GoString(name));
}

func main() {}

需要说明的是 //export hello 这个必须写,Go 定义要暴露的接口必须在接口前加上 //export function_name,由于是 C 接口,所以不能直接使用 go 的 string,使用 name *C.char,如果直接写 name string 的话,运行时输出 name 为空。其它类型的像 intfloat 之类的通用类型还是可以直接使用的。

开始编译上面代码:

go build -buildmode=c-shared -o libhello_go.so hello.go 

输出了 libhello_go.so 动态库,我们只要把 Ruby 代码中的 ffi_lib 路径改为指向 libhello_go.so 就好了,运行后应该也可以看到输出了 Hello World

略麻烦

下面来说说调用时使用的一些略复杂的数据结构

先说数组吧,假设我们要传入一组 int 类型的值,C 接口为 int sum(int *numbers, int len),在 Ruby 中使用如下方式定义接口:

module M
  # ...

  attach_function :sum, [:pointer, :int], :int
end

而调用时需要这么做:

a = [1, 2, 3, 4]

numbers_pointer = FFI::MemoryPointer.new(:int, a.length)
numbers_pointer.write_array_of_int a
puts M.sum(numbers_pointer, a.length)

通过创建一个 int 的数组的指针,然后向里面填好数据,再使用这个指针去调用 C 函数。

而创建二维数组时,也是类似的方式,比如我们有个 C 接口 void test_words(char **words, int len),在 Ruby 中定义接口时还是使用 pointer

module M
  # ...

  attach_function :test_words, [:pointer, :int], :void
end

调用时创建一个字符串的二维数组:

words = ["hello", "world"]
words_pointer = FFI::MemoryPointer.new(:pointer, words.length)
words_pointer.write_array_of_pointer(
  words.map {|word| FFI::MemoryPointer.from_string(word) }
)
M.test_words(words_pointer)

上面 FFI::MemoryPointer.from_string(word) 会创建一个 char * 指针,并把 word 这个字符串中的数据放进去,其实就是简化的 new(:char, len),再 write_array_of_char(word_chars_array)

而对于 Struct 可以参考文档 https://github.com/ffi/ffi/wiki/Structs,懒得写了 :)

尾声

在实际使用中,我们还会去关心更多的东西:会不会有内存泄露?FFI 是怎么管理内存的?Ruby 中 GIL 怎么办?相信万能的网络可以帮助到你的。

最后总要说点那个啥的:没有万能的语言,但万能的你可以取长补短,不要将自己圈住。

Comments: