DEV Community

Cover image for FreePascal/Lazarus and Rust Integration
Davide Del Papa
Davide Del Papa

Posted on

FreePascal/Lazarus and Rust Integration

Code available on Github

Going thorugh the TIOBE Index we can find that constantly, in the top 10 of the most popular languages, one find that constantly there are some "old" languages that "refuse to die." Among those we find the Delphi/Object Pascal, at the moment of writing it's on the 9th position, or Visual Basic, at the 7th position. Instead, new languages, like Rust can be found more often than not, below the top 10 (at the moment of writing, Rust is on the 13th position, behind both Perl and Fortran). Be it far from trying to give reaons for the popularity of a language instead of another; may it be the sheer amount of codebases still mantained in those languages, or other reasons, the fact is that those languages are still in use.

Seeing this I got thinking that maybe there is a potential benefit in talking about languages integration. So, why not integrate Rust and Pascal? In my case I'm presenting the Free Pascal dialect, and the Lazarus IDE (because I'm biased over Open Source), but the code can be adapted easily to other Pascal dialects.

This tutorial is split in two: in the first part I will present to you the "theory",a nd we will make up some contieved examples, and, while using Lazarus (which is a RAD IDE), we will concentrate on simple commandline Pascal applications (although in the companion Github repository there is an example of GUI usage). In the second part instead, we will produce a proper GUI application, with some real world usage, so stay tuned!

Hello, World!

Let's start with a sort of "hello world" integration: a simple Rust function that adds two numbers and a Pascal program that calls the function.

Rust Side

On the Rust side, let's create a library to be used by the Pascal code, which we will call rustlaz.

cargo new rustlaz --lib
cd rustlaz && zed .
Enter fullscreen mode Exit fullscreen mode

I write zed ., but you can write hx ., code ., or whichever editor you are using.

In Cargo.toml make sure we compile a library:

[lib]
crate-type = ["cdylib"]
Enter fullscreen mode Exit fullscreen mode

While in src/lib.rs we can create a dummy function:

#[unsafe(no_mangle)]
pub extern "C" fn add_numbers(a: i32, b: i32) -> i32 {
    a + b
}
Enter fullscreen mode Exit fullscreen mode

The #[unsafe(no_mangle)] will make sure that the function is ready to be called by an external language, and of course, the function itself must be pub extern "C" to leverage C-style FFI (Foreign Function Integration).

Now a build will give us a .so, a .dll, or a .dylib, according to our system (Linux, Windows, o Mac).

cargo build --release
Enter fullscreen mode Exit fullscreen mode

We need to copy the compiled library from /target/release/ into the root of the Lazarus project for a Windows system. In Linux instead, we need to copy it where the linker can see it, usually /usr/lib/:

sudo cp target/release/librustlaz.so /usr/lib/librustlaz.so 
Enter fullscreen mode Exit fullscreen mode

Lazarus Side

We start the Lazarus IDE and we select a New.. Project/Simple Program. We can save it in the root of our project as LazRust.lpi. In the companio Github repo this folder is called lazrust/.

At this point, before writing our program, we need to create a New unit and save it as RustLib.pas This unit will take care of integrating a C-style library interface, mapping it to Pascal functions.

This is the actual content of the unit:

unit RustLib;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils;

function add_numbers(a: LongInt; b: LongInt): LongInt; cdecl; external 'librustlaz';

implementation

end.
Enter fullscreen mode Exit fullscreen mode

With this we declare the interface of the function add_numbers of our external libray, so that we can use it in Lazarus freely. Seing how easy it is to call into freing functions in Pascal, no wonder it's a widespread and loved language.

Now we can go back to our main program and use the function:

program LazRust;

uses RustLib;

begin
  Writeln('Rust says 2 + 3 = ', add_numbers(2, 3));
end.
Enter fullscreen mode Exit fullscreen mode

Once compiled (SHIFT+F9), we can launch the program on the command line and see the result:

