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

  • CNVGetScalarDataValue expects 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.