This document describes the macros in:
C:\git\~tgrysztar\fasm2\include\macro\proc64.inc
The file is a Win64-oriented call/procedure macro layer for the fasmg dialect. It is small enough to read in one sitting, but dense enough that a few details are easy to miss. Some of those details are merely convenient tricks; others are serious footguns.
The source revision inspected here is the file dated 2026-02-24 21:53:30.
proc64.inc assumes the Microsoft x64 calling convention:
- integer/pointer arguments 1..4 in
rcx,rdx,r8,r9 - floating-point arguments 1..4 in
xmm0..xmm3 - 32 bytes of shadow space
- 16-byte stack alignment at call sites
It is not a SysV AMD64 layer. It also does not generate unwind metadata, stack probes, or any Windows exception metadata.
| Lines | Topic |
|---|---|
| 1-25 | Register aliases for the first four argument positions |
| 27-45 | fastcall.frame, frame, end frame, endf |
| 48-59 | fastcall.inline_string |
| 61-246 | fastcall call marshaller |
| 248-254 | invoke, cinvoke |
| 256-259 | pcountcheck, pcountsuffix |
| 261-294 | Default prologuedef / epiloguedef |
| 303-505 | proc, ret, locals, endl, proclocal |
| 507-550 | static_rsp_* alternate prologue/epilogue hooks |
The file contains four separate systems:
- Call-site helpers:
fastcall,invoke,cinvoke - Outgoing-stack reservation helpers:
fastcall.frame,frame,endf - Procedure-definition helpers:
proc,ret,locals,endl,proclocal - Prologue/epilogue customization hooks:
prologue@proc,epilogue@proc,close@proc
These systems are related, but not fully integrated. That is why some combinations are elegant and some are surprising.
The first 25 lines define aliases such as:
fastcall.r1=rcxfastcall.r2=rdxfastcall.r3=r8fastcall.r4=r9fastcall.rf1=xmm0fastcall.rf2=xmm1fastcall.rf3=xmm2fastcall.rf4=xmm3
There are also size-specific aliases:
fastcall.rd1=ecxfastcall.rw1=cxfastcall.rb1=cl
The fastcall macro uses these names instead of hard-coding the registers in its body.
fastcall is the core marshalling macro:
fastcall proc,argsIt:
- classifies each argument
- places the first four arguments in the Win64 argument registers
- writes stack arguments into the outgoing call frame
- reserves at least 32 bytes of shadow space
- rounds the total call frame up to a 16-byte boundary
- emits the final
call proc
fastcall emits a literal call proc at the end.
That means:
- use
fastcall SomeLabel,...for a direct call to a code label - use
fastcall [ptr],...for an indirect call through memory - use
fastcall rax,...only ifcall raxis what you want
The macro recognizes several special cases.
| Argument form | Meaning |
|---|---|
| ordinary operand | integer/pointer argument |
float expr |
scalar floating-point argument |
addr expr |
address-of, implemented with lea |
| quoted string | turned into inline TCHAR data, then passed by address |
nested fastcall ... or invoke ... |
evaluate nested call first, use its rax result |
For ordinary arguments, the source operand size controls the move width:
- 8-byte or unsized:
mov rcx/rdx/r8/r9,... - 4-byte:
mov ecx/edx/r8d/r9d,... - 2-byte:
mov cx/dx/r8w/r9w,... - 1-byte:
mov cl/dl/r8b/r9b,...
This is convenient, but it also means the macro will not automatically promote a byte or word argument to a clean 32-bit value. If the callee expects an int, pass an int.
The macro counts one 8-byte slot per argument, then does:
- minimum frame size =
20h - otherwise enough slots for all arguments
- then rounded up to a multiple of 16
Examples:
- 0..4 arguments:
20h - 5 arguments:
30h - 6 arguments:
30h - 7 arguments:
40h
Argument 5 goes to [rsp+20h], argument 6 to [rsp+28h], and so on.
These are thin wrappers:
invoke proc,args ; expands to fastcall [proc],args
cinvoke proc,args ; same thing on Win64So:
invoke MessageBox,...is for imported-IAT style labels or qword pointer variablesfastcall MessageBox,...is for a direct label call
A very common mistake is this:
invoke MyLocalProc,1,2,3That does not call MyLocalProc directly. It expands to fastcall [MyLocalProc],..., which means an indirect call through the qword stored at MyLocalProc.
For local procedures, use fastcall MyLocalProc,....
float expr puts the low 32 or 64 bits into xmm0..xmm3 for the first four positions, or into stack slots for later positions.
Examples:
invoke glUniform1f,[uTimeLoc],float xmm1
invoke glClearColor,float dword 0.02,float dword 0.02,float dword 0.04,float dword 1.0Important limits:
- this is for scalar single/double values
- it is not a general SIMD/vector argument layer
- it does not mirror FP arguments into GP registers for varargs calls
That last point matters for Microsoft x64 varargs and unprototyped-call rules. If you need C varargs behavior with FP arguments, set the registers manually.
addr expr means "pass the address of expr", implemented with lea.
Examples:
invoke GetMessage,addr msg,NULL,0,0
invoke ChoosePixelFormat,[hdc],addr pfdUse addr symbol, not addr [symbol]. The macro itself adds the addressing form.
If an argument looks like a string literal, fastcall calls fastcall.inline_string, which by default emits inline TCHAR data and passes its address.
More on that below.
This is where most of the cleverness lives.
The macro makes one pass over the arguments looking only for nested fastcall or invoke expressions. It evaluates those nested calls first. If there is more than one nested call, earlier rax results are spilled into the current outgoing-call area.
Then it makes a second pass and loads the real call arguments.
Nested calls are supported, but only partially:
- the nested result is assumed to live in
rax - earlier nested results may be spilled into the outer call frame
- non-call arguments are not "captured" before later nested calls run
So this can clobber data.
This source:
mov rcx,1234h
fastcall TargetA,rcx,fastcall TargetBassembled to the equivalent of:
mov rcx,1234h
sub rsp,20h
sub rsp,20h
call TargetB
add rsp,20h
mov rdx,rax
call TargetA
add rsp,20hNotice what did not happen:
- the first argument was not saved anywhere before
TargetB rcxwas reused as-is after the nested call
If TargetB clobbers rcx, the first argument is wrong.
Save volatile inputs before a nested call:
mov rbx,rcx ; nonvolatile
fastcall TargetA,rbx,fastcall TargetBor:
mov [saved_value],rcx
fastcall TargetA,[saved_value],fastcall TargetB- Nested FP-returning calls are not handled as FP values. The nested-call path is built around
rax, notxmm0. - Raw
rsp-relative memory arguments are fragile, because the macro allocates temporary outgoing space and also uses the home-area slots as scratch for nested results. raxis scratch during argument setup even without nested calls.
The safest assumption is:
- nested
invoke/fastcallis only safe for integer/pointer return values - any earlier volatile input must be saved explicitly first
fastcall.frame controls whether fastcall allocates stack space itself or only tracks how much space would be needed.
At the top of the file:
fastcall.frame = -1When it is negative, each fastcall does its own:
sub rsp,framesizecall ...add rsp,framesize
When fastcall.frame >= 0, a fastcall no longer allocates or frees its own call frame. Instead it updates fastcall.frame to the maximum frame size needed so far.
This is how the OpenGL example precomputes one shared frame size for a block of code:
fastcall.frame = 0
...
MAIN_FRAME := fastcall.frame
fastcall.frame = -1Then the code manually reserves MAIN_FRAME once.
frame ... end frame is a lexical helper around that tracking mode.
frame
invoke Foo, ...
invoke Bar, ...
end frameIt:
- switches
fastcall.frameto tracking mode for the block - emits one
sub rsp,sizebefore the block - lets nested
fastcall/invokereuse that space - emits
add rsp,sizeatend frame
endf is just a short alias for end frame.
frame is not integrated with ret.
Inside a proc, bare ret expands to the current epilogue hook. It does not know that you opened a frame block.
That means this source:
proc FrameProc
frame
fastcall TargetA,1,2,3,4,5
ret
endf
endpcan assemble into:
push rbp
mov rbp,rsp
sub rsp,30h
...
call TargetA
leave
retn
add rsp,30h ; dead codeThe leave balanced the stack at runtime, but the endf still emitted its add rsp,... after the ret.
So:
- you still need
endfin the source to restore assembler state - but if you want a clean runtime flow, structure the code so execution reaches
endfbeforeret
A safer pattern is:
proc Better
frame
...
endf
ret
endpor with one exit label:
proc Better
frame
...
jmp .done
.done:
endf
ret
endpOnly fastcall / invoke participate in fastcall.frame tracking.
If you write a raw:
call SomeProcinside a tracked block or a static-RSP procedure, you must reserve the required shadow space yourself.
Default definition:
macro fastcall?.inline_string var
local data,continue
jmp continue
if sizeof.TCHAR > 1
align sizeof.TCHAR,90h
end if
match value, var
data TCHAR value,0
end match
redefine var data
continue:
end macroMeaning:
- emit a jump over inline string data
- emit a zero-terminated
TCHARstring - redefine the original macro argument to the string label
On win64a.inc, TCHAR is ANSI bytes. On win64w.inc, TCHAR is UTF-16 words.
The default behavior means:
- string data is emitted at the call site
- identical strings are duplicated
- the code section now contains embedded data
That is fine for small code, but not always what you want.
The shipped examples\globstr\demo_windows.asm shows the intended extension point:
macro fastcall?.inline_string var
local data
data GLOBSTR var,0
redefine var data
end macroThis redirects string materialization into a global-string mechanism instead of inline code/data.
If you redefine this macro, make sure it still does one essential thing:
- it must leave
varredefined to an address expression usable by the rest offastcall
If your replacement emits bytes into the instruction stream, it also needs to handle control flow safely, like the default jmp continue version does.
The proc macro defines named procedures and creates helper macros inside the procedure body.
Basic forms:
proc Name
proc Name, p1,p2,p3
proc Name uses rbx rsi rdi, p1,p2
proc c Name, p1,p2
proc stdcall Name, p1,p2The parser accepts c and stdcall, but the default 64-bit code generation does not meaningfully distinguish them.
cinvokeis identical toinvokeproc candproc stdcallmainly pass differentflagvalues to custom hook macros
There is no 32-bit-style name decoration here.
A proc body is wrapped in:
if used name
name:
...
end ifSo an unreferenced procedure emits no code at all.
That is useful for dead-code removal, but it is surprising if you expected every proc to become a label unconditionally.
The defaults are:
push rbp
mov rbp,rsp
sub rsp,locals+padding
push used_regs...and:
pop used_regs...
leave
retnA few notes:
- locals are addressed from
rbp - parameter labels start at
rbp+16 - saved
usesregisters are below the local area - padding is inserted so that later calls remain aligned
uses simply expands to push / pop.
That means it is only appropriate for pushable general-purpose registers.
Do not expect it to handle:
xmm6..xmm15ymm/zmm- any custom save convention
If you need nonvolatile XMM preservation, do it manually or with a custom prologue.
This is the single most important semantic point of proc64.inc.
For a default proc, parameter labels start at rbp+16:
- arg1 at
[rbp+10h] - arg2 at
[rbp+18h] - arg3 at
[rbp+20h] - arg4 at
[rbp+28h] - arg5 at
[rbp+30h]
That matches the Win64 home-area / stack layout.
But the caller does not automatically write the first four register arguments into those home slots.
So inside:
proc WindowProc hwnd,wmsg,wparam,lparamthe symbols hwnd, wmsg, wparam, lparam are memory locations, not aliases for rcx, rdx, r8, r9.
If you want [hwnd] to hold the actual incoming rcx, you must spill it yourself:
proc WindowProc hwnd,wmsg,wparam,lparam
frame
mov [hwnd],rcx
mov [wmsg],rdx
mov [wparam],r8
mov [lparam],r9
...
endf
ret
endpThe OpenGL example does exactly this for hwnd.
- use the raw registers directly if you only need the first four arguments briefly
- spill them to their home slots or to locals if you want stable named storage
- arguments 5 and later already live in memory
This is one of the sharpest x64-specific traps in the file.
Typed parameters are laid out with:
label ?argname:type
rb typeThat means the offset advances by the declared type size, not by an 8-byte ABI slot.
For example:
proc TypedParams a:dword,b:dword,c:qword
mov eax,[a]
mov edx,[b]
mov rcx,[c]
ret
endpassembled to accesses at:
a->[rbp+10h]b->[rbp+14h]c->[rbp+18h]
That is tightly packed. It does not match Win64 argument-slot spacing.
So on x64:
proc p, a,b,cis ABI-shapedproc p, a:dword,b:dword,c:qwordis a packed overlay and usually not ABI-shaped
For real Win64 call interfaces, prefer untyped parameters:
proc Foo, a,b,cand then read them with explicit sizes:
mov eax,[a]
mov edx,[b]
mov rcx,[c]Treat typed parameters as an advanced overlay tool, not as a safe C-style prototype system.
Inside a proc, ret is redefined.
retexpands to the current epilogue hook, usually:
leave
retnret 8does not use the epilogue hook. It expands directly to:
retn 8That means it bypasses:
- register restoration from
uses - custom epilogue logic
- static-RSP cleanup
On Win64, retn imm is almost always the wrong thing anyway, because callers do not expect callee stack cleanup.
Practical rule:
- in
proc64.inc, use bareret - only use
ret immif you intentionally want raw machine semantics and are cleaning up everything yourself
Inside a proc, the macro defines a local-layout DSL.
This opens a virtual at localbase@proc+current block. You can declare locals with ordinary data-like syntax:
proc Example
locals
counter dd ?
value dq ?
pt POINT
buf rb 64
endl
...
ret
endpThe labels become stack-relative addresses.
If you initialize locals inside the block, the macro emits runtime mov instructions after the prologue.
Example:
proc InitLocals
locals
a dq 0123456789ABCDEFh
b dd 11223344h
c db 55h
endl
ret
endpassembled to code equivalent to:
sub rsp,10h
mov dword [rbp-10h],89ABCDEFh
mov dword [rbp-0Ch],01234567h
mov dword [rbp-08h],11223344h
mov byte [rbp-04h],55hNotice that the 64-bit constant was split into two 32-bit stores. That is intentional: x86-64 has no generic mov qword [mem], imm64 encoding.
proclocal is a shorthand that expands to a locals ... endl block. It accepts several forms:
name:typename[count]:typename[count]name typename
Examples:
proclocal temp:qword, flags:dword, buf[64]:byte, pair[2]Notes:
name[count]defaults to qword array storage- structure types work
- bare names default to qword storage
Three symbols control procedure generation:
prologue@procepilogue@procclose@proc
By default:
prologue@proc equ prologuedefepilogue@proc equ epiloguedefclose@procis empty
The hook macros receive:
procname,flag,parmbytes,localbytes,reglistThis is the intended extension mechanism for alternate stack layouts.
These three macros provide a useful alternate procedure style:
- save
usesregisters first - reserve locals plus a tracked outgoing-call frame once
- keep
rspstable for allfastcall/invokeinside the procedure
This is especially handy when:
- the procedure makes many calls
- you want one outgoing frame reservation
- you prefer
rsp-relative stability to repeatedsub/add rsp
prologue@proc equ static_rsp_prologue
epilogue@proc equ static_rsp_epilogue
close@proc equ static_rsp_close
proc Work uses rbx, a,b,c,d,e
...
invoke Target,1,2,3,4,5
ret
endp
restore prologue@proc,epilogue@proc,close@procstatic_rsp_prologue:
- pushes the
usesregisters - computes aligned local storage
- reserves one outgoing frame sized from
fastcall.frame - sets:
localbase@procregsbase@procparmbase@procframesize@proc
- switches
fastcall.frameinto tracking mode
static_rsp_close then assigns the final tracked maximum back into the prologue's frame symbol, forcing later assembly passes to grow the prologue if needed.
In a test procedure with one uses rbx and one 5-argument fastcall, the result was:
push rbx
sub rsp,30h
...
call Target
add rsp,30h
pop rbx
retThere was no extra per-call sub/add rsp inside the body. The one prologue reservation covered the call.
- it still does not spill the first four incoming argument registers for you
- raw
callinstructions are still invisible tofastcall.frame - unwind metadata is still not generated
fastcall always ends with call proc.
That means you can temporarily redefine call and pass a dummy token to reuse only the argument marshalling.
com64.inc does exactly that:
macro call dummy
mov rax,[rcx]
call [rax+Interface.proc]
end macro
fastcall -,handle,args
purge callThis is a very nice pattern for:
- COM vtable calls
- custom dispatchers
- trampoline wrappers
- "set up Win64 arguments, but use a special final call sequence"
By default:
macro pcountcheck? proc*,args*
end macro
define pcountsuffix %So plain proc64.inc does not enforce parameter counts.
However, win64axp.inc and win64wxp.inc redefine pcountcheck and load prototype-count tables for imported APIs. In that mode, invoke / fastcall can reject bad argument counts at assembly time.
Also, a proc defines name% := parmbytes/8 when used.
This is useful, but remember the typed-parameter caveat:
- packed typed parameters can make
parmbytes/8diverge from the conceptual parameter count
For ordinary untyped x64 procedures, it is fine.
invoke Foo,... means fastcall [Foo],..., not fastcall Foo,....
Use fastcall for local direct calls.
hwnd, arg1, and so on are home-slot labels, not magical register aliases.
Spill rcx, rdx, r8, r9 yourself if you want [param].
On x64 that usually does not match ABI slot spacing. Treat typed parameters as overlays, not prototypes.
fastcall Target,rcx,fastcall Helper is dangerous unless rcx has already been saved somewhere stable.
It is not a safe generic mechanism for FP-returning nested calls.
It skips uses restoration and custom epilogues. On Win64 it is almost always wrong.
Forgetting endf breaks assembler state. Reaching bare ret before endf can leave dead add rsp,... code behind.
Only fastcall / invoke update fastcall.frame.
Nonvolatile XMM registers must be saved manually.
That can duplicate data and produce mixed code/data layout.
These macros emit code only. They do not emit .pdata / .xdata or SEH unwind records.
That matters for:
- structured exception unwinding
- stack walking
- some debugger/profiler behavior
The default and static prologues just do sub rsp,.... If you need a very large frame, probe it yourself in the Windows-approved way.
If a procedure is never "used", it emits no code at all.
- Use
fastcallfor direct local labels andinvokefor imported/qword-pointer style labels. - Treat
procparameter names as stack locations, not register names. - Keep x64 parameters untyped unless you explicitly want a packed overlay.
- Save any volatile input before using nested
invoke/fastcallexpressions. - Use bare
ret, notret imm. - Balance every
framewithend frame/endf, even if the epilogue would restorerspanyway. - Use
static_rsp_*if the procedure makes many calls and you want a single reserved outgoing frame. - Redefine
fastcall.inline_stringif you want pooled strings instead of code-local literals. - Do not assume the macros handle varargs FP duplication, unwind metadata, or stack probing for you.
fastcall LocalRoutine,1,2,3invoke MessageBox,HWND_DESKTOP,addr text,addr caption,MB_OKproc WindowProc hwnd,wmsg,wparam,lparam
frame
mov [hwnd],rcx
mov [wmsg],rdx
mov [wparam],r8
mov [lparam],r9
...
endf
ret
endpprologue@proc equ static_rsp_prologue
epilogue@proc equ static_rsp_epilogue
close@proc equ static_rsp_close
proc Worker uses rbx, a,b,c,d,e
mov [a],rcx
invoke Target,1,2,3,4,5
ret
endp
restore prologue@proc,epilogue@proc,close@procmacro call dummy
mov rax,[rcx]
call [rax+Interface.Method]
end macro
fastcall -,handle,arg1,arg2
purge callproc64.inc is compact and powerful, but it is not a high-level ABI abstraction. It is closer to a carefully engineered macro toolkit with a Win64 bias.
Used in its intended style, it is elegant:
- direct local calls with
fastcall - imported calls with
invoke - manual spills of incoming register parameters
- optional frame preallocation
- custom prologue hooks when needed
Used as if it were a C prototype system, it becomes misleading:
- typed parameters do not mean ABI-safe parameters
- nested calls are only partially expression-safe
ret immis too raw- no runtime metadata is generated
Read it as a macro toolkit, not as a full calling-convention compiler, and it makes much more sense.