mirror of
https://github.com/irmen/prog8.git
synced 2025-11-09 13:17:14 +00:00
unit test for %output library, and docs.
This commit is contained in:
106
compiler/test/codegeneration/TestLibrary.kt
Normal file
106
compiler/test/codegeneration/TestLibrary.kt
Normal 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
125
docs/source/binlibrary.rst
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -220,6 +220,7 @@ Look in the `syntax-files <https://github.com/irmen/prog8/tree/master/syntax-fil
|
|||||||
compiling.rst
|
compiling.rst
|
||||||
programming.rst
|
programming.rst
|
||||||
variables.rst
|
variables.rst
|
||||||
|
binlibrary.rst
|
||||||
libraries.rst
|
libraries.rst
|
||||||
targetsystem.rst
|
targetsystem.rst
|
||||||
technical.rst
|
technical.rst
|
||||||
|
|||||||
@@ -418,6 +418,8 @@ Directives
|
|||||||
|
|
||||||
- type ``raw`` : no header at all, just the raw machine code data
|
- type ``raw`` : no header at all, just the raw machine code data
|
||||||
- type ``prg`` : C64 program (with load address header)
|
- 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>
|
.. data:: %zeropage <style>
|
||||||
|
|||||||
@@ -5,14 +5,8 @@ TODO
|
|||||||
%launcher none
|
%launcher none
|
||||||
%option no_sysinit
|
%option no_sysinit
|
||||||
%zeropage dontuse
|
%zeropage dontuse
|
||||||
You still have to set %address and %memtop yourself to tell prog8 where the library is meant to be placed in memory
|
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?
|
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.
|
||||||
|
|
||||||
- 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?
|
|
||||||
|
|
||||||
- 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)
|
- 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)
|
||||||
|
|
||||||
|
|||||||
@@ -14,14 +14,12 @@ main {
|
|||||||
; btw, $4c = opcode for JMP.
|
; btw, $4c = opcode for JMP.
|
||||||
$4c00, &library.func1,
|
$4c00, &library.func1,
|
||||||
$4c00, &library.func2,
|
$4c00, &library.func2,
|
||||||
$4c00, &library.func3,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
sub start() {
|
sub start() {
|
||||||
; has to be here for initialization
|
; has to be here for initialization
|
||||||
txt.print("lib initialized\n")
|
txt.print("lib initialized\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -33,8 +31,4 @@ library {
|
|||||||
sub func2() {
|
sub func2() {
|
||||||
txt.print("lib func 2\n")
|
txt.print("lib func 2\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
sub func3() {
|
|
||||||
txt.print("lib func 3\n")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user