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_voidor 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 CNVGetScalarDataValueThe 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.
Links #
Rust Macros vs Functions: What Java and Python Developers Should Know.