← SDKs
Rust SDK

majorsilence-reporting

Render RDL reports to PDF, Excel, CSV, and more from Rust — in-process native FFI via libloading. No .NET runtime. No subprocess. Thread-safe, Arc-backed library handle. One dependency.


Installation

The crate is not yet published to crates.io. Add it as a path dependency:

# Cargo.toml
[dependencies]
majorsilence-reporting = { path = "/path/to/reporting-rust" }

# Once published to crates.io:
# majorsilence-reporting = "1.0"

The only runtime dependency is libloading = "0.8". Rust edition 2021, minimum Rust 1.65.


Quick start

use majorsilence_reporting::RdlLibrary;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Load the native library once per process.
    let lib = RdlLibrary::load("/path/to/rdlnative/librdlnative.so")?;

    // Build a report configuration.
    let mut rpt = lib.report("/path/to/report.rdl");
    rpt.set_connection_string("Data Source=/path/to/northwindEF.db")
       .set_parameter("Country", "Germany");

    // Export to a file.
    rpt.export("pdf", "/tmp/output.pdf")?;

    // Or export to memory — no temp file written.
    let pdf_bytes: Vec<u8> = rpt.export_to_memory("pdf")?;
    println!("Got {} bytes", pdf_bytes.len());

    Ok(())
}

Loading the library

RdlLibrary::load does all necessary setup: it sets RDLNATIVE_LIB_DIR, pre-loads sibling .so / .dylib files with RTLD_GLOBAL so the .NET P/Invoke resolver can find them, resolves all FFI symbols, and calls rdl_init().

use majorsilence_reporting::RdlLibrary;

// Call once at process startup. RdlLibrary is Arc-backed — cheap to clone and Send + Sync.
let lib = RdlLibrary::load("/path/to/librdlnative.so")?;

// Clone it across threads (e.g. into Axum state or Actix Data)
let lib2 = lib.clone();
std::thread::spawn(move || {
    let mut rpt = lib2.report("/path/to/report.rdl");
    rpt.export("pdf", "/tmp/thread.pdf").unwrap();
});
LD_LIBRARY_PATH (Linux) or DYLD_LIBRARY_PATH (macOS) must include the native library directory before the process starts. RdlLibrary::load handles RDLNATIVE_LIB_DIR and sibling pre-loading, but cannot retroactively modify the dynamic linker search path for the current process.

Core API

Builder methods return &mut Self for chaining. A new native report handle is opened and closed for each call to export or export_to_memory, so a Report value can be reused across multiple renders.

MethodDescription
RdlLibrary::load(path)Load the native library and initialise the FFI symbol table. Returns Result<RdlLibrary, Error>. Call once per process.
lib.report(rdl_path)Create a Report value bound to an .rdl file. Cheap — no native handle is opened yet.
rpt.set_connection_string(cs)Set the database connection string. Returns &mut Self.
rpt.set_parameter(name, value)Set a named report parameter. Returns &mut Self. Call as many times as needed.
rpt.add_data(name, rows)Supply an in-memory dataset. rows is Vec<HashMap<String, String>>. Returns &mut Self. See In-memory datasets.
rpt.export(format, path)Render and write to a file path. Returns Result<(), Error>.
rpt.export_to_memory(format)Render and return the bytes. Returns Result<Vec<u8>, Error>. No temp file is written.

In-memory datasets

Pass structured data directly to the report without a database. Each row is a HashMap<String, String> where keys are column names and values are string-encoded field values.

use std::collections::HashMap;
use majorsilence_reporting::RdlLibrary;

let lib = RdlLibrary::load("/path/to/librdlnative.so")?;
let mut rpt = lib.report("/path/to/report.rdl");

let rows: Vec<HashMap<String, String>> = vec![
    [("Product", "PDF Library Pro"), ("Revenue", "1200.00"), ("Units", "3")]
        .iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
    [("Product", "Report Designer"),  ("Revenue", "250.00"),  ("Units", "1")]
        .iter().map(|(k, v)| (k.to_string(), v.to_string())).collect(),
];

rpt.add_data("SalesData", rows);
let pdf = rpt.export_to_memory("pdf")?;
The dataset name must match a dataset defined in the .rdl file. All field values must be strings — convert numbers, dates, and booleans before inserting into the map. Multiple datasets can be supplied with multiple add_data calls.

Export to memory

export_to_memory calls the native rdl_report_render_buffer FFI function, which fills a native-allocated buffer and returns a pointer + length. The Rust wrapper copies the bytes into a Vec<u8> and calls rdl_free. No temp file is written at any point — useful for HTTP response bodies or pipeline integration.

// Axum handler example
async fn report_handler(State(lib): State<RdlLibrary>) -> impl IntoResponse {
    let mut rpt = lib.report("/reports/sales.rdl");
    rpt.set_connection_string("Data Source=/data/northwindEF.db");

    match rpt.export_to_memory("pdf") {
        Ok(bytes) => (
            StatusCode::OK,
            [("content-type", "application/pdf")],
            bytes,
        ).into_response(),
        Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
    }
}

Error handling

All fallible operations return Result<T, majorsilence_reporting::Error>. Error implements std::error::Error and wraps the message returned by the native rdl_last_error() function.

match rpt.export_to_memory("pdf") {
    Ok(bytes) => { /* use bytes */ }
    Err(e)    => eprintln!("render failed: {e}"),
}

// Or propagate with ?
let bytes = rpt.export_to_memory("pdf")?;
An unrecognised format string does not return an error — it silently falls back to "pdf". To enumerate valid formats at compile time, use the public constant majorsilence_reporting::VALID_FORMATS: &[&str].

Export formats

All ten formats are supported. Unlike the Python/PHP/Ruby subprocess modes, all formats are available in the Rust wrapper because it always uses the native FFI path.

Format stringOutput
pdfPDF (also the fallback for any unrecognised string)
csvComma-separated values
xlsxExcel workbook
xlsx_tableExcel workbook (table style)
xmlXML data
rtfRich Text Format
tifTIFF image (colour)
tifbTIFF image (black & white)
htmlHTML
mhtMHTML web archive