unit test for %output library, and docs.

This commit is contained in:
Irmen de Jong
2025-01-27 22:24:57 +01:00
parent ee784e1ccc
commit a106c88054
6 changed files with 236 additions and 14 deletions

View File

@@ -0,0 +1,106 @@
package prog8tests.compiler.codegeneration
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import prog8.code.ast.PtBlock
import prog8.code.ast.PtVariable
import prog8.code.core.BaseDataType
import prog8.code.core.DataType
import prog8.code.target.Cx16Target
import prog8tests.helpers.compileText
import kotlin.io.path.readBytes
import kotlin.io.path.readText
class TestLibrary: FunSpec({
test("library compilation") {
val src="""
%address ${'$'}A050
%memtop ${'$'}C000
%output library
%import textio
main {
; Create a jump table as first thing in the library.
uword[] @shared @nosplit jumptable = [
; NOTE: the compiler has inserted a single JMP instruction at the start of the 'main' block, that jumps to the start() routine.
; This is convenient because the rest of the jump table simply follows it,
; making the first jump neatly be the required initialization routine for the library (initializing variables and BSS region).
; btw, ${'$'}4c = opcode for JMP.
${'$'}4c00, &library.func1,
${'$'}4c00, &library.func2,
]
sub start() {
; has to be here for initialization (BSS, variables init).
%asm {{
nop
}}
}
}
library {
sub func1() {
cx16.r0L++
}
sub func2() {
cx16.r0L--
}
}"""
val result = compileText(Cx16Target(), true, src, writeAssembly = true)!!
val ep = result.codegenAst!!.entrypoint()
val main = ep!!.parent as PtBlock
main.name shouldBe "p8b_main"
val jumptable = main.children[0] as PtVariable
jumptable.name shouldBe "p8v_jumptable"
jumptable.type shouldBe DataType.arrayFor(BaseDataType.UWORD, false)
jumptable.arraySize shouldBe 4u
val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
println(asm)
val bin = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".bin").readBytes().map { it.toUByte() }
val loadAddr = 0xa050u
val startAddr = loadAddr + 0xbu
val func1Addr = loadAddr + 0x10u
val func2Addr = loadAddr + 0x13u
fun msb(addr: UInt) = addr.shr(8).toUByte()
fun lsb(addr: UInt) = addr.and(255u).toUByte()
fun offset(addr: UInt) = (addr-loadAddr).toInt()+2
// PRG header
bin[0] shouldBe lsb(loadAddr)
bin[1] shouldBe msb(loadAddr)
// first jump table entry; a JMP to start() inserted by prog8 itself
bin[2] shouldBe 0x4cu // JMP
bin[3] shouldBe lsb(startAddr)
bin[4] shouldBe msb(startAddr)
bin[5] shouldBe 0x00u // 0 padding
// second jump table entry (first array element!)
bin[6] shouldBe 0x4cu // JMP
bin[7] shouldBe lsb(func1Addr)
bin[8] shouldBe msb(func1Addr)
bin[9] shouldBe 0x00u // 0 padding
// third jump table entry (second array element!)
bin[10] shouldBe 0x4cu // JMP
bin[11] shouldBe lsb(func2Addr)
bin[12] shouldBe msb(func2Addr)
bin[13] shouldNotBe 0x00u // no more padding after jump table
// check start() contents
bin[offset(startAddr)] shouldBe 0x20u // JSR (to clear_bss)
bin[offset(startAddr)+3] shouldBe 0xeau // NOP
bin[offset(startAddr)+4] shouldBe 0x60u // RTS
// check func1
bin[offset(func1Addr)] shouldBe 0xe6u // INC
// check func2
bin[offset(func2Addr)] shouldBe 0xC6u // DEC
}
})

125
docs/source/binlibrary.rst Normal file
View File

