其他语言使用 Rust 对象
让我们创建一个 Rust 对象,用来告诉我们,每个美国邮政编码有多少人。我们希望能够在其他语言中使用此逻辑,但我们只需要 在 FFI 边界上,传递整数或字符串等简单原始类型。而该对象会具有可变和不可变的方法。因为我们无法查看对象内部,所以,这通常被称为不透明的物体或者不透明的指针。
extern crate libc;
use libc::{c_char, uint32_t};
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..100000 {
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 fn zip_code_database_new() -> *mut ZipCodeDatabase {
Box::into_raw(Box::new(ZipCodeDatabase::new()))
}
#[no_mangle]
pub extern fn zip_code_database_free(ptr: *mut ZipCodeDatabase) {
if ptr.is_null() { return }
unsafe { Box::from_raw(ptr); }
}
#[no_mangle]
pub extern fn zip_code_database_populate(ptr: *mut ZipCodeDatabase) {
let database = unsafe {
assert!(!ptr.is_null());
&mut *ptr
};
database.populate();
}
#[no_mangle]
pub extern fn zip_code_database_population_of(ptr: *const ZipCodeDatabase, zip: *const c_char) -> uint32_t {
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)
}
该struct
以 Rust 正常的方式定义的。为对象的每个函数,创建一个extern
函数. C 没有内置命名空间的概念,因此,为每个函数添加,一个包名或类型名称前缀,是很正常的。 对于这个例子,我们使用zip_code_database
。遵循正常的 C 规范,始终将,指向对象的指针 作为 第一个参数。
要创建一个新的对象实例,我们 box
化对象的构造函数。(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>
typedef struct zip_code_database_S 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("%d\n", (int32_t)pop1 - (int32_t)pop2);
}
创建一个虚拟结构,以提供少量的类型安全性。
该const
修饰符给到了适当的函数,甚至即便,const-正确性 在 C 中 比 在 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
为了包装原始函数,我们创建了一个继承自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)
我们创建一个空结构(structure)来表示我们的类型。这只会与POINTER
方法结合使用,该方法会创建一个新类型,作为指向现有指针的指针。
为确保正确清理内存,我们使用了一个context manager。这通过__enter__
和__exit__
方法,绑住我们的 class。我们使用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)
我们首先定义一个空类型,来引用不透明对象。在定义导入的函数时,我们使用Ptr
的类型构造新类型, 将作为从 Rust 返回的指针的类型。我们也用到IO
了,因为 分配,释放和 populating 对象,都是具有副作用的函数。
由于分配,理论上可能会失败,我们会检查NULL
,并从构造函数返回一个Maybe
。这可能有点过头了,因为其实当分配器失败时,Rust 会中止该过程。
为了确保自动释放,分配的内存,我们使用ForeignPtr
类型。 这需要一个原始的Ptr
,以及 在解分配包装指针时,调用的函数。
使用包装指针时,withForeignPtr
用于在传递回 FFI 函数之前,展开包装指针。
Node.js
const ffi = require('ffi');
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 false; }
}
protected override bool ReleaseHandle()
{
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
类,用于保存所有定义。
为了确保自动释放分配的内存,我们创建了一个SafeHandle
类的子类。这需要 实现 IsInvalid
和ReleaseHandle
。由于我们的 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
方法。