I wasn't thinking that having a function prototype incompatible with the external fortran code would be undefined, just, uh, wrong in a normal sort of way.
The type signature is the rule of law that allows you to normally pass safe pieces of memory between two systems. If you define it badly, what exactly is supposed to catch this and tell you that it's wrong?
Is the compiler supposed to speak both languages, and be able to disassemble whatever object code it's given when linking to attempt to determine if the signature is valid?
Since the spec doesn't speak about FFI at all it seems, I think this is implementation defined behavior, not undefined behavior.
And it does. The signature is wrong, however, and so the resulting call will be wrong.
Since the standard doesn't speak about FFI, and the invalid call is actually in another language's memory, I'd say we're in completely undefined territory here.
To try and say that more clearly, I'd still call it undefined behaviour if an invalid type signature in Java calling Go resulted in something going wrong. Java can't know how Go is supposed to react to a fudge in it's internal memory semantics.
The issue I have with "undefined behavior" is that defining undefined behavior is like dividing by zero. It's like saying you have proven the efficacy of the placebo effect. Once you start believing in contradictions, you are on the road to hell.
There is an immeasurable difference between what you say Rust does, leaving undefined behavior undefined, and trying to have it both ways.
C was standardized nearly 20 years after the first C compiler. The original C standard and its revisions were written in a way that (with some exceptions and caveats) effectively christened existing implementations and codebases as "C", just not necessarily in their entirety. The guiding principle has always been to iteratively formalize and evolve existing practice. If a standard were to effectively tell any compiler or project that wasn't line-for-line "conformant" or "correct" or "strictly conformant" to GTFO, the C standard would have failed in bringing order to chaos. The use of "undefined behavior" was and remains a mechanisms for achieving that goal. C isn't trying to "have it both ways"; it's addressing real, substantial dilemmas within a huge software ecosystem.
C's approach is somewhat unique and idiosyncratic, but rooted in a historical context that cannot be ignored. And it's success is undeniable. Some languages, like Rust, with only a single well-accepted implementation have the luxury of staying silent. Other languages (e.g. Ada, once upon a time) with diverse, equally relevant implementations that use a heavier hand in directing the evolution have a poor track record. You can't force a vendor to implement some behavior if they don't want to; and if you attempt to coerce them they're liable to walk away. C++ is another somewhat unique case, but while there remains substantial diversity of C compilers that are as a practical matter functional C11 implementations (C11 made alot of C99 optional), there are now effectively only two relevant C++ compilers: GCC and clang. (Maybe Visual Studio, we'll see.)
Step back and consider why C remains so popular and successful despite its obvious and well acknowledged faults. People think it's an accident of history that C has been so influential, but I think that's self-serving. It's much easier to say that than to acknowledge that the evolution of C reflects an uncomfortable reality in the technology industry regarding the forces that lead to success and failure. Node.js and PHP reflect some similar uncomfortable truths. C's concept of "undefined behavior" is an attempt to cabin and grapple with some of those realities in an honest and more intellectually rigorous manner. That's why it's so easy to pick on "undefined behavior" in C; its whole function is to help outline the contours of and acknowledge some very deep pathologies rather than ignoring them or pretending they don't exist. Rust's unsafe{} is loosely similar, but you can't directly compare the two because both the historical and contemporary contexts are different. Again, there's precisely 1 Rust implementation; it doesn't need to formalize undefined behavior in a way that permits reconciling diverse behaviors (behaviors that are often justifiably relied upon) and providing a path to better, safer, unified semantics. Rust developers can ignore various matters of undefined behavior until one day it can dictate the One True Way, without risk of splitting the community or making itself irrelevant.
The computing world isn't constrained to Linux and BSD clones running on the server room or mobile phones.
Is that really true? Couldn't the same goal be achieved by using implementation-defined behavior instead?
I thought C keeps undefined behavior because it leaves the door open for optimizations. Otherwise (for example) int overflow could be left as implementation-defined behavior: almost everyone gets the expected behavior (two's complement wrap-around) and the 0.01% unlucky enough to be using a really old and weird processor gets some other behavior (one's complement wrap-around, traps or who knows what other kinds of processors have ever existed).
The reason why this is not done, in my understanding, is that int overflow (among other stuff) being left undefined means the compiler has more room for optimization.
An implementer, though, may write down, "The behavior of this construct is undefined", and they have discharged that responsibility.
Undefined really does mean not defined -- but, not defined in the Standard. Anybody is free to define anything the Standard doesn't, and a program may rely on that. #include <unistd.h> is UB, but your compiler uses the definition provided by the (rather, a) separate Posix Standard.
Implementations do take advantage of explicit UB for optimization opportunities, but that is not its purpose.
That's not true: https://github.com/thepowersgang/mrustc
In other words, it's not properly a compiler, as such, but a bootstrapping tool. It does important compiler-esque things, but not others, and is useful to no one else.
This is fundamentally an FFI issue. The C compiler is relying on the function signature declared, explicitly or implicitly, by the person writing the binding code. But this declared signature is wrong, and because it's wrong C says that the behavior when invoking the routine is undefined--undefined behavior.
Other languages would have the exact same result. If you told Rust that the FFI prototype for a FORTRAN routine was foo(pointer-to-char) instead of foo(pointer-to-char, size_t), you'd get the exact same broken runtime behavior as with the C code. But Rust never defines this as "undefined behavior". Rust's behavior is no less undefined in the practical sense, it just don't formalize the concept in its specification, such as it is. Like most languages it side steps the issue because it's a thorny area for a specification, especially thorny now that the literal phrase "undefined behavior" elicits reactionary flames from the peanut gallery. If you say nothing you're not inviting criticism, nor are you burdened with managing yet another dimension of consistency in your specification.
There are problems with "undefined behavior" in C, how it works in the standard and especially how compilers use it to shift blame for insecure semantics. The problems are just nuanced and technical and can't be constructively discussed outside specific contexts. And especially in discussions comparing C to different languages, these problems get conflated with orthogonal issues like type safety.
EDIT: For context here are some relevant citations to C11 itself:
C11 (N1570) 3.4.3p1: "undefined behavior [is] behavior, upon use of a nonportable or erroneous program construct or of erroneous data, for which this International Standard imposes no requirements"
C11 (N1570 3.4.3p2: "NOTE Possible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message)."
C11 (N1570) 4p2: "If a ‘‘shall’’ or ‘‘shall not’’ requirement that appears outside of a constraint or runtime-constraint is violated, the behavior is undefined. Undefined behavior is otherwise indicated in this International Standard by the words ‘‘undefined behavior’’ or by the omission of any explicit definition of behavior. There is no difference in emphasis among these three; they all describe ‘‘behavior that is undefined’’."