Background & Motivation
Some context:
As can be seen from these issues, we're having to come up with a growing list of language rules to make naked functions work reliably, and I worry that we're going to keep running into edge cases in both the language and compiler implementation that will need special handling for naked functions. These rules add extra complexity throughout Sema, and they're really just trying to contend with a rather simple reality: All compilers that implement GCC-style inline assembly and naked functions, including LLVM, consider asm statements to be the only well-defined contents of naked functions. This is a reasonable stance for a compiler to take because, in the absence of an ABI-compliant prologue and epilogue, there are very few constructs that can be lowered correctly. It also makes a lot more sense if you think of naked functions as just being a convenient language feature for emitting a black box of machine instructions in function form, which is how GCC/Clang define them.
Rather than this growing list of language rules and the implementation complexity that come with them, I'd like to suggest what I think will be a simpler way of specifying and implementing naked functions. This proposal will make Zig's naked functions match the GCC/Clang definition and therefore also conform to LLVM's requirements.
(Note that some elements of this proposal are not unique to it; for example, the asm restrictions that I describe below will have to be adopted in some form even if this particular proposal is rejected.)
Proposal
An fn decl prefixed with asm is known as an assembly function (AKA naked function). Such a function cannot be annotated with inline, noinline, or extern, and must have a body. Its calling convention must be specified explicitly and cannot be auto, async, or inline.
The compiler treats an assembly function as a black box for the purposes of optimization and code generation; reachable calls to an assembly function cannot be inlined, reordered, or elided. The compiler will perform basic scaffolding for an assembly function, such as defining the symbol in the resulting object file, but it will not emit any machine instructions in the function body - not even the usual prologue and epilogue. The programmer is expected to provide the implementation by way of inline assembly.
The above definition has some notable consequences:
- The compiler cannot, in the general case, emit valid code to access argument values in an assembly function. Therefore, accessing arguments by name is simply not permitted; instead, the programmer must be aware of the calling convention and unique constraints within the function, and write inline assembly to access them. To go with this, the compiler will not issue its usual "unused parameter" errors in assembly functions.
- Some calling convention options that ordinarily affect a function's implementation, such as
incoming_stack_alignment, will have no effect in an assembly function unless the programmer explicitly writes inline assembly code equivalent to what the compiler would normally have emitted.
return and try expressions are not permitted in an assembly function regardless of what its return type is.
An assembly function has its body comptime-evaluated during semantic analysis; that is, its body is implicitly a comptime { ... } block. During this evaluation, asm expressions that are lexically contained within the assembly function are recorded rather than executed, and have some additional restrictions (see below). Besides this, all the usual comptime rules apply. After comptime evaluation finishes, the function's body is formed in full by concatenating and assembling the recorded asm expressions, in the order they were analyzed. No further compiler transformations are performed on the function body. (Note: The semantics here are very similar to container-level comptime blocks and the way global asm expressions are treated there, but note #24077.)
In an assembly function, there are some additional restrictions for asm expressions:
- They must be marked
volatile.
- Inputs are allowed but with some restrictions.
- The operand must be a
comptime-known value, which includes addresses of fn decls and container-level var/const decls.
- Outputs are allowed but with some restrictions.
- The operand must be a mutable
comptime-known pointer - practically limiting it to pointers into container-level var decls.
- Result outputs are not allowed as they're effectively meaningless in an assembly function.
- Clobbers are not allowed as assembly functions are already assumed to clobber all calling convention registers and memory.
Example
Just to illustrate, here's how an asm fn would actually look:
const builtin = @import("builtin");
fn includeDwarfCfiDirectives() bool {
return builtin.unwind_tables != .none or !builtin.strip_debug_info;
}
pub asm fn clone(
func: *const fn (arg: usize) callconv(.c) u8,
stack: usize,
flags: u32,
arg: usize,
ptid: ?*i32,
tp: usize,
ctid: ?*i32,
) callconv(.c) usize {
asm volatile (
\\ movl $56, %%eax // SYS_clone
\\ movq %%rdi, %%r11
\\ movq %%rdx, %%rdi
\\ movq %%r8, %%rdx
\\ movq %%r9, %%r8
\\ movq 8(%%rsp), %%r10
\\ movq %%r11, %%r9
\\ andq $-16, %%rsi
\\ subq $8, %%rsi
\\ movq %%rcx, (%%rsi)
\\ syscall
\\ testq %%rax, %%rax
\\ jz 1f
\\ retq
\\1:
);
// This if condition is implicitly comptime...
if (includeDwarfCfiDirectives()) {
// ... and this asm is only included in the function body if the condition evaluated to true.
asm volatile (
\\ .cfi_undefined %%rip
);
}
asm volatile (
\\ xorl %%ebp, %%ebp
\\ popq %%rdi
\\ callq *%%r9
\\ movl %%eax, %%edi
\\ movl $60, %%eax // SYS_exit
\\ syscall
);
}
See Also
Background & Motivation
Some context:
start: Avoid string concatenation in inline asm. #21056inlinefunctions called from.Nakedfunctions cause stack allocations #21193comptimeevaluation #21415As can be seen from these issues, we're having to come up with a growing list of language rules to make naked functions work reliably, and I worry that we're going to keep running into edge cases in both the language and compiler implementation that will need special handling for naked functions. These rules add extra complexity throughout Sema, and they're really just trying to contend with a rather simple reality: All compilers that implement GCC-style inline assembly and naked functions, including LLVM, consider
asmstatements to be the only well-defined contents of naked functions. This is a reasonable stance for a compiler to take because, in the absence of an ABI-compliant prologue and epilogue, there are very few constructs that can be lowered correctly. It also makes a lot more sense if you think of naked functions as just being a convenient language feature for emitting a black box of machine instructions in function form, which is how GCC/Clang define them.Rather than this growing list of language rules and the implementation complexity that come with them, I'd like to suggest what I think will be a simpler way of specifying and implementing naked functions. This proposal will make Zig's naked functions match the GCC/Clang definition and therefore also conform to LLVM's requirements.
(Note that some elements of this proposal are not unique to it; for example, the
asmrestrictions that I describe below will have to be adopted in some form even if this particular proposal is rejected.)Proposal
An
fndecl prefixed withasmis known as an assembly function (AKA naked function). Such a function cannot be annotated withinline,noinline, orextern, and must have a body. Its calling convention must be specified explicitly and cannot beauto,async, orinline.The compiler treats an assembly function as a black box for the purposes of optimization and code generation; reachable calls to an assembly function cannot be inlined, reordered, or elided. The compiler will perform basic scaffolding for an assembly function, such as defining the symbol in the resulting object file, but it will not emit any machine instructions in the function body - not even the usual prologue and epilogue. The programmer is expected to provide the implementation by way of inline assembly.
The above definition has some notable consequences:
incoming_stack_alignment, will have no effect in an assembly function unless the programmer explicitly writes inline assembly code equivalent to what the compiler would normally have emitted.returnandtryexpressions are not permitted in an assembly function regardless of what its return type is.An assembly function has its body
comptime-evaluated during semantic analysis; that is, its body is implicitly acomptime { ... }block. During this evaluation,asmexpressions that are lexically contained within the assembly function are recorded rather than executed, and have some additional restrictions (see below). Besides this, all the usualcomptimerules apply. Aftercomptimeevaluation finishes, the function's body is formed in full by concatenating and assembling the recordedasmexpressions, in the order they were analyzed. No further compiler transformations are performed on the function body. (Note: The semantics here are very similar to container-levelcomptimeblocks and the way globalasmexpressions are treated there, but note #24077.)In an assembly function, there are some additional restrictions for
asmexpressions:volatile.comptime-known value, which includes addresses offndecls and container-levelvar/constdecls.comptime-known pointer - practically limiting it to pointers into container-levelvardecls.Example
Just to illustrate, here's how an
asm fnwould actually look:See Also