No #[no_mangle]

I’m very happy to see improved FFI in the 2017 goals.

In that area one thing that bugs me to no end is that pub extern "C" fn is not sufficient for C interoperability*.

*

(I know I could probably find Rust’s internal hashed name somehow and use that in C, but that just makes Rust/C interoperability even less usable).

Rust’s unique strength is in ability to create a library usable directly from C/C++ (and other languages via FFI), but in such case declarations of all API functions require #[no_mangle].

I’m fully aware that calling convention and name mangling are orthogonal concepts in theory. However, in Rust they aren’t orthogonal already, because function declarations it extern "C" {} imply mangling automagically (which is very handy).

In Rust pub extern "C" fn ends up creating an exotic mixed combination, instead of satisfying the most common use case of compatibility with C.

Inconsistent mangling defaults for extern "C" also means that this:

pub extern "C" fn foo() {};

won’t link with this:

extern "C" { fn foo(); }

To me that’s a language wart: similar syntax has different meaning in different contexts, and the shortest and most obvious syntax (especially for users familiar with C++) does not seem to work and causes a link error (and link errors aren’t nearly as nice as other errors in Rust).

To me adding #[no_mangle] to C functions is as practical as having to add #[no_ternary] to usize.

I’m proposing:

  • No change for extern "C" that isn’t “public” (this form is commonly used for callbacks).

  • Add #[mangle] to preserve orthogonality of calling convention and mangling, so that it’s still possible to have Rust-specific symbol name with foreign calling convention.

  • Have a transition period where pub extern "C" without #[mangle] causes a warning. It’d logically complement the existing warning for private extern "C" having #[no_mangle].

  • Make “public” pub extern "C" fn default to no name mangling (which would make it consistent with extern "C" {} in Rust and C++).

I’m not sure about exact definition of “public”. Does pub extern in a private module count as public? pub(restrict)? For interoperability with C this is only important for symbols exported from the crate.

While it’s theoretically a breaking change, I think it’s very unlikely to break anything in practice. Admittedly, if it breaks, it would be due to a duplicate symbol, which is an ugly error to have.

In case breaking change is unacceptable, then a minimal proposal is:

  • Add #[mangle] (or maybe #[mangle(Rust)]/#[mangle(C)]). It will allow linters to flag uses of pub extern "C" that don’t also specify what name mangling is required. This way a forgotten #[no_mangle] can be found other than by a linker error.
3 Likes

If these lose their implicit mangling, then they may collide even within the crate, no? Like foo:callback and bar::callback would both generate unmangled callback symbols -- boom?

4 Likes

I’m only proposing to change pub extern "C" fn, not private extern "C" fn, as the latter is not used for linking with C.

1 Like

Yeah I once proposed that “extern” was a bad keyword for ABIs. But the ship has sailed and I think orthogonality is more important than this small pedegaugy burden.

I am sympathetic to this. To be perfectly honest, I hadn’t really considered that calling Rust code directly from C was so common: I expected most Rust code to provide pointers into C APIs that they use to callback. But I guess I can see that one might commonly write some C shims that call into Rust code or something (or move some routine previously written in C into Rust).

It seems mildly surprising to me that pub or not pub would affect name-mangling, though I imagine it might line up well in practice.

It's indeed very rare for Rust programs and libraries that have only Rust API. However, I'm using Rust to write a library for use in C and other languages. In a library with C-compatible interface every public function has to have #[no_mangle].

