其他语言使用 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类的子类。这需要 实现 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 随后释放它。在这个例子中,我们展示了释放对象的两种方法:

内部构造函数ZipCodeDatabase(f),同时负责创建和释放对象。 使用 do 语法,用户代码变得类似 Python 语法with。或者,程序员可以使用其他构造,并在不再需要时,调用close方法。