@@ -0,0 +1,125 @@
.. _loadable_library:
=========================
Binary Loadable Libraries
=========================
**also called 'Library Blobs'.**
Prog8 allows you to create binary library files that contain routines callable by other programs.
Those programs can be written in Prog8, BASIC, or something else. They just LOAD the binary library
file into memory, and call the routines.
An example of a library file loaded in BASIC on the Commander X16:
.. image:: _static/x16library.png
:align: center
Requirements
^^^^^^^^^^^^
Such a loadable library has to adhere to a few rules:
It can't use zero page variables
Otherwise it might overwrite variables being used by the calling program.
For systems that have the 16 'virtual registers' cx16.r0-r15 in zero page:
these 16 words are free to use. For other systems, only the internal prog8
zeropage scratch variables can be used.
*note: this may be improved upon in a future version*
No system initialization and startup code
The library cannot perform any regular "system initialization" that normal
Prog8 programs usually perform (such as resetting the IO registers, clearing the screen,
changing the colors, and other initialization logic). This would disturb the
state of the calling program! The library can (must) assume that that the calling
program has already done all required initialization.
Variable initialization
The library still has to initialize any variables it might use and clear
uninitialized "BSS" variables! Otherwise the code will not run predictably as prog8 code.
So, the library must still have a "start" entrypoint subroutine like any outher prog8 program,
that must be called before any other library routine can be called.
Binary output and loaded into a fixed memory address
The library must not have a launcher such as a BASIC SYS command, because
it is not ran like a normal program.
Also, because it is not possible to create position independent code with prog8,
a fixed load address has to be decided on and the library must be compiled
with that address as the load address. For convenience (and compatibility with older CBM
target machines such as the C64 and C128) it's easiest if the resulting library
program includes a PRG load header: 2 bytes at the start of the library that contain
the load address. This allows BASIC to load the library via a simple ``LOAD "LIB.BIN",8,1`` for example.
``%output library``
^^^^^^^^^^^^^^^^^^^
Most (but not all) of the above requirements can be fulfilled by setting various directives in your
source code such as %launcher, %zeropage and so on. But there is a single directive that does it correctly for you in one go
(and makes sure there won't be any initialization code left at all): ``%output library``
Together with ``%address`` and possibly ``%memtop`` -to tell the compiler what the load address of the library should be-
it will create a "library.bin" file that fulfills the requirements of a loadable binary library program as listed above.
The entrypoint (= the start subroutine) that must be called to initialize the variables,
will be the very first thing at the beginning of the library.
Jump table
^^^^^^^^^^
For ease of use, libraries should probably have a fixed "jump table" where the offsets of the
library routines stay the same across different versions of the library. Without needing new syntax,
there's a trick in Prog8 that you can use to build such a jumptable:
add a non-splitted word array at the top of the library main block that contains JMP instructions
and the addresses of the individual library subroutines. Do NOT change the order of the subroutines
in this table!
Also note that the Prog8 compiler will insert a single JMP instruction at the very start of the library,
that jumps to the start subroutine (= the entrypoint of the library program).
Users of the library need to call this to initialize the variables, so it is a required part of the
external interface of the library.
Because the compiler will place the global word array jumptable immediately after this JMP instruction,
it seems as if the very first entry in the jump table is the jump to the start routine.
Look at the generated assembly code to see exactly what is going on.
But the users of the library are none the wiser and it just seems as if it is part of the jump table in a natural way :-)
Here is the small example library that was used in the example at the beginning of this chapter::
%address $A000
%memtop $C000
%output library
%import textio
main {
; Create a jump table as first thing in the library.
uword[] @shared @nosplit jumptable = [
; NOTE: the compiler has inserted a single JMP instruction at the start
; of the 'main' block, that jumps to the start() routine.
; This is convenient because the rest of the jump table simply follows it,
; making the first jump neatly be the required initialization routine
; for the library (initializing variables and BSS region).
; Btw, $4c = opcode for JMP.
$4c00, &library.func1,
$4c00, &library.func2,
]
sub start() {
; has to be here for initialization
txt.print("lib initialized\n")
}
}
library {
sub func1() {
txt.print("lib func 1\n")
}
sub func2() {
txt.print("lib func 2\n")
}
}

View File

@@ -220,6 +220,7 @@ Look in the `syntax-files <https://github.com/irmen/prog8/tree/master/syntax-fil
compiling.rst
programming.rst
variables.rst
binlibrary.rst
libraries.rst
targetsystem.rst
technical.rst

View File

@@ -418,6 +418,8 @@ Directives
- type ``raw`` : no header at all, just the raw machine code data
- type ``prg`` : C64 program (with load address header)
- type ``xex`` : Atari xex program
- type ``library`` : loadable library file (with CBM style load address header) See :ref:`loadable_library`.
.. data:: %zeropage <style>

View File

@@ -5,14 +5,8 @@ TODO
%launcher none
%option no_sysinit
%zeropage dontuse
You still have to set %address and %memtop yourself to tell prog8 where the library is meant to be placed in memory
TODO: it's now a raw binary file, should it include the 2-byte PRG header for easy loading from basic as well perhaps?
- Compiling Libraries: improve ability to create library files in prog8; for instance there's still stuff injected into the start of the start() routine (see translateSubroutine function)
AND there is separate setup logic going on before calling it. Make up our mind!
Maybe all setup does need to be put into start() ? because the program cannot function correctly when the variables aren't initialized properly bss is not cleared etc. etc.
Need to add some way to generate a stable jump table at a given address.
Library must not include prog8_program_start stuff either. Must not require 'start' entrypoint either? Although they need some initialization entry point?
You still have to set %address and %memtop yourself to tell prog8 where the library is meant to be placed in memory.
Result is a .bin file including the 2-byte PRG header that allows you to load it with ,8,1 from basic even on the C64.
- Make some of the target machine config externally configurable (for 1 new target, the existing ones should stay as they are for the time being)

View File

@@ -14,14 +14,12 @@ main {
; btw, $4c = opcode for JMP.
$4c00, &library.func1,
$4c00, &library.func2,
$4c00, &library.func3,
]
sub start() {
; has to be here for initialization
txt.print("lib initialized\n")
}
}
@@ -33,8 +31,4 @@ library {
sub func2() {
txt.print("lib func 2\n")
}
sub func3() {
txt.print("lib func 3\n")
}
}