关注「索引目录」公众号,获取更多干货。
你好世界!
让我们从一个“Hello World”集成开始:一个简单的 Rust 函数,用于将两个数字相加,以及一个调用该函数的 Pascal 程序。
锈蚀面
在 Rust 端,我们创建一个供 Pascal 代码使用的库,我们称之为rustlaz。
cargo new rustlaz --lib
cd rustlaz && zed .
我写的是 zed .,但你可以写的是hx .,code .或者任何你正在使用的编辑器。
在Cargo.toml 文件中,确保我们编译了一个库:
[lib]
crate-type = ["cdylib"]
在src/lib.rs中,我们可以创建一个虚拟函数:
#[unsafe(no_mangle)]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
a + b
}
这#[unsafe(no_mangle)]将确保该函数已准备好被外部语言调用,当然,该函数本身必须能够pub extern "C"利用 C 风格的 FFI(外部函数集成)。
现在,根据我们的系统(Linux、Windows 或 Mac),构建过程会生成.so 文件、.dll文件或.dylib文件。
cargo build --release
在 Windows 系统中,我们需要将编译后的库文件从/target/release/复制到 Lazarus 项目的根目录。而在 Linux 系统中,我们需要将其复制到链接器可以访问的位置,通常是/usr/lib/。
sudo cp target/release/librustlaz.so /usr/lib/librustlaz.so
拉撒路·赛德
我们启动 Lazarus IDE 并选择一个New.. Project/Simple Program。我们可以将其保存在项目根目录下,命名为LazRust.lpi。在配套的 Github 仓库中,该文件夹名为lazrust/。
此时,在编写程序之前,我们需要创建一个New unit并将其保存为RustLib.pas。该单元将负责集成 C 风格的库接口,并将其映射到 Pascal 函数。
这是本单元的实际内容:
unit RustLib;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils;
function add_numbers(a: LongInt; b: LongInt): LongInt; cdecl; external 'librustlaz';
implementation
end.
这样我们就声明了库中函数 ` add_numbers`external的接口,以便在 Lazarus 中自由使用它。鉴于在 Pascal 中调用外部函数如此简单,难怪它是一种广泛应用且备受喜爱的语言。
现在我们可以回到主程序并使用该函数:
program LazRust;
uses RustLib;
begin
Writeln('Rust says 2 + 3 = ', add_numbers(2, 3));
end.
编译完成后(SHIFT+F9),我们就可以在命令行运行程序并查看结果:
./lazrust
Rust says 2 + 3 = 5
我们甚至可以创建一个使用该库的标准 GUI 程序。在LazRust2/文件夹中,您可以找到一个利用onEditingDone两次编辑事件来更新标签的示例。欢迎查看代码。
一个稍微复杂一些的例子
Point如果我们创建一个结构体并在 Rust 和 Free Pascal 之间传递它,可能会使接口变得稍微复杂一些。
在 Rust 中,rustlaz/src/lib.rs:
use std::os::raw::c_int;
#[repr(C)]
pub struct Point {
pub x: c_int,
pub y: c_int,
}
#[unsafe(no_mangle)]
pub extern "C" fn move_point(p: Point, dx: c_int, dy: c_int) -> Point {
Point {
x: p.x + dx,
y: p.y + dy,
}
}
现在我们有了一个结构体和一个处理该结构体的函数。
我们可以编译它(发布模式),并将库复制到 Lazarus 可见的任何地方(参见上面的注释)。
现在,我们再次以拉撒路的身份,创造了一个New.. Project/Simple Program。
RustLib 单元必须类似于以下内容:
unit RustLib;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils;
type
TPoint = record
x: LongInt;
y: LongInt;
end;
function move_point(p: TPoint; dx: LongInt; dy: LongInt): TPoint; cdecl; external 'librustlaz';
implementation
end.
这样,我们就“重构”了 Rust 结构体接口,将其映射到名为 `<record>` 的 Pascal 记录TPoint,并将 FFI 函数链接到 `<iterface>`,就像链接到TPointRust 结构体一样。
通过这种方式,将数据结构和函数一一对应,我们实现了两种语言之间的 100% 兼容性。
现在来看我们的lazrust.lpr:
program lazrust;
uses RustLib;
var
P: TPoint;
P2: TPoint;
begin
P.x := 10;
P.y := 20;
P2 := move_point(P, 3, -2);
Writeln('Original: (', P.x, ', ', P.y, ')');
Writeln('Moved: (', P2.x, ', ', P2.y, ')');
end.
上面的代码很简单;实际上,在 Pascal 中使用起来非常自然。
编译完成后(SHIFT+F9),我们可以在命令行启动程序,并看到我们确实能够将 Pascal 记录作为结构体传递给 Rust,并得到映射到记录上的 Rust 结构体。
./lazrust
Original: (10, 20)
Moved: (13, 18)
您可以在lazrust3/中查看代码。
处理字符串和内存
我们的第三个例子更加复杂,因为它涉及从 Rust 和 Pascal 两方面管理字符串和内存,以避免内存泄漏和不安全行为。
我们从 Rust 开始,创建一个结构体和一个操作该结构体的函数(接受该结构体作为参数,并返回修改后的结构体):
use std::ffi::{CStr, CString};
use std::os::raw::{c_char, c_int};
#[repr(C)]
pub struct Person {
name: *mut c_char,
office: *mut c_char,
phone: *mut c_char,
age: c_int,
}
/// Utility to convert &str to *mut c_char.
fn make_cstring(s: &str) -> *mut c_char {
CString::new(s).unwrap().into_raw()
}
/// Utility to convert *mut c_char to Rust String.
unsafe fn to_rust_string(ptr: *mut c_char) -> String {
if ptr.is_null() {
String::new()
} else {
unsafe { CStr::from_ptr(ptr).to_string_lossy().to_string() }
}
}
/// Free CStrings.
#[unsafe(no_mangle)]
pub extern "C" fn free_cstring(s: *mut c_char) {
if s.is_null() {
return;
}
unsafe {
drop(CString::from_raw(s));
}
}
#[unsafe(no_mangle)]
pub extern "C" fn verify_person(p: Person) -> Person {
unsafe {
let name = to_rust_string(p.name);
let office = to_rust_string(p.office);
let phone = to_rust_string(p.phone);
// Add "(verified)" to the name
let new_name = format!("{} {}", name, "(verified)");
Person {
name: make_cstring(&new_name),
office: make_cstring(&office),
phone: make_cstring(&phone),
age: p.age,
}
}
}
除了Person结构体和verify_person()函数之外,上面的代码中还包含两个实用工具(一个用于将 CString 转换*mut c_char为 Rust 字符串,另一个用于从 CString 对象创建 CString &str)。
此外,我们还可以看到一个用于释放 CString 的函数:我们将从 Pascal 端调用它,以便 Rust 能够正确释放已分配的 CString,从而避免内存泄漏。
了解了 Rust 部分之后,我们来看看 Pascal 部分。我们像往常一样创建一个项目,并将 Rust 库映射到常用的 Pascal 单元,但这次略有不同:
unit rustlib;
{$mode ObjFPC}{$H+}
interface
uses
Classes, SysUtils;
type
PPerson = ^TPerson;
TPerson = record
Name: pchar;
office: pchar;
phone: pchar;
age: longint;
end;
function verify_person(p: TPerson): TPerson; cdecl; external 'librustlaz';
function free_cstring(s: pchar): longint; cdecl; external 'librustlaz';
function NewCString(const S: string): pchar;
implementation
function NewCString(const S: string): pchar;
var
Len: SizeInt;
begin
Len := Length(S) + 1; // include null terminator
Result := StrAlloc(Len);
StrPLCopy(Result, S, Len);
end;
end.
如您所见,除了将Person结构体映射到TPerson记录,以及公开 `and`verify_person()和 ` free_cstring()functions` 函数之外,本单元还包含一个实用工具:`or` 函数NewCString()。
该实用工具会分配字符串所需的内存并将其复制到调用方:我们将使用它来初始化结构体中的 CString,以便将其传递给 Rust。
让我们来看看程序的实际代码:
program lazrust;
uses rustlib, SysUtils;
var
P, P2: TPerson;
begin
P.name := NewCString('John Doe');
P.office := NewCString('IT');
P.phone := NewCString('555-0101');
P.age := 28;
P2 := verify_person(P);
Writeln('Rust returned:');
Writeln(' name: ', P2.name);
Writeln(' office: ', P2.office);
Writeln(' phone: ', P2.phone);
Writeln(' age: ', P2.age);
// Free Rust-allocated strings
free_cstring(P2.name);
free_cstring(P2.office);
free_cstring(P2.phone);
// Free Pascal-allocated strings
StrDispose(P.name);
StrDispose(P.office);
StrDispose(P.phone);
end.
如您所见,我们在 Pascal 记录中分配字符串,以便传递给 Rust(我们使用上面创建的实用程序来完成此操作)。完成后,我们使用 Rust 的 expose 函数free_cstring()来释放 Rust 端已分配的内存;但是,我们也需要在 Pascal 端释放内存:这同样可能导致内存泄漏。
诚然,我们有很多清理工作要做,这是因为我们既不能使用完全由 Rust 管理的字符串,也不能使用完全由 Pascal 管理的对应字符串。由于我们正在研究这两种语言的互操作性,因此我们必须自行清理已使用的资源,而不是依赖于托管类型。
除了卫生方面的一些小麻烦(这使得代码稍微冗长一些)之外,两种语言之间的互操作性似乎真的很好。
以上代码位于lazrust4/文件夹中。
结论
总而言之,Rust 和 Free Pascal 之间的接口非常流畅。面对改造整个遗留代码库的难题,令人欣慰的是,只需用现代且安全的 Rust 代码重写和扩展一些关键库,就能完成大量工作。这里的可能性是无限的。尤其值得一提的是,Lazarus 提供了一种简单且久经考验的创建 GUI 的方法:与其追求完全使用 Rust 编写代码库的不切实际的幻想,不如将一些关键功能委托给其他语言,这有时才是最明智的选择。
关注「索引目录」公众号,获取更多干货。

