mirror of
https://github.com/irmen/prog8.git
synced 2025-11-03 04:17:16 +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
|
||||
programming.rst
|
||||
variables.rst
|
||||
binlibrary.rst
|
||||
libraries.rst
|
||||
targetsystem.rst
|
||||
technical.rst
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user