Rust FFI 通俗易懂手册

从其他语言使用 Rust 对象

让我们创建一个 Rust 对象,它将告诉我们每个美国邮政编码区域居住的人数。我们希望能够在其他语言中使用这个逻辑,但我们只需要通过 FFI 边界传递简单的基本类型,如整数或字符串。这个对象将具有可变和不可变方法。由于我们无法查看对象的内部,这通常被称为 不透明对象不透明指针

extern crate libc;

use libc::c_char;
use std::collections::HashMap;
use std::ffi::CStr;

pub struct ZipCodeDatabase {
    population: HashMap<String, u32>,
}

impl ZipCodeDatabase {
    fn new() -> ZipCodeDatabase {
        ZipCodeDatabase {
            population: HashMap::new(),
        }
    }

    fn populate(&mut self) {
        for i in 0..100_000 {
            let zip = format!("{:05}", i);
            self.population.insert(zip, i);
        }
    }

    fn population_of(&self, zip: &str) -> u32 {
        self.population.get(zip).cloned().unwrap_or(0)
    }
}

#[no_mangle]
pub extern "C" fn zip_code_database_new() -> *mut ZipCodeDatabase {
    Box::into_raw(Box::new(ZipCodeDatabase::new()))
}

#[no_mangle]
pub extern "C" fn zip_code_database_free(ptr: *mut ZipCodeDatabase) {
    if ptr.is_null() {
        return;
    }
    unsafe {
        Box::from_raw(ptr);
    }
}

#[no_mangle]
pub extern "C" fn zip_code_database_populate(ptr: *mut ZipCodeDatabase) {
    let database = unsafe {
        assert!(!ptr.is_null());
        &mut *ptr
    };
    database.populate();
}

#[no_mangle]
pub extern "C" fn zip_code_database_population_of(
    ptr: *const ZipCodeDatabase,
    zip: *const c_char,
) -> u32 {
    let database = unsafe {
        assert!(!ptr.is_null());
        &*ptr
    };
    let zip = unsafe {
        assert!(!zip.is_null());
        CStr::from_ptr(zip)
    };
    let zip_str = zip.to_str().unwrap();
    database.population_of(zip_str)
}

Rust 中的 struct 通常以常规方式定义。为对象的每个函数创建一个 extern 函数。由于 C 中没有内置的名称空间概念,因此通常使用包名和/或类型名作为每个函数的前缀。在此示例中,我们使用了 zip_code_database。按照正常的 C 约定,对象的指针始终作为第一个参数提供。

要创建对象的新实例,我们将对象的构造函数的结果包装成 Box。这将使结构体放在堆上,具有稳定的内存地址。使用 Box::into_raw 将该地址转换为原始指针。

此指针指向 Rust 分配的内存;由 Rust 分配的内存必须由 Rust 回收。当要释放对象时,我们使用 Box::from_raw 将指针转换回 Box<ZipCodeDatabase>。与其他函数不同,我们允许传递 NULL,但在这种情况下不执行任何操作。这对客户端程序员来说是一种便利。

要从原始指针创建引用,您可以使用简洁的语法 &*,这表示应解引用然后重新引用指针。创建可变引用类似,但使用 &mut *。与其他指针一样,您必须确保指针不为 NULL

请注意,*const T 可以自由地转换为 *mut T,没有什么能阻止客户端代码从不调用释放函数,或者多次调用它。内存管理和安全性保证完全由程序员掌握。

C

#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

typedef struct zip_code_database zip_code_database_t;

extern zip_code_database_t *
zip_code_database_new(void);

extern void
zip_code_database_free(zip_code_database_t *);

extern void
zip_code_database_populate(zip_code_database_t *);

extern uint32_t
zip_code_database_population_of(const zip_code_database_t *, const char *zip);

int main(void) {
  zip_code_database_t *database = zip_code_database_new();
  zip_code_database_populate(database);

  uint32_t pop1 = zip_code_database_population_of(database, "90210");
  uint32_t pop2 = zip_code_database_population_of(database, "20500");

  zip_code_database_free(database);

  printf("%" PRId32 "\n", (int32_t)pop1 - (int32_t)pop2);
}