I've scanned 8000 files I have in ~/.cargo/registry as well as Rust's own repo (excluding test cases):

  • Inherited #[mangle] exception_cleanup src/libpanic_unwind/lib.rs

  • Inherited #[mangle] locking_function openssl-sys-0.7.1/src/lib.rs

  • Inherited #[mangle] thread_id openssl-sys-0.7.1/src/lib.rs

  • Inherited #[mangle] shutdown log-0.3.4/src/lib.rs

  • Inherited #[mangle] raw_verify openssl/src/ssl/mod.rs

  • Inherited #[mangle] subtransport_free git2/src/transport.rs

  • Inherited #[no_mangle] rust_eh_personality src/libpanic_unwind/lib.rs

  • Inherited #[no_mangle] block_context_copy block/src/lib.rs

  • Inherited #[no_mangle] locking_function openssl-sys/src/lib.rs

  • Inherited #[no_mangle] ppapi_instance_created ppapi/src/lib/lib.rs

  • Inherited #[no_mangle] ppapi_instance_destroyed ppapi/src/lib/lib.rs

  • Public #[no_mangle] __rust_allocate src/liballoc_jemalloc/lib.rs

  • Public #[no_mangle] __rust_allocate src/liballoc_system/lib.rs

  • Public #[no_mangle] __rust_deallocate src/liballoc_jemalloc/lib.rs

  • Public #[no_mangle] __rust_deallocate src/liballoc_system/lib.rs

  • Public #[no_mangle] __rust_maybe_catch_panic src/libpanic_abort/lib.rs

  • Public #[no_mangle] __rust_maybe_catch_panic src/libpanic_unwind/lib.rs

  • Public #[no_mangle] __rust_reallocate src/liballoc_jemalloc/lib.rs

  • Public #[no_mangle] __rust_reallocate src/liballoc_system/lib.rs

  • Public #[no_mangle] __rust_reallocate_inplace src/liballoc_jemalloc/lib.rs

  • Public #[no_mangle] __rust_reallocate_inplace src/liballoc_system/lib.rs

  • Public #[no_mangle] __rust_start_panic src/libpanic_abort/lib.rs

  • Public #[no_mangle] __rust_start_panic src/libpanic_unwind/lib.rs

  • Public #[no_mangle] __rust_usable_size src/liballoc_jemalloc/lib.rs

  • Public #[no_mangle] __rust_usable_size src/liballoc_system/lib.rs

  • Public #[no_mangle] rust_eh_personality src/libpanic_abort/lib.rs

  • Public #[no_mangle] rust_eh_personality_catch src/libpanic_unwind/lib.rs

These would be affected:

  • Public #[mangle] callback glutin/src/api/win32/callback.rs -- used as pub(crate)

  • Public #[mangle] curl_progress_fn curl/src/ffi/easy.rs -- used as a callback

  • Public #[mangle] did_change_focus ppapi/src/lib/lib.rs -- used as pub(crate)

  • Public #[mangle] did_change_view ppapi/src/lib/lib.rs

  • Public #[mangle] did_create ppapi/src/lib/lib.rs

  • Public #[mangle] did_destroy ppapi/src/lib/lib.rs

  • Public #[mangle] graphics_context_lost ppapi/src/lib/lib.rs

  • Public #[mangle] handle_document_load ppapi/src/lib/lib.rs

  • Public #[mangle] handle_input_event ppapi/src/lib/lib.rs

  • Public #[mangle] handle_message ppapi/src/lib/lib.rs

  • Public #[mangle] mz_crc32 flate2/src/ffi.rs -- used as pub(crate)? Probably doesn't have to be extern "C"

  • Public #[mangle] mz_deflateInit2 flate2/src/ffi.rs

  • Public #[mangle] mz_inflateInit2 flate2/src/ffi.rs

  • Public #[mangle] open libc/rust/src/liblibc/lib.rs -- odd, looks like an error?

  • Public #[mangle] FD_SET libc-0.x/src/lib.rs - pub extern fn functions without #[no_mangle] · Issue #376 · rust-lang/libc · GitHub

  • Public #[mangle] FD_ZERO libc-0.x/src/lib.rs

  • Public #[mangle] WCOREDUMP libc-0.x/src/lib.rs

  • Public #[mangle] WEXITSTATUS libc-0.x/src/lib.rs

  • Public #[mangle] WIFCONTINUED libc-0.x/src/lib.rs

  • Public #[mangle] WIFEXITED libc-0.x/src/lib.rs

  • Public #[mangle] WTERMSIG libc-0.x/src/lib.rs

  • Public #[mangle] _WSTATUS libc-0.x/src/lib.rs

