让我们创建一个 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,没有什么能阻止客户端代码从不调用释放函数,或者多次调用它。内存管理和安全性保证完全由程序员掌握。
#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
更灵活。
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。
#!/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__
方法将自动调用,以防止内存泄漏。
{-# 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 块中调用释放方法。
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
类的子类。这需要实现 IsInvalid 和
ReleaseHandle。由于我们的 Rust 函数接受释放
NULL 指针,我们可以说每个指针都是有效的。
我们可以使用我们的安全包装 ZipCodeDatabaseHandle 作为
FFI 函数的类型,除了释放函数。实际指针将自动进行封送到和从包装中。
我们还允许 ZipCodeDatabase 参与 IDisposable
协议,转发给安全包装。
#!/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。