C 代码中创建了一个虚拟结构以提供一定的类型安全性。const 修饰符在适当的函数上使用,尽管在 C 中 const-correctness 比 Rust 更灵活。

Ruby

require 'ffi'

class ZipCodeDatabase < FFI::AutoPointer
  def self.release(ptr)
    Binding.free(ptr)
  end

  def populate
    Binding.populate(self)
  end

  def population_of(zip)
    Binding.population_of(self, zip)
  end

  module Binding
    extend FFI::Library
    ffi_lib 'objects'

    attach_function :new, :zip_code_database_new,
                    [], ZipCodeDatabase
    attach_function :free, :zip_code_database_free,
                    [ZipCodeDatabase], :void
    attach_function :populate, :zip_code_database_populate,
                    [ZipCodeDatabase], :void
    attach_function :population_of, :zip_code_database_population_of,
                    [ZipCodeDatabase, :string], :uint32
  end
end

database = ZipCodeDatabase::Binding.new

database.populate
pop1 = database.population_of("90210")
pop2 = database.population_of("20500")

puts pop1 - pop2

Ruby 代码中,我们从 FFI::AutoPointer 继承一个小类,AutoPointer 会在对象释放时确保底层资源被释放。为此,用户必须定义 self.release 方法。

不幸的是,由于我们继承了 AutoPointer,我们无法重新定义初始化器。为了更好地将概念分组在一起,我们在一个嵌套模块中绑定 FFI 方法。我们为绑定的方法提供了较短的名称,这使客户端只需调用 ZipCodeDatabase::Binding.new

Python

#!/usr/bin/env python3

import sys, ctypes
from ctypes import c_char_p, c_uint32, Structure, POINTER

class ZipCodeDatabaseS(Structure):
    pass

prefix = {'win32': ''}.get(sys.platform, 'lib')
extension = {'darwin': '.dylib', 'win32': '.dll'}.get(sys.platform, '.so')
lib = ctypes.cdll.LoadLibrary(prefix + "objects" + extension)

lib.zip_code_database_new.restype = POINTER(ZipCodeDatabaseS)

lib.zip_code_database_free.argtypes = (POINTER(ZipCodeDatabaseS), )

lib.zip_code_database_populate.argtypes = (POINTER(ZipCodeDatabaseS), )

lib.zip_code_database_population_of.argtypes = (POINTER(ZipCodeDatabaseS), c_char_p)
lib.zip_code_database_population_of.restype = c_uint32

class ZipCodeDatabase:
    def __init__(self):
        self.obj = lib.zip_code_database_new()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        lib.zip_code_database_free(self.obj)

    def populate(self):
        lib.zip_code_database_populate(self.obj)

    def population_of(self, zip):
        return lib.zip_code_database_population_of(self.obj, zip.encode('utf-8'))

with ZipCodeDatabase() as database:
    database.populate()
    pop1 = database.population_of("90210")
    pop2 = database.population_of("20500")
    print(pop1 - pop2)

Python 代码中,我们创建一个空结构以表示我们的类型。这只会与 POINTER 方法一起使用,POINTER 方法会创建一个新类型,作为指向现有类型的指针。

为确保内存正确清理,我们使用了一个上下文管理器。这与我们的类绑定在一起,通过 __enter____exit__ 方法。我们使用 with 语句来启动一个新上下文。当上下文结束时,__exit__ 方法将自动调用,以防止内存泄漏。

Haskell

