Rust - Macro vs Function
A practical look at when to use macros and when to stick with functions in Rust, using a real FFI example (void_ptr) to show why macros don’t magically make your code faster — and why small helper functions often compile to identical machine code.
Let’s say I have an FFI call like this:
unsafe extern "C" {
pub fn CNVGetScalarDataValue(
data: CNVData,
type_: CNVDataType,
value: *mut ::std::os::raw::c_void,
) -> ::std::os::raw::c_int;
}
This comes from CVI Network Variables:
int CVIFUNC CNVGetScalarDataValue(CNVData data, CNVDataType type, void * value);
The function supports different scalar types, so in Rust it might be called like this:
//...
let mut value: c_int = 0;
let result = CNVGetScalarDataValue(
data,
CNVDataType_CNVInt32,
&mut value as *mut c_int as *mut c_void,
);
println!("{}", value);
It works, but the expression &mut value as *mut c_int as *mut c_void isn’t exactly pleasant to look at. Making the cast more idiomatic
If value is already a c_int, you can shorten the casts:
&mut value as *mut _ as *mut c_void
or even
value.as_mut() as *mut _ as *mut c_void
📌 Why this is the minimum
CNVGetScalarDataValueexpects a*mut c_void- Rust references (
&mut T) must be cast to raw pointers (*mut T) - Then cast again to
*mut c_void
So you always need two casts, but you can shorten the types.
⭐ Even cleaner: helper function
If you call this often, hide the ugliness:
fn void_ptr<T>(v: &mut T) -> *mut c_void {
v as *mut T as *mut c_void
}
let result = CNVGetScalarDataValue(
data,
CNVDataType_CNVInt32,
void_ptr(&mut value),
);
This is the most readable approach, but our goal is simply to avoid writing the casts manually, this is the closest macro equivalent:
⭐ Slightly nicer: macro that takes value and borrows it If you want the macro to always take a mutable reference:
macro_rules! void_ptr {
($v:expr) => {
&mut $v as *mut _ as *mut c_void
};
}
Then usage:
let result = CNVGetScalarDataValue(
data,
CNVDataType_CNVInt32,
void_ptr!(value),
);
Are macros faster than functions? Short answer: no. Rust aggressively inlines small generic functions, so the helper function and the macro produce identical machine code.
Here’s the relevant assembly from both versions:
.text:0000000140001266 mov [rbp+60h+var_4C], 0
.text:000000014000126D mov rcx, qword ptr [rbp+60h+var_78]
.text:0000000140001271 mov edx, 3
.text:0000000140001276 mov r8, rdi
.text:0000000140001279 call CNVGetScalarDataValue
.text:000000014000127E mov r12d, eax
.text:0000000140001281 mov [rbp+60h+var_50], 0
.text:0000000140001288 mov rcx, qword ptr [rbp+60h+var_78]
.text:000000014000128C mov edx, 3
.text:0000000140001291 mov r8, rbx
.text:0000000140001294 call CNVGetScalarDataValue
The pointer argument ends up in r8 in both cases, and there is no difference at all between the macro and the function.
Conclusion
Macros are great for syntactic convenience, but they don’t make your code faster. For FFI pointer casts, a small helper function is usually the cleanest and safest option — and thanks to Rust’s optimizer, it compiles down to the same machine code as a macro.