Zulip Chat Archive

Stream: lean4

Topic: Issue with String marshalling between Lean and Rust FFI


Daneshvar Amrollahi (Jul 17 2025 at 21:50):

Hello everyone,

I'm working on a project that uses Lean to call into a Rust library via FFI. I'm having trouble with passing file paths from Lean to Rust.

My setup:

In Lean

(FFI.Lean):

namespace SMTParser

@[extern "test_ffi"] opaque testFFI : UInt32  UInt32

@[extern "parse_smt_file"]
opaque parseSMTFile (path : @& String) : UInt32

end SMTParser

(Main.Lean):

def main (args : List String) : IO UInt32 := do
  match args with
  | [] => do
    IO.println "Please provide an SMT file path"
    return 1
  | filename :: _ => do
    IO.println s!"Processing SMT file: {filename}"
    SMTParser.processSMTFile filename

In Rust
(lib.rs):

#[unsafe(no_mangle)]
pub extern "C" fn parse_smt_file(_path: *const c_char) -> u32 {
    if _path.is_null() {
        eprintln!("Received null pointer for path");
        return 0;
    }

    // Convert C string to Rust string
    let path_str = unsafe {
        match CStr::from_ptr(_path).to_str() {
            Ok(s) => s,
            Err(e) => {
                eprintln!("Error converting path: {}", e);
                return 0;
            },
        }
    };

    eprintln!("Reading file: '{}'", path_str);

    // Read the file
    let contents = match fs::read_to_string(path_str) {
        Ok(s) => s,
        Err(e) => {
            eprintln!("Error reading file: {}", e);
            return 1;
        }
    };

    // Rest of the code...
}

When I pass a file path from Lean to Rust, the string doesn't seem to be properly marshalled. The Rust side can't correctly interpret the string pointer that Lean is passing.

A simple numeric FFI test works fine (test_ffi function that doubles an integer), but string passing is failing.

Does anyone know how can we properly pass a Lean String as a C-compatible string through Rust's FFI?

Arthur Paulino (Jul 17 2025 at 23:35):

This is how Lean strings are stored in memory:

// from lean.h
typedef struct {
    lean_object m_header;
    size_t      m_size;     /* byte length including '\0' terminator */
    size_t      m_capacity;
    size_t      m_length;   /* UTF8 length */
    char        m_data[];
} lean_string_object;

So you either have to intercept the call in C and pass ptr->m_data to Rust or you need to mimic that precise memory layout in Rust in order to access the m_data attribute

Arthur Paulino (Jul 17 2025 at 23:40):

Or, if you're feeling brave, add 32 to your pointer and read from there. It's the size of a lean_object (8 bytes) and three size_ts (8 bytes each on a 64bit machine).

Arthur Paulino (Jul 17 2025 at 23:57):

If you choose to mimic the memory layout in Rust, make sure to tag your LeanStringObject struct with #[repr(C)]!

Daneshvar Amrollahi (Jul 18 2025 at 00:11):

Thanks Arthur. I managed to do it by mimicing the struct in my Rust code.
But it seems like a very fragile solution. For instance if the String object for Lean changes for some reason in the future updates, this would break. Is there a more clean solution for this?

Arthur Paulino (Jul 18 2025 at 00:17):

These basic structs from lean.h are pretty stable already. But passing ptr->m_data (or, even better, lean_string_cstr(ptr)) from C to Rust is more stable because in the worst case you'd get a compilation error from your C code. The price of this solution is that your build becomes a bit messier, mixing up C and Rust.

Arthur Paulino (Jul 18 2025 at 00:20):

But keep in mind that FFI is fundamentally unsafe, so it's always gonna feel a bit itchy.


Last updated: Dec 20 2025 at 21:32 UTC