The LibreOffice Rust UNO Bindings project delivers a production-ready, type-safe Rust language binding for LibreOffice's Universal Network Objects (UNO) API, featuring automatic code generation for all UNO types through an innovative three-layer FFI architecture. We successfully bridged Rust's memory safety guarantees with LibreOffice's complex C++ UNO system, enabling direct runtime integration and safe office automation capabilities. This technical report explores our architectural decisions, implementation challenges, and lessons learned that can benefit developers working on similar cross-language integration projects.
We built this project to enable Rust developers to write scripts, extensions, and business logic for LibreOffice using Rust's safety and performance advantages.
This binding system allows developers to create LibreOffice scripts in Rust for document automation, data processing workflows, and custom document manipulation tools as an alternative to Basic or Python scripting. We can now develop powerful LibreOffice extensions that add new functionality to Writer, Calc, Impress, and Draw while leveraging Rust's ecosystem and libraries. The system also enables integrating LibreOffice document processing into larger Rust applications and workflows with full access to all LibreOffice capabilities.
Previously, developers were limited to C++, Java, and Python for LibreOffice extension development. We've now added Rust as a option, giving developers access to Rust's memory safety, performance, and rich ecosystem while working with LibreOffice's comprehensive document processing capabilities.
When we started this project, we faced a fundamental challenge: how do we bridge LibreOffice's complex C++ object model with interfaces, abstract classes, inheritance, multiple inheritance, vtables, and polymorphism to Rust's memory-safe type system while maintaining both safety and performance?
LibreOffice UNO types present several complex features that don't map directly to Rust. We have interfaces with single and multiple inheritance hierarchies, where a single object might implement dozens of interfaces through complex vtable structures. Beyond interfaces, UNO includes structs, POD (Plain Old Data) types, enums, sequences (arrays), and complex nested types that require careful memory layout considerations. UNO uses dynamic interface querying where you can ask any object "do you support interface X?" and get back a properly typed pointer if it does. The system also heavily relies on reference counting for memory management across interface boundaries.
We considered existing Rust-C++ binding tools like cxx.rs, bindgen, and others, but they all fell short of our requirements.
cxx.rs has significant limitations for our use case: it requires manual definition of every type and function in both Rust and C++, making it impractical for UNO's extensive type system. It doesn't support C++ inheritance hierarchies or virtual methods, which are fundamental to UNO interfaces. The tool also requires static knowledge of all types at compile time, while UNO's dynamic interface querying needs runtime type information.
bindgen creates direct unsafe bindings to C++ code, which would expose raw vtable pointers and manual memory management to Rust developers. While it can handle large APIs automatically, it doesn't understand UNO's semantic requirements and would generate bindings that are difficult to use safely. The generated code would require extensive unsafe blocks and manual lifetime management.
Most importantly, neither tool understands UNO's specific requirements. UNO interface conversion requires UNO_QUERY operations rather than simple C++ casts, and the memory management follows UNO's reference counting rules, not standard C++ RAII patterns. POD types need specific memory layout guarantees, and sequences require special handling for their dynamic array nature.
Before we could design our solution, we had to understand the fundamental type system challenges that make UNO-to-Rust conversion so complex. The problems stem from the fundamental differences between C++ object models and what can safely cross FFI boundaries.
POD (Plain Old Data) Types are simple C++ types that have no user-defined constructors, destructors, virtual functions, or complex inheritance - essentially primitive types and old-style C structs with only public data members. These types have predictable, platform-consistent memory layout that matches C's memory model exactly. If all UNO types were POD, our problem would be dramatically simpler because we could use bindgen to automatically generate Rust bindings with #[repr(C)] structs that directly match the C++ memory layout, enabling direct function calls without complex conversion logic.
Non-POD Types introduce fundamental incompatibilities with FFI. These types have:
- Constructors that must be called to properly initialize memory
- Destructors that must be called to clean up resources
- Virtual function tables (vtables) with function pointers expecting C++ calling conventions
- Reference counting mechanisms requiring coordinated memory management across language boundaries
Each of these features creates a barrier that standard FFI cannot cross safely.
Bindgen Limitations with Virtual Functions: When bindgen encounters non-POD types, it faces insurmountable architectural limitations. For classes with virtual functions, bindgen generates raw function pointer tables (vtables) that point to C++ functions, but these function pointers are fundamentally incompatible with Rust's FFI model. They expect C++ calling conventions, proper C++ exception handling mechanisms, and specific this pointer setup that Rust cannot provide. Attempting to call these function pointers from Rust would result in undefined behavior, crashes, or memory corruption.
Reference Counting Complexity: Types with reference counting semantics would be generated as raw structs containing function pointers for reference management operations, but bindgen cannot generate the complex logic needed to automatically call these functions at the right times. Every reference copy, assignment, or destruction would require manual intervention, creating an API where memory safety depends entirely on correct manual calls by the developer - precisely what Rust's ownership system is designed to eliminate.
UNO-Specific Interface Querying: Most critically for UNO, the dynamic interface querying system represents a semantic concept that cannot be translated by any automatic binding generator. UNO_QUERY operations perform runtime type checking, interface negotiation, and reference count management in a single atomic operation. These operations are implemented as C++ template instantiations with complex type traits and RTTI (Run-Time Type Information) that bindgen cannot parse or understand. The result would be raw, untyped pointer casts that completely bypass UNO's type safety mechanisms.
Platform-Specific Memory Layouts: C++ non-POD types often have platform-specific memory layouts that can vary between compilers, architectures, and even compilation flags. Virtual function tables, multiple inheritance structures, and reference counting fields may be arranged differently on different systems. Rust's FFI model requires exact memory layout compatibility, but non-POD C++ types cannot provide this guarantee.
Fundamental Memory Management Incompatibility: At the deepest level, the challenge stems from fundamental philosophical differences between C++ and Rust memory management. C++ uses manual resource management with reference counting and explicit lifetime control, while Rust uses ownership with compiler-enforced borrowing rules. UNO's non-POD types are designed around C++'s model and cannot be safely adapted to Rust's ownership system through simple automatic translation.
FFI Type Compatibility: Rust's FFI can only safely handle POD-like types that match C's memory model. Non-POD C++ types cannot be safely represented in Rust because they may have different memory layouts on different platforms, require constructor calls to initialize properly, need destructor calls for cleanup, or contain vtable pointers that point to C++ virtual function tables.
Type Conversion and Method Calls: UNO interfaces cannot be cast using simple pointer casts because they require UNO_QUERY operations that perform runtime type checking and reference count management. When calling UNO methods from Rust, each parameter type requires different handling strategies. POD types like integers can be passed directly by value, but interface types need proper wrapper construction and reference counting.
Memory Lifecycle Management: The most critical challenge is that C++ UNO uses reference counting and manual memory management, while Rust uses ownership and RAII. When UNO methods return objects, someone must call their destructors, but Rust's ownership system cannot automatically manage C++ objects. When interfaces are passed between languages, their reference counts must be properly incremented and decremented, but Rust cannot automatically call UNO's acquire() and release() methods.
Instead of trying to directly map C++ inheritance to Rust traits or using existing tools that fail with non-POD types, we developed a novel three-layer opaque pointer architecture that completely sidesteps the FFI compatibility problems while maintaining both safety and performance.
Our solution recognizes that the fundamental challenge is not just technical but architectural - we need to bridge two incompatible memory management paradigms. Rather than forcing compatibility where none exists, we created a clean separation that allows each language to work within its strengths while providing safe communication across the boundary.
Layer 1: Rust RAII Wrappers - The top layer provides safe, ergonomic Rust interfaces that feel natural to Rust developers. Each UNO interface becomes a simple Rust struct containing only a *mut c_void pointer, but wrapped with comprehensive safety guarantees. These wrappers implement Rust's Drop trait for automatic cleanup and provide null-pointer safety through Option<T> return types. The Rust layer never directly manipulates UNO objects - it only holds opaque handles and makes calls through the bridge layer.
Layer 2: C++ Bridge Functions - The middle layer consists of generated extern "C" functions that handle all UNO complexity while presenting a simple C-compatible interface to Rust. These functions perform the actual UNO method calls, handle interface querying with proper UNO_QUERY operations, manage reference counting through Reference<T> wrappers, and provide comprehensive exception handling with SAL_WARN logging. Each bridge function is essentially a small adapter that translates between Rust's simple handle-based calls and UNO's complex object model.
Layer 3: LibreOffice UNO - The bottom layer remains unchanged - native UNO interfaces with their full C++ complexity, inheritance hierarchies, reference counting, and dynamic interface querying. Our architecture preserves all UNO semantics while making them safely accessible from Rust.
Our opaque pointer approach elegantly solves the POD vs non-POD challenge by eliminating the need for direct type translation. Instead of trying to represent complex C++ types in Rust (which we established is impossible), we represent them as opaque handles that Rust never needs to understand.
What are Opaque Pointers? An opaque pointer is simply a *mut c_void (or void* in C) that points to some memory, but the type information about what's stored at that address is hidden from the receiving language. In our case, Rust holds a *mut c_void that actually points to a C++ Reference<XInterface> object, but Rust never needs to know this detail. The pointer is "opaque" because Rust cannot see through it to understand the underlying C++ object structure - it's just an address that can be passed back to C++ functions that do understand the type.
This approach solves the fundamental FFI problem we described earlier - Rust never needs to understand C++ vtables, reference counting, or constructor/destructor semantics. It just holds addresses and passes them to functions that do understand these concepts.
Type-Safe Handle Generation: Our code generator creates unique typedefs for each UNO interface to provide compile-time type safety at the C boundary. Instead of raw void* everywhere, we generate specific handle types like typedef void* XTextDocumentHandle that prevent accidentally passing the wrong interface type to bridge functions. This solved a critical issue identified during development where extern "C" function names were causing linkage conflicts - our opaque pointer approach naturally generates unique function names like XTextDocument_getText that avoid these conflicts.
How Rust's Drop Trait Works in Our Architecture: Rust's Drop trait is similar to a destructor in C++ - it's automatically called when a value goes out of scope. In our Rust wrappers, we implement Drop to ensure that when a Rust object is destroyed, we call the corresponding C++ cleanup function. For example, when a Rust XTextDocument wrapper is dropped, its Drop implementation calls the XTextDocument_destructor C function, which then properly deletes the C++ Reference<XTextDocument> object. This creates automatic memory management that spans both languages.
The beauty of this system is that Rust's ownership rules ensure that cleanup happens at exactly the right time, while the C++ side handles the complex UNO reference counting. We get Rust's memory safety guarantees while preserving UNO's existing memory management semantics.
Constructor and Destructor Patterns: Our architecture implements consistent constructor/destructor patterns across all generated types. For interfaces, we generate XInterface_new() functions that create new Reference<XInterface>() objects and return them as opaque handles, while XInterface_destructor(handle) functions use delete static_cast<Reference<XInterface>*>(handle) to properly clean up the C++ objects. For structs, constructors create new StructType() instances with proper initialization, and destructors handle cleanup of any nested objects or resources. The Rust side automatically calls destructors through the Drop trait implementation, ensuring that C++ resources are always properly released when Rust objects go out of scope.
Bridge Function Pattern: Each bridge function follows a consistent pattern that safely translates between Rust handles and UNO objects:
// Example: Generated C++ bridge function
extern "C" void* XTextDocument_getText(void* handle) {
Reference<XTextDocument>* ref = static_cast<Reference<XTextDocument>*>(handle);
if (!ref || !ref->is()) return nullptr;
try {
auto result = (*ref)->getText();
return new Reference<XText>(result);
} catch (const Exception& e) {
SAL_WARN("rustmaker", "UNO exception: " << e.Message);
return nullptr;
}
}The bridge functions handle all UNO complexity - interface querying with UNO_QUERY, reference counting through Reference<T> wrappers, and comprehensive exception handling - while presenting a simple C interface to Rust.
Member Variable Access and Struct Handling: For UNO structs, we generate complete getter and setter functions that provide safe access to all member variables, including those inherited from base types. Each struct member gets its own dedicated bridge functions - for example, a struct with a Source member generates StructName_get_Source(handle) and StructName_set_Source(handle, value) functions. The C++ implementation handles proper type conversion and memory management: obj->member = *reinterpret_cast<const MemberType*>(value) for setters, and return &(static_cast<StructType*>(handle)->member) for getters. This approach ensures that Rust code can safely access and modify struct data without understanding the underlying C++ memory layout.
from_ptr Implementation and Type Casting Safety: The from_ptr functionality represents one of our most critical safety mechanisms for interface conversion. On the Rust side, from_ptr methods accept raw *mut c_void pointers and create typed wrapper objects, but only after null pointer validation: if ptr.is_null() { None } else { Some(Self { ptr }) }. The real complexity happens in the C++ bridge where we implemented proper UNO_QUERY interface conversion instead of unsafe static casting. When converting between interface types, our generated C++ code performs Reference<TargetInterface> queryResult(sourcePtr->get(), UNO_QUERY) which provides runtime type checking and proper reference count management. This solved a critical bug where static_cast operations were causing type safety violations - the UNO_QUERY approach ensures that interface conversions only succeed when the underlying UNO object actually supports the target interface type.
The from_ptr mechanism also handles the complex case of converting from generic XInterface* pointers to specific interface types. Our code generator creates bridge functions that take the opaque handle, cast it back to Reference<XInterface>*, perform the UNO_QUERY operation to get the specific interface, create a new Reference<TargetInterface> wrapper, and return it as a new opaque handle. This preserves all UNO interface semantics while providing compile-time type safety in Rust.
Name Mangling and Symbol Visibility Management: One of the critical challenges we solved was managing C++ name mangling and symbol visibility across the FFI boundary. C++ compilers perform name mangling to support function overloading and namespaces, but this creates unpredictable symbol names that Rust cannot reliably link to. Our solution uses extern "C" linkage for all bridge functions, which prevents name mangling and ensures that function names remain exactly as declared. For example, XTextDocument_getText always generates the same symbol name regardless of compiler or platform.
However, this created the function name uniqueness problem we mentioned earlier - without C++ namespaces to provide scoping, function names like run_ would conflict across different interfaces. Our opaque pointer approach naturally solved this by generating interface-prefixed names like XMain_run instead of generic run_ functions, eliminating linkage conflicts entirely.
Shared Library Visibility and Dynamic Linking: Our architecture generates a shared library (rust_uno-cpp.so on Linux, .dylib on macOS, .dll on Windows) that contains all the C++ bridge functions and must be properly linked with both LibreOffice and the Rust cdylib. We manage symbol visibility using several strategies: all bridge functions are explicitly marked with C linkage and appropriate visibility attributes to ensure they're exported from the shared library, the generated C++ code includes proper header guards (#pragma once) to prevent multiple definition errors during compilation, and we use LibreOffice's existing build system integration to ensure proper linking order and dependency management.
The Rust side declares all bridge functions as extern "C" imports, which tells the Rust compiler to look for these symbols in linked libraries at runtime. The LibreOffice build system automatically links the rust_uno-cpp library with the main soffice binary, making all bridge functions available to the Rust code when it loads as a cdylib. This creates a three-way linking relationship: LibreOffice loads the Rust cdylib, the Rust code calls functions in the C++ bridge library, and the C++ bridge library calls back into LibreOffice's UNO implementation.
Cross-Platform ABI Compatibility: Our architecture must work across different platforms and compilers while maintaining ABI (Application Binary Interface) compatibility. We achieve this by using only C-compatible types in our function signatures (void pointers, primitive types, and C-style function pointers), avoiding C++ types that might have different memory layouts on different platforms, and using LibreOffice's existing build system and compiler configurations to ensure consistent compilation flags across all components. The opaque pointer approach is particularly valuable here because it hides all platform-specific C++ implementation details behind a simple, portable void pointer interface that works identically on all supported platforms.
Enabling Future Language Bindings: One of the most significant benefits of our architecture is that we've solved the hardest part of the LibreOffice integration problem - converting between C++ UNO's complex object model and a simple C interface. This creates a foundation that makes implementing bindings for other languages dramatically easier.
The challenging work we've completed includes handling C++ vtables and virtual method calls, managing UNO's reference counting system, implementing proper UNO_QUERY interface conversion with runtime type checking, dealing with C++ exceptions and converting them to C-compatible error codes, managing complex inheritance hierarchies and method resolution, handling C++ constructor/destructor semantics safely, and resolving name mangling and symbol visibility issues across shared library boundaries.
What this means for future language bindings is that languages like Zig, Go, Julia, Swift, or any other language with C FFI capabilities can now integrate with LibreOffice by simply calling our generated C functions - they don't need to understand UNO's C++ complexity at all. For example, a Zig binding would only need to declare extern fn XTextDocument_getText(handle: ?*anyopaque) ?*anyopaque and implement wrapper types with appropriate resource management, which is far simpler than trying to interface directly with C++ UNO.
Our C bridge essentially provides a "UNO-as-a-C-library" interface that any language can consume. The hard problems of C++ interoperability, UNO semantics, memory management, and cross-platform compatibility are solved once in our C++ bridge layer. Future language bindings become primarily an exercise in creating idiomatic wrappers around simple C function calls, rather than solving complex FFI challenges.
This approach follows the Unix philosophy of solving a complex problem once and providing a simple, reusable interface. Languages that implement LibreOffice bindings on top of our C bridge will be more reliable, maintainable, and consistent than attempting to interface directly with the C++ UNO API. The community benefits from having one well-tested, comprehensive C++ to C conversion layer rather than multiple language-specific attempts at solving the same fundamental interoperability challenges.
The architecture scales through comprehensive code generation that creates both sides of the bridge simultaneously. Our rustmaker tool analyzes UNO type libraries (offapi.rdb and types.rdb) and generates matching pairs of Rust wrappers and C++ bridge functions that are guaranteed to be compatible because they're created from the same source information.
Generated Components for Each UNO Type:
- Type-safe handle typedef (like
typedef void* XTextDocumentHandle) - Bridge functions with unique names that avoid extern "C" conflicts (like
XTextDocument_getText) - Proper UNO method implementations with exception handling and reference counting
- Corresponding Rust wrappers with automatic memory management and Drop trait implementation
Comprehensive Type Coverage:
- Interfaces with complex inheritance - automatic method inheritance through recursive base interface traversal
- Structs with nested members - proper getter/setter functions including base type inheritance
- Enums with duplicate resolution - using std::set to track discriminant values and eliminate duplicates
- Sequences with dynamic typing - proper array marshaling code for all element types
The coordinated generation ensures that both sides of the bridge are always synchronized and compatible, eliminating the entire category of binding maintenance problems that affect manually written FFI code. The C++ bridge layer handles all the non-POD complexity - constructor calls, destructor management, vtable access, and reference counting - while exposing only simple, POD-compatible function signatures to Rust.
Our architecture preserves UNO's dynamic capabilities while providing Rust's safety guarantees. UNO's interface querying works exactly as expected - when Rust code requests an interface conversion, our bridge generates appropriate UNO_QUERY operations that perform proper runtime type checking and reference counting. The difference is that Rust code receives a properly typed opaque handle rather than a raw pointer, maintaining compile-time type safety.
Memory management works through cooperation between layers. C++ manages UNO reference counting as normal, while Rust's Drop trait ensures that bridge resources are properly released. The opaque handles prevent Rust code from accidentally corrupting C++ objects, while the C++ bridge ensures that UNO lifetime rules are followed correctly.
Despite the three-layer architecture, performance remains excellent because each layer is thin and focused. Rust wrapper calls translate directly to C function calls with minimal overhead. C++ bridge functions are simple adapters that add only exception handling and type casting overhead. The opaque pointer approach actually eliminates the complex type marshaling that other FFI approaches require, often resulting in better performance than more direct but error-prone approaches.
The architecture scales automatically to UNO's full complexity - we've successfully generated working bindings for all UNO types, including interfaces with complex inheritance hierarchies, structs with nested members, enums with duplicate aliases, and sequences with dynamic typing. Each type gets appropriate bridge functions and Rust wrappers without requiring manual intervention or special-case handling.
✅ Complete UNO API Coverage: Successfully generated working Rust bindings for all UNO types in LibreOffice's type system:
- Interface types with complex inheritance hierarchies and dynamic interface querying
- Struct types with nested members and base type inheritance
- Enum types with duplicate alias resolution and safe integer conversion
- Sequence types for dynamic arrays with full type safety
✅ Production Deployment: System successfully integrated and operational:
- Runtime validation through LibreOffice startup testing
- Cross-platform deployment (Linux, Windows, macOS)
- Build system integration via CustomTarget makefiles
- Zero regression in LibreOffice functionality or performance
✅ Developer Experience: Comprehensive tooling and workflow:
- Automated
rustmakercode generation from UNO type libraries - Coordinated C++ and Rust code generation ensuring compatibility
- Intelligent batching system handling large-scale type generation
- Clear development workflow with essential build commands
Novel FFI Architecture: The opaque pointer approach provides a reusable pattern for bridging incompatible memory models between C++ and Rust, solving fundamental problems that existing tools cannot address.
Coordinated Generation Strategy: Simultaneous creation of matching C++ bridge functions and Rust wrappers ensures type safety and eliminates the binding maintenance problems that plague manually written FFI code.
Foundation for Multi-Language Support: Our C++ bridge layer solves UNO integration complexity once, providing a simple C interface that other languages (Go, Zig, Julia, Swift) can easily consume.
Generated File Locations:
- C++ bridge functions:
workdir/CustomTarget/rust_uno/rustmaker/cpp/ - Rust wrapper types:
rust_uno/src/rustmaker/
The LibreOffice Rust UNO Bindings implementation consists of three major patches integrated into LibreOffice core:
1. Rust UNO FFI Architecture Foundation - Patch #186425
- Establishes the core three-layer opaque pointer architecture
- Implements basic Rust UNO integration with LibreOffice runtime
- Provides foundation FFI types and bridge function patterns
2. Rustmaker Code Generator Implementation - Patch #188088
- Adds complete
rustmakercode generation tool to LibreOffice codemaker system - Implements coordinated generation of Rust wrappers and C++ bridge functions
- Handles all UNO type categories with proper inheritance and interface querying
3. Extension-based Integration and Service Creation - Patch #190382
- Provides extension-based UNO integration architecture
- Implements service creation and singleton access patterns
- Demonstrates complete document automation workflow integration
These patches collectively deliver the full production-ready Rust UNO binding system, enabling safe access to LibreOffice's complete UNO API from Rust applications and extensions.
The LibreOffice Rust UNO Bindings project successfully solved one of the most challenging problems in cross-language integration: safely bridging C++'s complex object model with Rust's memory safety guarantees. Through our innovative three-layer opaque pointer architecture, we've created a production-ready system that enables safe, performant access to LibreOffice's complete UNO API from Rust.
What We Achieved:
- Enabled Rust for LibreOffice Development: Rust developers can now create LibreOffice extensions, automation scripts, and document processing applications using safe, familiar language constructs
- Demonstrated Scalable FFI Architecture: Our approach handles complex C++ types automatically, proving the architecture can scale to real-world API complexity
- Created Reusable Foundation: The C++ bridge layer provides a foundation for other languages to build LibreOffice bindings without solving the same complex interoperability challenges
Why This Matters:
- Memory Safety at Scale: Proves that complex C++ APIs can be safely accessed from Rust without compromising performance or functionality
- Cross-Language Integration Pattern: The opaque pointer architecture provides a reusable pattern for bridging incompatible memory management systems
- Open Source Community Impact: Adds Rust as a first-class LibreOffice development language, opening LibreOffice's powerful capabilities to Rust's growing ecosystem
Advanced UNO Features: Future work includes comprehensive UNO exception mapping, event listener support, and performance optimizations for large data operations.
Community Foundation: Our C bridge specifications can be published to enable community development of LibreOffice bindings for additional languages.
The success of this project demonstrates that with careful architectural design, seemingly incompatible systems can be bridged while maintaining both safety and performance. The techniques and patterns developed here benefit not only LibreOffice developers but the broader systems programming community working on complex language interoperability challenges.
By solving this hard problem once and providing a clean, reusable interface, we've enabled Rust development for LibreOffice and created architectural patterns that can benefit developers working with other complex C++ libraries. This embodies the best principles of open source development: solving complex problems collaboratively and making the solutions available for the entire community to build upon.