{-# LANGUAGE ForeignFunctionInterface #-}

import Data.Word (Word32)
import Foreign.Ptr
import Foreign.ForeignPtr
import Foreign.C.String (CString(..), newCString)

data ZipCodeDatabase

foreign import ccall unsafe "zip_code_database_new"
  zip_code_database_new :: IO (Ptr ZipCodeDatabase)

foreign import ccall unsafe "&zip_code_database_free"
  zip_code_database_free :: FunPtr (Ptr ZipCodeDatabase -> IO ())

foreign import ccall unsafe "zip_code_database_populate"
  zip_code_database_populate :: Ptr ZipCodeDatabase -> IO ()

foreign import ccall unsafe "zip_code_database_population_of"
  zip_code_database_population_of :: Ptr ZipCodeDatabase -> CString -> Word32

createDatabase :: IO (Maybe (ForeignPtr ZipCodeDatabase))
createDatabase = do
  ptr <- zip_code_database_new
  if ptr /= nullPtr
    then do
      foreignPtr <- newForeignPtr zip_code_database_free ptr
      return $ Just foreignPtr
    else
      return Nothing

populate = zip_code_database_populate

populationOf :: Ptr ZipCodeDatabase -> String -> IO (Word32)
populationOf db zip = do
  zip_str <- newCString zip
  return $ zip_code_database_population_of db zip_str

main :: IO ()
main = do
  db <- createDatabase
  case db of
    Nothing -> putStrLn "Unable to create database"
    Just ptr -> withForeignPtr ptr $ \database -> do
        populate database
        pop1 <- populationOf database "90210"
        pop2 <- populationOf database "20500"
        print (pop1 - pop2)

Haskell 代码从头开始定义了一个空类型来引用不透明对象。在定义导入的函数时,使用了 Ptr 类型构造函数,将这个新类型作为从 Rust 返回的指针的类型。我们还使用 IO,因为分配、释放和填充对象的内存都是具有副作用的函数。

由于分配理论上可能失败

,因此我们检查 NULL 并从构造函数中返回 Maybe,这可能有点过分,因为 Rust 目前在分配器失败时会中止进程。

为确保分配的内存被自动释放,我们使用了 ForeignPtr 类型。它接受一个原始 Ptr 和一个在包装指针被释放时调用的函数。

在使用包装指针时,我们使用 withForeignPtr 来解包它,然后将其传递给 FFI 函数。 ## Node.js

const ffi = require('ffi-napi');

const lib = ffi.Library('libobjects', {
  zip_code_database_new: ['pointer', []],
  zip_code_database_free: ['void', ['pointer']],
  zip_code_database_populate: ['void', ['pointer']],
  zip_code_database_population_of: ['uint32', ['pointer', 'string']],
});

const ZipCodeDatabase = function() {
  this.ptr = lib.zip_code_database_new();
};

ZipCodeDatabase.prototype.free = function() {
  lib.zip_code_database_free(this.ptr);
};

ZipCodeDatabase.prototype.populate = function() {
  lib.zip_code_database_populate(this.ptr);
};

ZipCodeDatabase.prototype.populationOf = function(zip) {
  return lib.zip_code_database_population_of(this.ptr, zip);
};

const database = new ZipCodeDatabase();
try {
  database.populate();
  const pop1 = database.populationOf('90210');
  const pop2 = database.populationOf('20500');
  console.log(pop1 - pop2);
} finally {
  database.free();
}

在导入这些函数时,我们只需声明返回或接受 pointer 类型。

为了使函数访问更简洁,我们创建了一个简单的类,它为我们维护指针并抽象了将其传递给底层函数。这还给我们提供了一个机会,使用惯用的 JavaScript 骆驼命名法来重命名函数。

为了确保资源得到清理,我们使用 try 块,在 finally 块中调用释放方法。

C#

using System;
using System.Runtime.InteropServices;

internal class Native
{
    [DllImport("objects")]
    internal static extern ZipCodeDatabaseHandle zip_code_database_new();
    [DllImport("objects")]
    internal static extern void zip_code_database_free(IntPtr db);
    [DllImport("objects")]
    internal static extern void zip_code_database_populate(ZipCodeDatabaseHandle db);
    [DllImport("objects")]
    internal static extern uint zip_code_database_population_of(ZipCodeDatabaseHandle db, string zip);
}

internal class ZipCodeDatabaseHandle : SafeHandle
{
    public ZipCodeDatabaseHandle() : base(IntPtr.Zero, true) {}

    public override bool IsInvalid
    {
        get { return this.handle == IntPtr.Zero; }
    }

    protected override bool ReleaseHandle()
    {
        if (!this.IsInvalid)
        {
            Native.zip_code_database_free(handle);
        }

        return true;
    }
}

public class ZipCodeDatabase : IDisposable
{
    private ZipCodeDatabaseHandle db;

    public ZipCodeDatabase()
    {
        db = Native.zip_code_database_new();
    }

    public void Populate()
    {
        Native.zip_code_database_populate(db);
    }

    public uint PopulationOf(string zip)
    {
        return Native.zip_code_database_population_of(db, zip);
    }

    public void Dispose()
    {
        db.Dispose();
    }

    static public void Main()
    {
        var db = new ZipCodeDatabase();
        db.Populate();

        var pop1 = db.PopulationOf("90210");
        var pop2 = db.PopulationOf("20500");

        Console.WriteLine("{0}", pop1 - pop2);
    }
}

由于调用 native 函数的职责将分散,我们创建了一个 Native 类来保存所有定义。

为了确保分配的内存会自动释放,我们创建了 SafeHandle 类的子类。这需要实现 IsInvalidReleaseHandle。由于我们的 Rust 函数接受释放 NULL 指针,我们可以说每个指针都是有效的。

我们可以使用我们的安全包装 ZipCodeDatabaseHandle 作为 FFI 函数的类型,除了释放函数。实际指针将自动进行封送到和从包装中。

我们还允许 ZipCodeDatabase 参与 IDisposable 协议,转发给安全包装。

Julia

#!/usr/bin/env julia
using Libdl

libname = "objects"
if !Sys.iswindows()
    libname = "lib$(libname)"
end

lib = Libdl.dlopen(libname)

zipcodedatabase_new_sym = Libdl.dlsym(lib, :zip_code_database_new)
zipcodedatabase_free_sym = Libdl.dlsym(lib, :zip_code_database_free)
zipcodedatabase_populate_sym = Libdl.dlsym(lib, :zip_code_database_populate)
zipcodedatabase_populationof_sym = Libdl.dlsym(lib, :zip_code_database_population_of)

struct ZipCodeDatabase
    handle::Ptr{Nothing}

    function ZipCodeDatabase()
        handle = ccall(zipcodedatabase_new_sym, Ptr

{Cvoid}, ())
        new(handle)
    end

    function ZipCodeDatabase(f::Function)
        database = ZipCodeDatabase()
        out = f(database)
        close(database)
        out
    end
end

close(database:: ZipCodeDatabase) = ccall(
    zipcodedatabase_free_sym,
    Cvoid, (Ptr{Cvoid},),
    database.handle
)

populate!(database:: ZipCodeDatabase) = ccall(
    zipcodedatabase_populate_sym,
    Cvoid, (Ptr{Cvoid},),
    database.handle
)

populationof(database:: ZipCodeDatabase, zipcode:: AbstractString) = ccall(
    zipcodedatabase_populationof_sym,
    UInt32, (Ptr{Cvoid}, Cstring),
    database.handle, zipcode
)

ZipCodeDatabase() do database
    populate!(database)
    pop1 = populationof(database, "90210")
    pop2 = populationof(database, "20500")
    println(pop1 - pop2)
end

与其他语言一样,我们在新数据类型后面隐藏一个处理程序指针。用于填充数据库的方法被命名为 populate!,以遵循 Julia 惯例,即具有修改值的方法上会有 ! 后缀。

目前关于 Julia 应如何处理本机资源尚无共识。虽然在这里,用于分配 ZipCodeDatabase 的内部构造函数是合适的,但我们可以想到许多方法来让 Julia 在后续释放它。在此示例中,我们展示了释放对象的两种方式:(1) 用于与 do 语法一起使用的映射构造函数,和 (2) 用于手动释放对象的 close 重载。内部构造函数 ZipCodeDatabase(f) 既负责创建又负责释放对象。使用 do 语法时,用户代码类似于使用 Python 的 with 语法。或者,程序员可以使用其他构造函数,并在不再需要时调用方法 close