Weak linkage


#1

While investigating a linkage bug, I stumbled upon the weak linkage handling in Rust and I think it’s currently quite questionable. This topic is to brainstorm about a better design.

First of all, to remove all confusion anyone (including myself) might have about weak linkage, here’s how it works. When you define a weak symbol and don’t link it in, referencing that symbol directly is illegal. Conversely, obtaining a pointer to that symbol results in NULL. Consider the following C code

#include <stdio.h>

extern __attribute__((weak)) void weakfunc();
extern __attribute__((weak)) void(*weakfuncptr)();
extern __attribute__((weak)) void* weakvar;

void main() {
    printf("%p %p %p\n",&weakfunc,&weakfuncptr,&weakvar);
}

which results in this LLVM code

declare extern_weak void @weakfunc(...) #1
@weakfuncptr = extern_weak global void (...)*
@weakvar = extern_weak global i8*

and the program prints (nil) (nil) (nil). If you would’ve typed printf("%p\n",weakvar) you would have gotten a segfault.

Now that we understand how weak linkage actually works in practice, we can think about what we want in Rust. See issue 31508 and PRs 12556 and 11978 on how weak linkage currently works in Rust. One of the main arguments for the current design is something about non-nullable types. However, this does not seem to have anything to do with whether a type is nullable. It has to do with whether one can safely use a particular variable at all. There are some subtleties with functions and function pointers, so I think it makes more sense to consider the variable case first and extend from there.

It does seem that requiring everything to go through a pointer to the weak symbol makes a lot of sense, but the chosen syntax really does not. Perhaps we can make referencing a weak symbol unsafe (like a static mut) and provider a safe wrapper type/macro. The wrapper would would essentially be Option<&T> for a weak symbol of type T. What are other people’s thoughts?

Side note: This would be the converse of the C weakfunc definition in Rust, but it doesn’t work at all (no linkage specified in LLVM):

extern {
    #[linkage="extern_weak"] fn weakfunc();
}

#2

This was the main rationale for requiring pointers, because otherwise it was basically impossible to actually test whether a weak pointer was null or not. For example:

extern {
    #[linkage = "extern_weak"]
    static FOO: i32;
}

fn foo() -> &'static i32 { &FOO }

This is valid Rust code, yet if we just applied the extern_weak annotation to the definition of LLVM, this would actually return a null pointer, which is a type system violation for the representation of &T. Along the same lines, there’s not actually any way to test whether &FOO is null or not, because by definition (in Rust) that returns &T which is not null.

This is basically the same story for function pointers as well, where they’re non-nullable so if you want to test for null they can’t be represented as a null pointer.


So that’s at least the problem that the current restrictions are trying to solve, and I’d be more than ok tweaking them! I think, however, that we still need to be sure there’s a typesafe way to test whether a weak value is null or not in Rust