./lazrust

Rust says 2 + 3 = 5
Enter fullscreen mode Exit fullscreen mode

We can even make a standard GUI program that uses the library and in the folder LazRust2/ you will find an example that leverages the events onEditingDone of two edits to update a label. Feel free to take a look at the code.

A Slightly More Complex Example

We can complicatre a little the interface if we create a Point struct and we pass it around between Rust and Free Pascal.

In 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,
    }
}
Enter fullscreen mode Exit fullscreen mode

Now we have a struct and a function to handle that struct.
We can compile it (release mode) and copy again the library wherever visible by Lazarus (see notes above).

Now in lazarus again, we create a New.. Project/Simple Program.

The RustLib unit must look like the following:

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.
Enter fullscreen mode Exit fullscreen mode

In this way we "reconstruct" the Rust struct interface mapping it to a Pascal record called TPoint, and we link the FFI function to iterface with the TPoint as it were a Rust struct.
In this way, mapping one to one data structures and functions, we achieve 100% compatibility between the two languages.

Now onto our 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.
Enter fullscreen mode Exit fullscreen mode

The above code is easy enough; the usage in fact gets quite natural in Pascal.

Once compiled (SHIFT+F9), we can launch the program on the command line and see that indeed we are able to pass a Pascal record as a struct to Rust, and get the Rust struct back, mapped onto a record.

./lazrust

Original: (10, 20)
Moved:    (13, 18)
Enter fullscreen mode Exit fullscreen mode

You can check the code in lazrust3/

Handling Strings and Memory

Our third example is more complex still, as it involves managing strings and memory from both Rust and Pascal sides, to avoid memory leaks and unsafe behavior.

We start from Rust were we create a struct and a function that manipulates it (accepting that struct as a parameter, and returning a changed struct):

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,
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Besides the Person struct and the verify_person() function, in the above code we see two utilities (one to convert CStrings, i.e. *mut c_char, to Rust Strings, and one to create CStrings starting from &str).
We can see also a function to free a CString: we will call it from the Pascal side, so that Rust will properly free the allocated CStrings, and those memory spaces will not leak.

Having seen the Rust side of things, let's go over the Pascal side. We create a projectt as usual, and we map the Rust library with the usual Pascal unit, but this time with a twist:

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.
Enter fullscreen mode Exit fullscreen mode

As you can see, besides mapping the Person struct with a TPerson record, and besides exposing both the verify_person() and the free_cstring() functions, we have an utility in this unit as well: the function NewCString().
This utility will allocate the memory needed for a string and copy it over to the caler: we will use it to initialized the CStrings inside the struct to pass over to Rust.

Let's have a look at the actual program's code:

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.
Enter fullscreen mode Exit fullscreen mode

A you can see, we allocate the strings in the Pascal record to pass over to Rust (we do so with the utility we created above). After we finish, we use the expose Rust function free_cstring() to dispose of the allocated memory on the Rust side; however, we need to de-allocate the memory also on the Pascal's side: that can leak too.

Admittedly, we have a lot of clean up to do, but it is so, because we cannot use strings managed entirely by Rust, nor can we use the corresponding strings managed entirely by Pascal. Since we are working on the interoperability of these two language, we have to clean up the used resources by ourselves, intead of relying on managed types.

Other than the hygene nuissaces, that makes the code a tad more verbose, the interoperativity between the two languages seems really good.

The above code can be found in the lazrust4/ folder.

Conclusion

All in all the interfacing between Rust and Free Pascal goes really smooth. When facing the problem of renovating a whole legacy codebase, it is comforting to know that so much could be done by just re-writing and extending some key libraries with modern and secure Rust code. The possibilities are endless here. Not the least of them all is the fact that Lazarus can provide a way to create GUIs in an easy and time tested way: instead of chasing the chimera of a Rust only codebase, delegating some key features to other languages, sometimes is the smartest move available.

Stay tuned for Part II of this mini series.

Top comments (0)