FFI 调用 Go/Rust 动态链接库
FFI 调用 Go/Rust 动态链接库
前言
下面有两个例子
# fib.rb
def fib(n)
return n if n <= 1
fib(n - 1) + fib(n - 2)
end
puts fib(40)
运行:
$ time ruby fib.rb
102334155
ruby fib.rb 9.06s user 0.06s system 99% cpu 9.204 total
CPU
占比 99%- 总耗时: 9s 多
package main
import (
"fmt"
)
func fib(n uint) uint {
if n <= 1 {
return n
} else {
return fib(n - 1) + fib(n - 2)
}
}
func main() {
fmt.Println(fib(40))
}
$ time go run fib.go
102334155
go run fib.go 0.77s user 0.37s system 94% cpu 1.202 total
CPU
占用 90% 左右- 总耗时 1.1s 左右
先编译后执行
$ go build fib.go
$ time ./fib
102334155
./fib 0.57s user 0.00s system 99% cpu 0.577 total
结论: 效率更高了
对比一下性能,可想而知,go 语言的性能是非常高的,将近20倍 由上可知,其实 ruby 在某一些方面性能确实不如别的语言好
我们就会有这样的场景,在 A 语言写的函数可以在 B 语言里面调用 这时一般有两种解决方案:
- 一是将函数做成一个服务,通过进程间通信(IPC)或网络协议通信(RPC, RESTful等)
- 二就是直接通过 FFI 调用。
前者需要至少两个独立的进程才能实现,而后者直接将其它语言的接口内嵌到本语言中,所以调用效率比前者高。
FFI 简介
全名: 语言交互接口 (Foreign function interface
)
一个可以在某种计算机语言中调用其它语言的接口
FFI 简单应用
使用 Go 编写和生成共享库
使用 Go 编写共享库和写普通的 Go 程序差别不大,主要是在代码里 import 一个名为 “C” 的伪包,然后在执行 go build 编译的时候加上 -buildmode=c-shared 参数。
我们先来写一下作为共享库的 fibonacci 函数
package main
import (
"C"
)
//export fib
func fib(n uint) uint {
if n <= 1 {
return n
} else {
return fib(n-1) + fib(n-2)
}
}
func main() {}
注:可调用的函数是通过添加注释 //export fib 来实现的,因此这行注释是必须的。
编译生成共享库
- Mac OS
go build -buildmode=c-shared -ldflags -s -o fib.dylib fib.go
- Linux:
go build -buildmode=c-shared -o fib.so fib.go
编译好了会生成两个文件
- fib.h 文件
- fib.dylib 二进制文件
引用共享库
- 安装
gem install 'ffi'
- ruby 代码如下:
# fib.rb
require "ffi"
module Fib
extend FFI::Library
# 下面是我在 macOS 上运行的写法,如果在 Linux 上调用的文件为 "fib.so"
ffi_lib "fib.dylib"
attach_function :fib, [:int], :int
end
puts Fib.fib(40)
$ time ruby fib.rb
102334155
ruby ffi/fib/fib.rb 0.63s user 0.04s system 95% cpu 0.709 total
性能提升了很多
FFI 实际应用
链接数据库查询的小 demo
go
语言如下:
package main
import (
"C"
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
)
type Food struct {
id int
name string
}
// var db *gorm.DB
//export test
func test() {
db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/food_dev")
//设置数据库最大连接数
db.SetConnMaxLifetime(10)
//设置上数据库最大闲置连接数
db.SetMaxIdleConns(3)
if err != nil {
return
}
defer db.Close()
rows, e := db.Query("select id, name from foods where id in (1,2,3)")
// fmt.Println(e)
if e != nil {
fmt.Println(e)
}
for rows.Next() {
var food Food
e := rows.Scan(&food.id, &food.name)
if e == nil {
fmt.Println(food)
}
}
}
func main() {
test()
}
编译生成共享函数库
go build -buildmode=c-shared -ldflags -s -o test.dylib test.go
ruby 代码如下:
require "ffi"
module Test
extend FFI::Library
# macOS: fib.dylib,Linux: "fib.so"
ffi_lib "test.dylib"
attach_function :test, [], :void
end
Test.test
运行一下:
time ruby test.rb
{1 小麦}
{2 该粉}
{3 小麦粉(标准粉)}
ruby test.rb 0.10s user 0.07s system 44% cpu 0.377 total
所以我们就可以在自己的项目里愉快的写 golang
了
但是这对 FFI
来说仅仅是不够的。
扩展
我们也可以对任何 C
系语言进行扩展
比如我们写烦了 golang
, 也可以尝试一下 Rust
举个例子:
Rust 实现 fibonacci
代码如下:
// 正如上一章讲述的,为了能让rust的函数通过ffi被调用,需要加上extern "C"对函数进行修饰。
// 但由于rust支持重载,所以函数名会被编译器进行混淆,就像c++一样。因此当你的函数被编译完毕后,函数名会带上一串表明函数签名的字符串。
// 比如:fn test() {}会变成_ZN4test20hf06ae59e934e5641haaE. 这样的函数名为ffi调用带来了困难,因此,rust提供了#[no_mangle]属性为函数修饰。 对于带有#[no_mangle]属性的函数,rust编译器不会为它进行函数名混淆
#[no_mangle]
extern "C" fn fib(i: i32) -> i32 {
if i <= 0 {
panic!("索引要大于0");
}
return if i <= 2 { 1 } else { fib(i - 1) + fib(i - 2) };
}
fn main() {
println!("fib is {}", fib(40));
}
编译生成共享行数:
rustc --crate-type dylib fib.rs
ruby 代码如下:
require "ffi"
module FibRust
extend FFI::Library
# macOS: fib.dylib,Linux: "fib.so"
ffi_lib "libfib.dylib"
attach_function :fib, [:int], :int
end
puts FibRust.fib(40)
$ time ruby fib.rb
102334155
ruby fib.rb 0.64s user 0.04s system 97% cpu 0.696 total