Virtual Origins
A !virtual block assembles code for an address it is not placed at. Labels inside it resolve at the address you give; the bytes carry on inline in the image, exactly where they would have gone without it.
This is what you want for a routine that runs somewhere other than where it is loaded — one copied into a scratch buffer, into screen memory, or into a bank that is not paged in at build time.
!virtual
Syntax: !virtual <address> [, <page>] … !endvirtual
!virtual opens a block whose labels resolve at address while its bytes stay inline. address is an expression; page is an expression or ?, and names the page the block’s labels are recorded in — see Page Argument.
A block runs until !endvirtual and there is no other closing form, unlike a library, which runs to the end of its file.
org $8000
main:
ld hl, src ; where the bytes are
ld de, dst ; where they will run
ld bc, src_end - src
ldir
jp dst
src:
!virtual $C000
dst:
ld a, $11
out ($FE), a
ret
!endvirtual
src_end:dst is $C000, read from anywhere in the program. src and src_end are the image addresses either side of the block, so src_end - src is its length — you never count the bytes yourself, and the figure stays right when the routine grows.
A Second Build
The alternative is a second build: assemble the routine at $C000 into a .bin of its own, declare it as a target, then incbin the blob.
That works, and it costs you the routine’s symbols. They belong to the other build’s symbol table, so nothing in the main program can name them — no --sld entry, no exported symbol, no !assert about anything inside it, and a debugger stepping into the copied code has nothing but addresses. A virtual block keeps the lot, because it is one assembly.
It also costs you a build step, a dependency to declare, and a file on disk.
$ and Jumps
$ inside the block is the virtual address. That is what makes a jr, a djnz or a jr nz correct: the displacement is worked out from where the instruction will actually run.
org $8000
!virtual $C000
.loop:
djnz .loop ; correct at $C000, where it will run
!endvirtualAn absolute reference is right for the same reason. jp dst from outside the block reads $C000, which is where dst is once the copy has run — and nowhere before that.
Page Argument
!virtual <addr>, <page> names the 8 KiB page the block’s labels are recorded in.
Without it, debug info takes the page from whatever the slot held while the source was read. For code that will be copied into a different page that is simply wrong, and a debugger told so will show you the wrong disassembly:
!device zxnext
org $8000
payload:
!virtual $C000, 34 ; slot 6 holds page 34 when this runs
entry:
ret
!endvirtualIt changes what is recorded and nothing about where bytes go. The slot map still routes emission by the image address, which is the whole point of the block, so the argument cannot be used to move code into a bank. Use bank or page for that, and see Memory and Banking.
Write ? where you cannot name the page. A dot command’s payload is copied into whatever the OS hands over, so there is no page to give:
!device zxnext
org $8000
!virtual $2000, ? ; the OS chooses where this runs
handler:
!assert a == 0
ret
!endvirtualEvery SLD record the block covers then carries page -1 — the labels, the trace records and the annotations alike — and a debugger arms them by address alone. page ?, <slot> says the same thing for ordinary code at an org; see Memory and Banking.
It needs a machine that pages. zx16 and zx48 refuse it (E1136), anything but zxnext says so once (W1130), and a page the machine does not have is E1082.
Garbage Collection
Code in a block is reached by the copy rather than by a call to its virtual address. A sweep must not take it, and does not: !virtual ends the extent of whatever label preceded it, so no label outside a block owns the lines inside one and --gc-modules has nothing it can name unreachable.
A label inside a block owns its lines as usual and is swept when nothing references it. If you want one kept, keep it — see Modules.
Refusals
| Code | |
|---|---|
org, bank, page, slot | E1144 |
module, !library | E1144 |
!rhai | E1144 |
Another !virtual | E1145 |
--reloc | E1146 |
Still open at the end of a file, or of an include | E1074 |
!endvirtual with nothing open | E1143 |
The first three are about the two addresses. org, bank, page and slot each move both the address labels resolve to and the address bytes go to, so inside a block each is ambiguous about which you meant. A module or a !library body is moved to the end of the program, which would take it out of the block. A !rhai script is handed one address and hands one back.
Nesting is refused for now rather than for good. What it would express is two-stage relocation — a block copied to one address that itself copies an inner payload to a second, where the inner payload’s run-time source address is its address inside the outer block. Nothing else can name that, and the message says as much.
--reloc is refused for good. A relocation offset is an image address, and it tells a loader which bytes to add its load address to; a block’s operands name addresses the image does not hold. See Output Formats.
A struct instance inside a block is fine, and places its members at virtual addresses.
!test and a Virtual Block
⚠ A !test cannot reach into one. A block’s code is not at its virtual address until the copy has run, so a test that calls into one gets whatever happens to be at that address instead — usually nothing, and silently.
Test the copy rather than the target, or write the routine outside a block, test it there, and wrap it in one when it ships. See Testing.