Just a reminder that in C++, the only effect of extern "C" is to disable mangling. The default calling convention must be compatible anyway, or else you wouldn't be able to pass normal C++ function pointers to, e.g., C standard library functions. Platforms with multiple calling conventions generally provide separate nonstandard mechanisms to choose one, like __cdecl. Effectively, extern "C" in Rust acts like one of those, and totally unlike/orthogonal to extern "C" in C++, which is pretty confusing.

* Sidenote: Spec nerds may point out that according to the C++ specification, you can't do that; it's not just UB, it's supposed to be a compile error. Actual C++ compilers universally ignore this wording, lots of code depends on it, and the spec will probably be changed in the future.

This is not true at all. Why would extern "C" be supported on function pointers if it only affected name mangling. Please read Language linkage - cppreference.com and look at the examples.

1 Like

Writing extern functions is a totally normal thing when creating callbacks to be passed to C libraries. It is a pattern that is used extensively in Windows API. In some places those callbacks are even generic. For example https://github.com/retep998/wio-rs/blob/master/src/apc.rs#L8. What would happen if that function was #[no_mangle]? As a result, I am opposed to automatically adding #[no_mangle] for all extern functions.

On the other hand, if it is only limited to pub extern functions which are accessible from outside that crate, that is generally less likely to run into issues and more likely to be where people actually intend to export those functions to be callable from C. I’m not entirely comfortable with an implicit #[no_mangle] in those situations, but I’m not really that opposed either. Still would be nice to collect more detailed info on which crates would be negatively affected.

1 Like

is as practical as having to add #[no_ternary] to usize.

If your computer actually had ternary numbers, would you want indexes into arrays to use it? I actually kinda would want to use the native numerical representation, especially for a number that already has its size defined by the platform.

I know, it's beside the point, and we wouldn't want to run Rust on a computer like that anyway. I'll show myself out...

Did you read my sidenote?

Ouch. Sorry.

But still, this [quote="comex, post:7, topic:3973"] the spec will probably be changed in the future [/quote] is not going to happen as far as I know how C++ standardization works.

That's why C++ extends C library with new special overloads taking extern "C++" fn pointers :slight_smile:

It is sort-of my point. Integer size and base are orthogonal concepts in theory. Calling convention and mangling are orthogonal concepts in theory, too.

But my CPU doesn't work in ternary, and my C language doesn't do Rust-like name mangling.

The problem is that no_mangle is an unsafe attribute (because it allows messing up with the linker), while extern "C" is perfectly safe.

It is a wart that we use extern to label our calling conventions (especially because we use it to also label foreign blocks), but that’s about as bad as not having dyn on trait objects.

Maybe we should allow bodies in foreign blocks, so that you could write

extern "C" {
    fn foo() {}
}

But that looks like extra complexity for not much gain.

I'd say linking is unsafe in general. You can link (or LD_PRELOAD) with a library containing e.g. _ZN2io5stdio6_print20h94cd0587c9a534faX3gE and replace a safe Rust function with an unsafe one.

But #[no_mangle] and overriding of symbols exists already, so making mangling easier to use for its intended purpose is not a (new) safety issue.

Hm, this change would (at least with current mangling behaviour) be breaking for me, as I use functions with type parameters as callbacks quite often.

I would be fine if public non-parametrized functions with extern "C" wouldn’t be mangled, this is indeed a nuisance.

The thing with no_mangle is that it is unsafe even if all C code is well-behaved.

In any case, making all pub extern "C" functions #[no_mangle] by-default would be a potentially-silent gratuitous breaking change.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.