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.
| Method | Description |
|---|---|
| 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")?;
.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")?;
"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 string | Output |
|---|---|
pdf | PDF (also the fallback for any unrecognised string) |
csv | Comma-separated values |
xlsx | Excel workbook |
xlsx_table | Excel workbook (table style) |
xml | XML data |
rtf | Rich Text Format |
tif | TIFF image (colour) |
tifb | TIFF image (black & white) |
html | HTML |
mht | MHTML web archive |