Go调用Rust方法及外部函数接口前置

2022-06-13 20:02:04
目录
前言FFI 和 Binding准备 Rust 示例程序用 Cargo 创建项目准备 Rust 代码编译 Rust 代码准备 Go 代码编写 main.go编译代码总结

前言

近期>

我最近看到很多小伙伴说的话:

    Rust 还值得学吗?社区是不是不稳定呀Rust 和 Go 哪个好?Rust 还值得学吗?

    这些问题如果有人来问我,那我的回答是:

    小孩子才做选择,我都要!

    当然,关于 Rust 和 Go 的问题也不算新,比如之前的一条推文:

    我在本篇中就来介绍下如何用 Go 调用 Rust。

    当然,这篇中我基本上不会去比较 Go 和 Rust 的功能,或者这种方式的性能之类的,Just for Fun

    FFI>

    FFI (Foreign Function Interface) 翻译过来叫做外部函数接口(为了比较简单,下文中都将使用 FFI 指代)。最早来自于 Common Lisp 的规范,这是在 wiki 上写的,我并没有去考证。 不过我所使用过的绝大多数语言中都有 FFI 的概念/术语存在,比如:Python、Ruby, Haskell、Go、Rust、LuaJIT 等。

    FFI 的作用简单来说就是允许一种语言去调用另一种语言,有时候我们也会用 Binding 来表示类似的能力。

    在不同的语言中会有不同的实现,比如在 Go 中的 cgo , Python 中的 ctypes , Haskell 中的 CAPI (之前还有一个 ccall)等。 我个人感觉 Haskell 中用 FFI 相比其他语言要更简单&方便的多,不过这不是本篇的重点就不展开了。

    在本文中,对于 Go 和 Rust 而言,它们的 FFI 需要与 C 语言对象进行通信,而这部分其实是由操作系统根据 API 中的调用约定来完成的。

    我们来进入正题。

    准备>

    Rust 的安装和 Cargo 工具的基本使用,这里就不介绍了。大家可以去 Rust 的官网进行了解。

    用>

    我们先准备一个目录用来放本次示例的代码。(我创建的目录叫做 go-rust )

    然后使用 Rust 的 Cargo 工具创建一个名叫 rustdemo 的项目,这里由于我增加了 --lib 的选项,使用其内置的 library 模板。

    ➜  go-rust git:(master) ✗ mkdir lib && cd lib
    ➜  go-rust git:(master) ✗ cargo new --lib rustdemo
         Created library `rustdemo` package
    ➜  go-rust git:(master) ✗ tree rustdemo 
    rustdemo
    ├── Cargo.toml
    └── src
        └── lib.rs
    1 directory, 2 files
    

    准备>
    extern crate libc;
    use std::ffi::{CStr, CString};
    #[no_mangle] 
    pub extern "C" fn rustdemo(name: *const libc::c_char) -> *const libc::c_char {
        let cstr_name = unsafe { CStr::from_ptr(name) };
        let mut str_name = cstr_name.to_str().unwrap().to_string();
        println!("Rust get Input:  "{}"", str_name);
        let r_string: &str = " Rust say: Hello Go ";
        str_name.push_str(r_string);
        CString::new(str_name).unwrap().into_raw()
    }
    

    代码比较简单,Rust 暴露出来的函数名叫做 rustdemo ,接收一个外部的参数,并将其打印出来。之后从 Rust 这边再设置一个字符串。

    CString::new(str_name).unwrap().into_raw() 被转换为原始指针,以便之后由 C 语言处理。

    编译>

    我们需要修改下 Cargo.toml 文件以便进行编译。注意,这里我们增加了 crate-type = ["cdylib"] 和 libc 。

    [package]
    name = "rustdemo"
    version = "0.1.0"
    edition = "2021"
    [lib]
    crate-type = ["cdylib"]
    [dependencies]
    libc = "0.2"
    

    然后进行编译

    ➜  rustdemo git:(master) ✗ cargo build --release
       Compiling rustdemo v0.1.0 (/home/tao/go/src/github.com/tao12345666333/go-rust/lib/rustdemo)
        Finished release [optimized] target(s) in 0.22s
    

    查看生成的文件,这是一个 .so 文件(这是因为我在 Linux 环境下,你如果在其他系统环境下会不同)

    ➜  rustdemo git:(master) ✗ ls target/release/librustdemo.so 
    target/release/librustdemo.so
    

    准备>

    Go 环境的安装之类的这里也不再赘述了,继续在我们的 go-rust 目录操作即可。

    编写>
    package main
    /*
    #cgo LDFLAGS: -L./lib -lrustdemo
    #include <stdlib.h>
    #include "./lib/rustdemo.h"
    */
    import "C"
    import (
    	"fmt"
    	"unsafe"
    )
    func main() {
    	s := "Go say: Hello Rust"
    	input := C.CString(s)
    	defer C.free(unsafe.Pointer(input))
    	o := C.rustdemo(input)
    	output := C.GoString(o)
    	fmt.Printf("%sn", output)
    }
    

    在这里我们使用了 cgo ,在 import "C" 之前的注释内容是一种特殊的语法,这里是正常的 C 代码,其中需要声明使用到的头文件之类的。

    下面的代码很简单,定义了一个字符串,传递给 rustdemo 函数,然后打印 C 处理后的字符串。

    同时,为了能够让 Go 程序能正常调用 Rust 函数,这里我们还需要声明其头文件,在 lib/rustdemo.h 中写入如下内容:

    char* rustdemo(char *name);
    

    编译代码

    在>

    ➜  go-rust git:(master) ✗ cp lib/rustdemo/target/release/librustdemo.so lib
    

    所以完整的目录结构就是:

    ➜  go-rust git:(master) ✗ tree -L 2 .
    .
    ├── go.mod
    ├── lib
    │   ├── librustdemo.so
    │   ├── rustdemo
    │   └── rustdemo.h
    └── main.go
    2 directories, 5 files
    

    编译:

    ➜  go-rust git:(master) ✗ go build -o go-rust  -ldflags="-r ./lib" main.go
    ➜  go-rust git:(master) ✗ ./go-rust 
    Rust get Input:  "Go say: Hello Rust"
    Go say: Hello Rust Rust say: Hello Go
    

    可以看到,第一行的输出是由 Go 传入了 Rust , 第二行中则是从 Rust 再传回 Go 的了。符合我们的预期。

    总结

    本篇介绍了如何使用>