Introduction
Language mapping from IDL to Rust.
Naming Conventions
Rust uses the following naming conventions:
| Item | Convention |
|---|---|
| Crates | snake_case or kebab-case |
| Modules | snake_case |
| Types | PascalCase |
| Traits | PascalCase |
| Enum variants | PascalCase |
| Functions | snake_case |
| Methods | snake_case |
| General constructors | new or with_more_details |
| Conversion constructors | from_some_other_type |
| Macros | snake_case! |
| Local variables | snake_case |
| Statics | SCREAMING_SNAKE_CASE |
| Constants | SCREAMING_SNAKE_CASE |
| Type parameters | concise PascalCase, usually single uppercase letter: T |
| Lifetimes | short lowercase, usually a single letter: 'a, 'de, 'src |
Most of the above naming conventions are enforced by the compiler. Deviating from the above scheme will result in a compilation warning. For that reason, the generated code should follow the above naming scheme to the extent possible.
IDL to Rust Name Transformation
IDL names are automatically transformed to match Rust conventions. The following transformations are applied:
| IDL Item | Transformation | Example |
|---|---|---|
| Modules | snake_case | MyModule → my_module |
| Structs | PascalCase | my_struct → MyStruct |
| Unions | PascalCase | my_union → MyUnion |
| Enums | PascalCase | my_enum → MyEnum |
| Enumerators | PascalCase | MY_VALUE → MyValue |
| Bitmasks | PascalCase | my_bitmask → MyBitmask |
| Bit flags | snake_case | MY_FLAG → my_flag |
| Typedefs | PascalCase | my_alias → MyAlias |
| Interfaces | PascalCase | my_interface → MyInterface |
| Exceptions | PascalCase | my_exception → MyException |
| Constants | SCREAMING_SNAKE_CASE | myConst → MY_CONST |
| Struct members | snake_case | MyField → my_field |
| Union variants | PascalCase | my_variant → MyVariant |
| Operations | snake_case | DoSomething → do_something |
| Parameters | snake_case | MyParam → my_param |
Suffix Stripping
Common type suffixes are stripped before case conversion to produce cleaner names:
_tsuffix:my_type_t→MyType_esuffix:my_enum_e→MyEnum
Enum Prefix Stripping
Enumerator names that share a common prefix with their enum type have that prefix stripped to avoid redundancy:
// IDL
enum Color {
COLOR_RED,
COLOR_GREEN,
COLOR_BLUE
};
#![allow(unused)]
fn main() {
// Rust - prefix "COLOR_" is stripped
pub enum Color {
Red,
Green,
Blue,
}
}
Protected names
Rust groups keywords into two categories: weak and strict.
Strict keywords
Strict keywords are keywords which can only be used in their correct contexts, which is to say they cannot be used as the names of items, variables, fields and variants, type parameters, macros or crates.
Generated files, modules, types and variables may not use strict keywords.
| Keywords | |||||
|---|---|---|---|---|---|
as | break | const | continue | crate | else |
enum | extern | false | fn | for | if |
impl | in | let | loop | match | mod |
move | mut | pub | ref | return | self |
Self | static | struct | super | trait | true |
type | unsafe | use | where | while | async |
await | dyn | abstract | become | box | do |
final | macro | override | priv | typeof | unsized |
virtual | yield | try |
Strict keywords will be escaped in the generated Rust code by appending an
underscore to the name, e.g. module_.
Weak keywords
Weak keywords have special meaning only in certain contexts. None of the current weak keywords affect the naming of types or variables. Thus, weak keywords are not reserved and may be freely used.
| Keyword | Context |
|---|---|
union | Only a keyword when used in a union declaration. |
'static | Cannot be used as a generic lifetime parameter or loop label. |
For example, the following is perfectly valid Rust code:
#![allow(unused)]
fn main() {
fn union() {
union union<'union> {
union: &'union union<'union>
}
}
}
Anonymous Types
Anonymous union and struct types are not supported. Rust mandates that all types are named, i.e. it does not have a concept of anonymous types beyond tuples. Other interim anonymous types do not need any special handling, as they can be expressed directly in the type system.
Comments
The Rust compiler comes bundled with rustdoc, a tool for generating
documentation from comments in the code.
- Lines starting with
//are implementation comments and are omitted from the generated documentation. - Lines starting with
///are considered documentation comments. - Lines starting with
//!are module-level or crate-level documentation. Such comments may only appear at the beginning of a file or above a module. Using it other places is treated as an error.
Documentation comments attached to IDL definitions are emitted using ///
style comments. Module-level comments are currently not emitted in the
generated code.
IDL comments that are denoted by // should be omitted from the generated code
for the sake of brevity.
Mapping for Modules
Files
Files implicitly become modules in Rust. Since this is not the case for IDL, files and how they are organized does not affect the generated Rust code.
Includes
Files included by the processed IDL file will be publicly imported in the generated Rust module.
// IDL
#include "LocalInclude.idl"
#include <SystemInclude.idl>
#![allow(unused)]
fn main() {
// Rust
pub mod local_include;
pub mod system_include;
}
Note the different naming scheme: the file names were converted to
snake_case.
Modules
To correctly map the scope of each IDL type, IDL modules are mapped to a hierarchy of Rust files. Given a set of IDL files, the compiler should generate the following:
- A
lib.rsfile suitable for being included directly from Rust crates. It should publicly include all other modules, and it should contain all global top-level definitions. - For each IDL module, the compiler should generate a single file with the same name as the module. Type definitions that appear directly under this module should be placed in the same file.
- If a module contains nested modules, the compiler should create a directory with the parent module’s name, and put the child module inside said directory. This process should repeat for modules nested inside the child module.
// IDL
struct MyGlobalStruct {};
module my_mod {
struct MyModStruct {};
module my_nested_module {
module foo {
struct FooStruct {};
};
};
};
The above modules will produce the following file hierarchy:
.
├── lib.rs
├── my_mod
│ ├── my_nested_module
│ │ └── foo.rs
│ └── my_nested_module.rs
└── my_mod.rs
MyGlobalStruct will be placed inside lib.rs; MyModStruct will be placed
inside my_mod.rs, and all modules nested inside my_mod will be put in the
my_mod directory.
Mapping for Primitives
The basic data types have the mappings shown below.
| IDL | Rust | Default value |
|---|---|---|
boolean | bool | false |
octet | u8 | 0 |
int8 | i8 | 0 |
uint8 | u8 | 0 |
int16 | i16 | 0 |
uint16 | u16 | 0 |
int32 | i32 | 0 |
uint32 | u32 | 0 |
int64 | i64 | 0 |
uint64 | u64 | 0 |
short | i16 | 0 |
long | i32 | 0 |
float | f32 | 0_f32 |
double | f64 | 0_f64 |
long double | f64 | 0_f64 |
char | char | '\x00' |
wchar | char | '\x00' |
char8 | char | '\x00' |
char16 | char | '\x00' |
All of the above types are primitive, built-in types. Rust chars are
guaranteed to be valid UTF-8 characters.
Limitations
Long Double
The IDL long double type is mapped to f64 in Rust, which is the same as
double. Rust does not have not yet have native support for 80-bit or 128-bit
floating point types in the standard library. This means that long double
values may lose precision when used in Rust code. If higher precision is
required, an implementation may provide a custom implementation of an
f128-equivalent type.
There is an open RFC for
adding support for an f128 type, but it is not yet stabilized.
Constants
IDL constants are mapped directly to Rust constants. Rust supports octal,
hexadecimal and decimal integer literals. String constants will be mapped to
string slices instead of String.
// IDL
const int32 MY_DECIMAL = 123;
const int32 MY_HEX = 0xFFF;
const int32 MY_OCTAL = 0655;
const string MY_STRING = "my string";
const octet MY_ARRAY[4] = {0, 1, 2, 3};
#![allow(unused)]
fn main() {
// Rust
pub const MY_DECIMAL: i32 = 123;
pub const MY_HEX: i32 = 0xFFF;
pub const MY_OCTAL: i32 = 0o655;
pub const MY_STRING: &str = "my_string";
pub const MY_ARRAY: [u8; 4] = [0, 1, 2, 3];
}
Complex types
Constants can only be used for types that can be constructed at compile-time,
i.e. types whose constructor is const. For non-const types, statics must be
used instead.
Rust does not permit code execution before main, i.e. dynamic initialization
of statics must happen afterwards. Constants that require dynamic
initialization – i.e. vectors and complex IDL types – must thus be explicitly
initialized the first time the member is accessed. To avoid data races, the
variable can be initialized using std::sync::LazyLock, a synchronization
primitive which lazily initializes the value on first access. This is only done
for types which contain non-const-constructible members.
// IDL
struct MyTrivialStruct {
int32 x;
int32 y;
int32 z;
};
struct MyComplexStruct {
string my_str;
};
const MyTrivialStruct MY_TRIVIAL_CONST = { ... };
const MyComplexStruct MY_COMPLEX_CONST = { ... };
#![allow(unused)]
fn main() {
// Rust
pub const MY_TRIVIAL_CONST: MyTrivialStruct = MyTrivialStruct { ... };
pub static MY_COMPLEX_CONST: ::std::sync::LazyLock<MyComplexStruct> =
::std::sync::LazyLock::new(|| MyComplexStruct { ... });
}
Typedefs
Typedefs of types correspond to type aliases in Rust. Type aliases may only be used for types and not traits. If a typedef defines an alias of an interface, it will instead be publicly re-exported under the specified name.
// IDL
typedef long T;
typedef sequence<long> S1;
typedef S1 S2;
interface A1;
typedef A1 A2;
#![allow(unused)]
fn main() {
pub type T = i32;
pub type S1 = Vec<i32>;
pub type S2 = S1;
// ...definitions for A1...
pub use A1 as A2;
}
Mapping for Enums
Enums are mapped to trivial enums in Rust. Each generated enum has a const
constructor, and should implement the Default trait.
For convenience, each enum also implements std::str::FromStr and
std::fmt::Display, for respectively converting the enum from and to its
string representation. The Display implementation automatically provides
ToString.
Enums are always trivial (they consist only of discriminant values) and always
have total order, so they derive Copy, Eq, Ord, and Hash in addition to
the base traits. See Derived Traits for details on
trait derivation rules.
Representation
The #[repr] attribute specifies the underlying integer type used to store the
enum discriminant. This is determined by the @bit_bound annotation in IDL:
@bit_bound | Rust #[repr] |
|---|---|
| 8 | u8 |
| 16 | u16 |
| 32 (default) | u32 |
| 64 | u64 |
If @bit_bound is not specified, the default is 32 bits (u32).
Example
@bit_bound(32)
enum MyEnum {
ONE,
TWO,
@value(9) NINE
};
#![allow(unused)]
fn main() {
#[repr(u32)]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum MyEnum {
One,
Two,
Nine = 9,
}
impl MyEnum {
#[must_use]
pub const fn new() -> Self {
MyEnum::One
}
}
impl FromStr for MyEnum {
// ...
}
impl Display for MyEnum {
// ...
}
impl Default for MyEnum {
fn default() -> Self {
Self::new()
}
}
}
Mapping for String Types
Both bounded and unbounded IDL string types are mapped to String from the
standard library.
// IDL
typedef string MyString;
typedef wstring MyWideString;
#![allow(unused)]
fn main() {
// Rust
pub type MyString = String;
pub type MyWideString = String;
}
Default Value
The default value for a string is an empty string:
#![allow(unused)]
fn main() {
String::new()
}
Bounded Strings
IDL allows specifying a maximum length for strings. This bound is not enforced
at the type level in Rust, but is checked during serialization. An
implementation may choose to provide a custom BoundedString<N> type for such
purposes.
// IDL
typedef string<100> BoundedString;
#![allow(unused)]
fn main() {
// Rust - bound is not reflected in the type
pub type BoundedString = String;
}
Wide String Types
The char type in Rust is always a UTF-8 character. The String type is only
a collection of chars; for that reason, both IDL strings and wide strings are
mapped to the String type in Rust.
Mapping for Sequence Types
Bounded and unbounded IDL sequence types are mapped to Vec in Rust.
// IDL
typedef sequence<T> V1;
typedef sequence<T, 2> V2;
typedef sequence<V1> V3;
#![allow(unused)]
fn main() {
// Rust
type V1 = Vec<T>;
type V2 = Vec<T>;
type V3 = Vec<V1>;
}
Default Value
The default value for a sequence is an empty vector:
#![allow(unused)]
fn main() {
Vec::new()
}
Bounded Sequences
IDL allows specifying a maximum size for sequences. This bound is not enforced
at the type level in Rust, but is checked during serialization. An
implementation may choose to provide a custom BoundedVec<T, N> type for such
purposes.
// IDL
typedef sequence<int32, 100> BoundedSeq;
#![allow(unused)]
fn main() {
// Rust - bound is not reflected in the type
pub type BoundedSeq = Vec<i32>;
}
Mapping for Array Types
IDL arrays are mapped to Rust arrays, which allows the definition of statically initialized data.
// IDL
typedef float F[10];
typedef string V[10];
typedef string M[1][2][3];
#![allow(unused)]
fn main() {
// Rust
pub type F = [f32; 10];
pub type V = [String; 10];
pub type M = [[[String; 3]; 2]; 1];
}
Default Value
The default value for an array is an array where each element is initialized to
its type’s default value. For example, an array of 10 integers defaults to
[0; 10], and an array of strings defaults to an array of empty strings.
Mapping for Map Types
IDL map types are mapped to BTreeMap from the Rust standard library. A
BTreeMap is used rather than HashMap because it provides deterministic
ordering, which is important for serialization consistency.
// IDL
typedef map<string, int32> StringToInt;
typedef map<int32, sequence<string>> IntToStrings;
#![allow(unused)]
fn main() {
// Rust
pub type StringToInt = ::std::collections::BTreeMap<String, i32>;
pub type IntToStrings = ::std::collections::BTreeMap<i32, Vec<String>>;
}
Default Value
The default value for a map is an empty map:
#![allow(unused)]
fn main() {
::std::collections::BTreeMap::new()
}
Bounded Maps
IDL allows specifying a maximum size for maps. This bound is not enforced at
the type level in Rust, but is checked during serialization. An implementation
may choose to provide a custom BoundedMap<K, V, N> type for such purposes.
// IDL
typedef map<string, int32, 100> BoundedMap;
#![allow(unused)]
fn main() {
// Rust - bound is not reflected in the type
pub type BoundedMap = ::std::collections::BTreeMap<String, i32>;
}
Key Type Requirements
Map keys must be types that implement Ord in Rust (required by BTreeMap).
This means the key type must have total order. Floating-point types (f32,
f64) cannot be used as map keys because they do not implement Ord.
Constant Maps
Map constants are initialized using the BTreeMap::from constructor with an
array of key-value tuples:
// IDL
const map<string, int32> MY_MAP = {
{"one", 1},
{"two", 2},
{"three", 3}
};
#![allow(unused)]
fn main() {
// Rust
pub static MY_MAP: ::std::sync::LazyLock<::std::collections::BTreeMap<String, i32>> =
::std::sync::LazyLock::new(|| {
::std::collections::BTreeMap::from([
("one".into(), 1),
("two".into(), 2),
("three".into(), 3),
])
});
}
Note that map constants containing non-trivial types (like String keys) use
LazyLock for lazy initialization. See Constants
for details.
Mapping for Structured Types
Generated structs have public members. This design choice is intentional for
data-carrying types:
- Simplicity: Avoids the need for getter/setter methods in three variants (by-value, by-reference, by-mutable-reference) for each field.
- Ergonomics: Enables direct field access and struct literal syntax.
- Consistency: Matches Rust conventions for plain data structures.
- Flexibility: Allows partial initialization with
..Default::default(). - Partial moves: Enables moving individual fields out of a struct without consuming the entire value.
#![allow(unused)]
fn main() {
// Rust
let _ = MyStruct {
my_int: 5,
my_str: "my string".into(),
my_vec: vec![1, 2, 3],
};
}
In addition, this plays nicely alongside the Default trait: users can specify
some values, whilst defaulting the rest. For example:
#![allow(unused)]
fn main() {
let _ = MyStruct {
my_int: 5,
..Default::default()
};
}
The variables that were not named in such a construct will be filled in with
the values acquired from the default function, i.e. the default values
specified in IDL.
Derived Traits
All generated types (struct, enum, union, bitmask, exception)
automatically derive a set of Rust traits. Some traits are always derived, while
others are conditionally derived based on recursive type analysis.
Always Derived
The following traits are always derived for all generated types:
| Trait | Purpose |
|---|---|
Clone | Enables deep copying of values |
Debug | Enables formatting with {:?} |
PartialEq | Enables equality comparison (==, !=) |
PartialOrd | Enables ordering comparison (<, >, etc) |
Conditionally Derived
The compiler performs a recursive analysis of each type to determine whether additional traits can be derived. This analysis examines all members, parent types, and nested types to make the determination.
Triviality Analysis (Copy)
A type is considered trivial if it consists only of:
- Primitive types (
bool, integers,char) - Arrays of trivial types
- Other trivial user-defined types
A type is not trivial if it contains:
Stringor bounded/unbounded string typesVec(sequences)BTreeMap(maps)- Recursive/circular types
Any,Fixed, or interface types
Trivial types derive Copy, which enables bitwise copying without explicit
.clone() calls.
Total Order Analysis (Eq, Ord, Hash)
A type has total order if all its members can form a well-ordered set. This requires that all member types satisfy:
- The type is not a floating-point type (
f32,f64) - All nested/member types also have total order
Floating-point types break total order because NaN != NaN, which violates the
reflexivity property required by Eq.
Types with total order derive:
| Trait | Purpose |
|---|---|
Eq | Guarantees reflexive equality (x == x) |
Ord | Enables total ordering comparisons |
Hash | Enables use as keys in hash-based collections |
Default Trait
All generated types implement the Default trait. The implementation delegates
to the new() constructor, which initializes all members to their default
values (either from @default annotations or type-specific defaults).
#![allow(unused)]
fn main() {
impl Default for MyStruct {
fn default() -> Self {
Self::new()
}
}
}
Summary Table
| Trait | Condition |
|---|---|
Copy | Type is trivial (no heap allocation, not recursive) |
Clone | Always |
Debug | Always |
Default | Always (via new() constructor) |
Eq | Type has total order (no floating-point members) |
PartialEq | Always |
Ord | Type has total order (no floating-point members) |
PartialOrd | Always |
Hash | Type has total order (no floating-point members) |
Example
// IDL - trivial type with total order
struct Point {
int32 x;
int32 y;
};
// IDL - non-trivial type with total order
struct Person {
string name;
int32 age;
};
// IDL - non-trivial type without total order
struct Measurement {
float value;
string unit;
};
#![allow(unused)]
fn main() {
// Trivial + total order: derives Copy, Eq, Ord, Hash
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Point {
pub x: i32,
pub y: i32,
}
// Non-trivial + total order: derives Eq, Ord, Hash but not Copy
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Person {
pub name: String,
pub age: i32,
}
// Non-trivial + no total order: only base traits
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct Measurement {
pub value: f32,
pub unit: String,
}
}
Example
// IDL
struct MyStruct {
unsigned long my_int;
string my_str;
sequence<int32> my_vec;
};
#![allow(unused)]
fn main() {
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct MyStruct {
pub my_int: u32,
pub my_str: String,
pub my_vec: Vec<i32>,
}
impl MyStruct {
pub fn new() -> Self {
Self {
my_int: 0,
my_str: String::new(),
my_vec: Vec::new(),
}
}
}
impl Default for MyStruct {
fn default() -> Self {
Self::new()
}
}
}
Nested Types
Nested or scoped types will be placed inside a module with the name of the parent scope.
// IDL
struct MyA {
struct MyB {
struct MyC {};
};
};
#![allow(unused)]
fn main() {
// Rust
struct MyA {};
pub mod my_a {
struct MyB {};
pub mod my_b {
struct MyC {};
};
};
}
Inheritance
Rust does not support inheritance. Following the IDL specification, fields from base types will be copied into the derived type.
// IDL
struct Parent {
int64 parent_field;
};
struct Derived : Parent {
int64 derived_field;
};
#![allow(unused)]
fn main() {
// Rust
pub struct Parent {
pub parent_field: i64,
}
pub struct Derived {
pub parent_field: i64,
pub derived_field: i64,
}
}
Mapping for Union Types
Unions in IDL are mapped to Rust enums where each case label maps to an enum
variant. Enums are easier to use and are better suited for pattern matching.
The value of the discriminator is inferred during serialization by looking at
the populated field.
In the event that a union has multiple switch cases mapped to a single value, the name of the case label is appended to the end of the member value. If not all cases are covered by the union, an implicit default case will be inserted.
Each union has a disc function which returns the corresponding enum value of
the would-be discriminator, deduced from the populated variant. Similarly, each
union implements the From<T> trait, where T is the type of the discriminant.
The derived traits for unions depend on their variant types. Unions containing
heap-allocated types (like String) will not derive Copy. Unions containing
floating-point types will not derive Eq, Ord, or Hash. See
Derived Traits for the complete derivation rules.
// IDL
enum MyEnum {
ONE,
TWO,
THREE,
FOUR
};
union MyUnion switch(MyEnum) {
case ONE:
string my_string;
case TWO:
case THREE:
int32 my_int;
default:
string default_value;
};
#![allow(unused)]
fn main() {
// Rust
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum MyEnum {
One,
Two,
Three,
Four,
}
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub enum MyUnion {
MyString(String),
MyIntTwo(i32),
MyIntThree(i32),
DefaultValue(String),
}
impl MyUnion {
pub fn new() -> Self {
Self::MyString(String::new())
}
pub const fn disc(&self) -> MyEnum {
match self {
Self::MyString(_) => MyEnum::One,
Self::MyIntTwo(_) => MyEnum::Two,
Self::MyIntThree(_) => MyEnum::Three,
Self::DefaultValue(_) => MyEnum::Four,
}
}
}
impl From<MyEnum> for MyUnion {
fn from(disc: MyEnum) -> Self {
// maps the discriminant to the correct variant
}
}
impl Default for MyUnion {
fn default() -> Self {
Self::new()
}
}
}
Bitmask Types
Bitmasks are mapped to newtype structs in Rust, where each value is an
associated constant of the newtype. This effectively scopes the value of each
constant into the bitmask type, and prevents doing bitwise operations with
different types. The underlying integer can be accessed through the bits
function.
The implementation uses a bitmask! macro to generate the type and all
required trait implementations. The generated API is equivalent to the expanded
form shown below.
Bitmasks are always trivial (they wrap a primitive integer) and have total
order, so they derive Copy, Debug, Eq, Ord, and Hash. See
Derived Traits for the general trait derivation
rules.
The #[repr(transparent)] attribute guarantees that the newtype has the same
memory layout as the underlying integer type. This ensures ABI compatibility
when passing bitmasks across FFI boundaries and allows safe transmutation
between the bitmask and its underlying type.
// IDL
@bit_bound(32)
bitmask MyBitmask {
A,
B,
@position(5) C
};
#![allow(unused)]
fn main() {
// Rust
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct MyBitmask(u32);
impl MyBitmask {
pub const A: Self = Self(1 << 0);
pub const B: Self = Self(1 << 1);
pub const C: Self = Self(1 << 5);
#[inline]
pub const fn nil() -> Self {
Self(0)
}
#[inline]
pub const fn all() -> Self {
Self(Self::A.0 | Self::B.0 | Self::C.0)
}
#[inline]
pub const fn bits(&self) -> u32 {
self.0
}
#[inline]
pub const fn is_empty(&self) -> bool {
self.0 == 0
}
#[inline]
pub const fn contains(&self, rhs: Self) -> bool {
(self.0 & rhs.0) == rhs.0
}
#[inline]
pub fn clear(&mut self) {
self.0 = 0
}
}
impl ::std::ops::BitOr for MyBitmask {
type Output = Self;
#[inline]
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl ::std::ops::BitOrAssign for MyBitmask {
#[inline]
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl ::std::ops::BitXor for MyBitmask {
type Output = Self;
#[inline]
fn bitxor(self, rhs: Self) -> Self::Output {
Self(self.0 ^ rhs.0)
}
}
impl ::std::ops::BitXorAssign for MyBitmask {
#[inline]
fn bitxor_assign(&mut self, rhs: Self) {
self.0 ^= rhs.0
}
}
impl ::std::ops::BitAnd for MyBitmask {
type Output = Self;
#[inline]
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl ::std::ops::BitAndAssign for MyBitmask {
#[inline]
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0
}
}
impl ::std::ops::Not for MyBitmask {
type Output = Self;
#[inline]
fn not(self) -> Self::Output {
Self(!self.0)
}
}
}
Mapping for Exception Types
Rust does not support exceptions. It primarily relies on monads to convey results of operations.
Each exception defined in IDL will be mapped to a struct, as described
before. In addition, each exception type should have a typedef for
std::result::Result<T, E> where E is the exception type.
All exceptions implement the Error and Display traits. The derived traits
follow the same rules as structs, based on member type analysis. See
Derived Traits for details.
// IDL
exception MyException {
string what;
};
#![allow(unused)]
fn main() {
#[derive(Clone, Debug, PartialEq, PartialOrd)]
pub struct MyException {
pub what: String,
}
impl MyException {
pub fn new() -> Self {
Self {
what: String::new(),
}
}
}
impl Default for MyException {
fn default() -> Self {
Self::new()
}
}
pub type MyExceptionResult<T> = Result<T, MyException>;
impl std::fmt::Display for MyException {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "MyException")
}
}
impl std::error::Error for MyException {}
}
Mapping for Interfaces
Interfaces in IDL map to traits in Rust.
Parameter Passing
The parameter direction (in, inout, out) determines how parameters are
passed in the generated Rust code.
in Parameters
For in parameters, the passing convention depends on the type:
| Type Category | Rust Passing Convention | Example |
|---|---|---|
Primitives (bool, integers, char, f32, f64) | By value | value: i32 |
| Enums | By value | value: MyEnum |
| Bitmasks | By value | value: MyBitmask |
| Strings | Immutable slice reference | value: &str |
| Sequences | Immutable slice reference | value: &[T] |
| Structs, Unions, Exceptions | Immutable reference | value: &MyStruct |
| Interfaces | Boxed trait object | value: Box<dyn MyInterface> |
inout and out Parameters
Both inout and out parameters are passed as mutable references:
#![allow(unused)]
fn main() {
fn operation(&mut self, value: &mut i32);
}
Self Parameter
The self parameter depends on the function’s annotations:
| Annotation | Self Parameter |
|---|---|
| (none) | &mut self |
@const | &self |
@static | (no self parameter) |
Static functions require a where Self: Sized bound since they cannot be
called through trait objects.
Exception Handling
Exceptions in IDL are mapped to Rust’s Result type:
| Exception Count | Return Type |
|---|---|
| 0 | T (or () for void) |
| 1 | ExceptionNameResult<T> (type alias for Result<T, ExceptionName>) |
| 2+ | Result<T, Box<dyn Error>> |
Nested Types
Type definitions or aliases defined inside IDL interfaces will be defined in the outer scope, prefixed with the name of the interface.
Example
// IDL
interface MyInterface {
boolean negate(boolean value);
void increment(inout long value);
@static
float square_root(in float value);
@const
void print(Nested value) raises (MyException);
int32 throws_multiple() raises(MyException, MyOtherException);
struct Nested {};
};
#![allow(unused)]
fn main() {
// Rust
pub trait MyInterface {
fn negate(&mut self, value: bool) -> bool;
fn increment(&mut self, value: &mut i32);
fn square_root(value: f32) -> f32
where
Self: Sized;
fn print(&self, value: &Nested) -> MyExceptionResult<()>;
fn throws_multiple(&mut self) -> Result<i32, Box<dyn Error>>;
}
pub struct MyInterfaceNested {}
}
Inheritance
Derived interfaces become additional trait bounds in Rust.
// IDL
interface CordA {};
interface CordB {};
interface CordC {};
interface Vector3 : CordA, CordB, CordC {};
#![allow(unused)]
fn main() {
// Rust
trait CordA {}
trait CordB {}
trait CordC {}
trait Vector3 : CordA + CordB + CordC {}
}
Note that the above means that any type that implements the Vector3 trait
must manually implement CordA, CordB, and CordC.
Annotations
optional
The @optional annotation will cause member fields to be wrapped in an
Option<T> type.
default and default_literal
The @default and @default_literal annotations affect the values generated
in the new() function.
external
The @external annotation will cause the value to be wrapped in a Box<T>.
This indicates that the value is allocated on the heap and owned through a
pointer indirection.
const and static
Function prototypes may be annotated with @const or @static.
derive
The @derive annotation adds custom derive macros to generated types.
Rust offers access to the syntax tree during compilation through derive macros.
In some cases, it may be desirable by the user to expand upon the generated
types through such macros. To allow for this, the @derive annotation adds
user-specified derive macros to the attached type.
For example:
@derive("MyDerive")
struct MyStruct {};
Results in the following Rust code:
#![allow(unused)]
fn main() {
#[derive(..., MyDerive)]
struct MyStruct {};
}
Rust does not require the relevant symbol to be explicitly imported. Thus, the user will have to be responsible for providing a qualified name to the macro.