Preventing absence of @alignCast() on function pointer when going from x86 to ARM

Today I ran into a little problem when I compiled my code for ARM:

const fn_ptr: *const fn (i32) void = @ptrCast(opaque_ptr);

The above works in x86 since alignment of function is 1 for that architecture. On ARM the alignment is 4, on the other hand, so the @alignCast() is required.

Correcting the problem is easy enough. I can imagine though, how this issue can lead to serious frustration for ARM developers trying to use third-party modules. Forcing the use of @alignCast() in this situation on x86 too would benefit cross-platform compatibility. Alternatively, maybe there should be a anyfunction type that allows us to easily specific an opaque pointer with the correct alignment.

I wonder whether you could overalign the function pointer types to 4 always, to force your code to use @alignCast?

I mean I would expect most functions to have an actual alignment that is higher than 1, but I guess there is nothing that guarantees this on x86?

const std = @import("std");

fn print(number: i32) void {
    std.debug.print("number: {d}\n", .{number});
}

pub fn main() !void {
    const opaque_ptr: *const anyopaque = @ptrCast(&print);
    const fn_ptr: *align(4) const fn (i32) void = @alignCast(@ptrCast(opaque_ptr));
    fn_ptr(31);
}

So I think this code is problematic, because the functions could be unaligned, can we specify functions with a specific alignment?

Yes we can Language Reference - Alignment:

You can specify alignment on variables and functions. If you do this, then pointers to them get the specified alignment:

fn derp() align(@sizeOf(usize) * 2) i32 {
    return 1234;
}
fn noop1() align(1) void {}
fn noop4() align(4) void {}

So I think you could define your functions with alignment four and then the function pointer types with alignment four too, but it might be a bit annoying having to specify that alignment everywhere.

x86 uses variable-length encoding, so a function can start at any byte offset. Going the other direction though might actually more make sense. While ARM instructions are 32-bit, we don’t really gain anything from enforcing that fact. Say you’re dynamically generating a function (in newly allocated executable memory). While encoding the instructions you’d inevitably be working with 32-bit packed structs. You have to really go out of your way to generate a sequence of bytes that would represent a valid function but for its misaligned address.

Forcing an alignment of 1 on all functions irrespective of architecture is actually a better solution.

I think it depends on what you mean with better, I think it may be the more convenient solution, but using align(1) everywhere effectively would turn of alignment checks and then the code just requires to be used with actual alignments that work on the target platform. Basically telling “trust me bro” to the compiler and then possibly not making sure of the right alignment anywhere.

This topic is named preventing absence of @alignCast, if everything is declared with align(1) then you never enforce an @alignCast going from *anyopaque to a function pointer, so it seems like the opposite of what the topic was about.

It seems similar to using []const u8 to refer to strings where you know that they happen to be zero terminated and then passing the .ptr to C functions, it works, but it means that you are removing guard rails and some day it may cause some foot-shooting to you or somebody else.

Still there may be situations where removing guard rails like this makes sense, but it probably would be nice if it was only done in “restricted areas”, where you have alternative measures that somehow bring back some of the lost checking through alternative tools / asserts / tests / build-steps / linters etc.

Using align(4) everywhere may be very inconvenient but it would ensure that it always works because it would prevent absence of @alignCast when going from *anyopaque to function pointer.

When something is align(4) it also can be used as align(1) because higher alignments are compatible with lower alignments, but going from lower to higher alignment always requires a check (if there isn’t some code that ensures that the higher alignment is the actual alignment).

I think over-aligning the functions on x86 to align(4) would be the solution that keeps the type system checks for alignment enabled and making it compatible with ARM, setting it to align(1) would disable alignment checks and rely on “lets hope every part just happens to do the right thing for the architecture we are on”, basically asking for align(1), but hoping to always get align(4) where it is needed.

I also think that the actual solution may be none of the two.

If we want code to run on both x86 and ARM the best thing to do may be to actually test the code on both platforms, through those tests we would discover cases where an @alignCast is missing, without forcing any non-default alignments.

If you are in control of the code that defines the type of opaque_ptr, I would suggest defining it as

const OpaqueFnPtr = *align(@alignOf(fn () void)) const anyopaque;

which should let you drop the @alignCast for all targets.


Speaking broadly, I think this problem is an extension of general problem that there’s no way to statically ensure that code will compile for all supported targets and compile time-known configurations without actually compiling the code. Even if this specific issue with function pointers was addressed, you will still have situations like

const T = if (@import("builtin").os.tag == .windows) u8 else u32;

where @ptrCast without @alignCast might succeed for some targets but not others.

However, for opaque/anyopaque pointers specifically, perhaps it would be useful if the compiler considered them to have an unknown alignment by default (instead of defaulting to 1) and thus always require an explicit @alignCast when casting even if the target type has an alignment of 1. This wouldn’t fix all casts, but it would take care of the most common cases.

2 Likes

While we can force the compiler to place x86 functions on align(4) addresses, we have no control over those stored in external libraries. Perfectly valid function pointers would end up getting flagged as misaligned.

Removing alignment check on function pointer is the simplest solution. It just isn’t necessary. 99.99% of the time function pointers are going be pointing at functions generated by the compiler. They’re going to be correctly aligned.

And even in the very improbably scenario where you encounter a function pointer whose only fault is that is misaligned, you’d get the exact same outcome with or without the check: your program will crash. The only difference is that you’d get a panic message in one case and not the other. This is difference from data pointers, where misaligned read/write operations would still succeed but are less performant.

1 Like