1
0
mirror of https://gitlab.com/camelot/kickc.git synced 2024-09-29 03:56:15 +00:00
kickc/manual/asm_libraries.md
Sven Van de Velde 30e6aff441 - Documentation for .asm libraries fixes.
- Asm library code debug fixes.
- Added test programs as explained in the documentation.
2023-12-15 14:51:18 +01:00

19 KiB

2.11 .ASM Libraries

KickC has now a new facility that allows to create pre-compiled assembler libraries, with flexible export facilities, and use these libraries with easy import facilities. Throughout this section, we will refer to these libraries as .asm libraries, as the artefacts of libraries are actually kick assembler (kickass) .asm source files.

Consider the following scenario, where we want to create an .asm library of flight functions:

graph TD
    subgraph flight_lib.c
        asm_library["
            #pragma asm_library
            #pragma calling(__varcall)
            "]
        asm_export["
            #pragma asm_export(flight_init)
            #pragma asm_export(flight_route)
            #pragma asm_export(flight_direction)
           "]
        code[["void flight_init(char flightno) { ... }
            void flight_route(char flightno, int direction) { ... }
            void flight_direction(char flightno, signed char distance) { ... }"]]
        asm_library-.->asm_export-.->code
    end
    compile_flight(("
        COMPILE 
        flight_lib.c
    "))
    subgraph flight_lib_asm.h
       prototypes["
           external void __asm_import('flight_lib') __varcall __zp_reserve(...) 
           flight_init(char __zp(...) flightno);
           external void __asm_import('flight_lib') __varcall __zp_reserve(...) 
           flight_route(char __zp(...) flightno, int __zp(...) direction);
           external void __asm_import('flight_lib') __varcall __zp_reserve(...)
           flight_direction(char __zp(...) flightno, signed char __zp(...) distance);
       "]
    end
    subgraph flight_lib.asm
       assembler_flight["
           .namespace flight_lib {
               ...
           }
       "]
    end
    subgraph plane.asm
        assembler_plane["
            #import flight_lib.asm
        "]
    end
    flight_lib.c-->compile_flight
    compile_flight--.asm libary-->flight_lib.asm
    compile_flight--_asm.h header file-->flight_lib_asm.h
    flight_lib_asm.h-.imported.->plane.c
    
    subgraph plane.c
        include["#include flight_lib_asm.h"]
        main[["int main() {
            flight_init( 2 );
            flight_route( 2, 10 );
            flight_direction( 2, 2000 ); }
        "]]     
        include-.->main
    end

    subgraph plane.prg
        binary_plane["03 05 FF ..."]
    end
    compile_plane(("
        COMPILE
        plane.c
    "))
    assemble_plane(("
        ASSEMBLE
        plane.asm
    "))

    plane.c-->compile_plane
    compile_plane-->plane.asm
    plane.asm-->assemble_plane
    flight_lib.asm-->assemble_plane
    assemble_plane-->plane.prg
    flight_lib.asm-.imported.->plane.asm
    flight_lib_asm.h --> compile_plane

The #pragma asm_library specifies that flight_lib.c is a library and can be compiled separately, generating 2 output files: a flight_lib.asm file (the .asm library) and flight_lib_asm.h (the _asm.h C header) that contains specific C function prototypes to be used by the main C program. The flight_lib_asm.h contains prototypes for the 3 flight functions, with .asm library directives to tell kickc that these functions are pre-compiled in flight_lib.asm.
Next, we use the .asm library in our main plane.c program, by #include-ing the generated header file flight_lib_asm.h into plane.c. When we compile plane.c, it will create a plane.asm that imports flight_lib.asm, which is then together assembled to generate plane.prg!

In other words, for flight_lib there won't be any compilation done, just an import of already pre-compiled code!

Using .asm libraries allows you to speed up significantly the compilation time for your growing program, and to efficiently re-use pre-compiled code over many programs (set standards). However, using .asm libraries has specific consequences, like code size, segmentation, optimization, and more ..., which is explained further in this section.

The idea of libraries is that you first export your C functions to <library>.asm files, which are meant to contain small and independent pieces of pre-compiled functional logic, and then import those pre-compiled functions in other C main programs.

  • export : C functions are exported by compiling a library C source file, which outputs a <library>.asm file in the output directory location. The base name of the .asm library will be the base name of the C source file. This library C source file must start with the #pragma asm_library pre-processor command, to indicate to the compiler that the output of the compilation is an .asm library file. On top, the compilation will generate a <library>_asm.h file that contains C function prototypes for import.

  • import : .asm library functions are imported by #include-ding the generated <library>_asm.h file, which declares the specific protoypes for the pre-compiled functions in your main C program and the .asm library file will automaticlaly be included during compilation.

2.11.1 Consequences and dependencies using libraries

Because .asm libraries already contain pre-compiled code, important limitations and design decisions are to be taken by the programmer. .Asm library functions are pre-compiled through the KICKC interface mechanisms, using a standard calling convention, passing sequence of function parameters and return value allocated either on CPU registers A or X or Y, or through zero-page memory, or through main memory.

The following attention points are to be considered:

  1. "Less" optimized assembler: Your resulting assembler using pre-compiled code in libraries will be less optimized by KickC (through SSA optimization), compared to compiling all your source in one compilation run. KickC cannot optimize library functions accross the interface boundaries, as the pre-compiled assembler has the interface of such functions already defined in assembler level. The resulting assembler will be more generic, and as a consequence, less-optimized. However, the assembler code that is generated "within" the library, will be fully optimized, as long as library function interface boundaries are not crossed.

  2. Zero-page allocation: There is a high likelihood that your .asm library will have to consume 6502 zero-page registers within the .asm library. Because these zero page allocations are pre-compiled or statically defined, it is important that your C program indicates the zero-page usage by the library functions when importing! This is to ensure that the zero-page used by the .asm library functions are not conflicting with the zero-page usage of your main program. In practice, there is limited chance of zero-page conflicts, as long as libraries contain functions that are atomic (see point 4).

  3. Zero-page coalesce: Related to point 2, any zero-page usage in your .asm library will consume fixed zero-page memory space in your KickC program. The compiler however will further coalesce zero-page of your program aligning the zero-page usage of the .asm library as long as there is no memory overlap, depending on the call-graph. However, in practice, it is very unlikely that zero-page will overlap, as .asm libraries are self contained.

  4. Limit function dependencies: Ensure that the C functions being exported into an .asm library are as much as possible self contained. Exporting library functions with lots of call dependencies to other library code, result in all this code included in your library, resulting in code overhead and possibly even duplicate code! And this is the main difference compared to normal C style .o (object) or .lib (library) files linkage. Your .asm libraries contains all the code required to run the functionality of the library functions and is self-contained. So ensure that when exporting functions into .asm libraries, that no extra or unexpected code or logic is exported, which can be a result of call dependencies. It is good practice to inspect the generated assembler in your .asm libraries when exporting C functions into .asm libraries, to verify if there aren't any other embedded and unwanted functions within your .asm library. But be aware that in some cases such cannot be avoided ...

TIP: Avoid to use any conio, stdio or printf statements in your .asm libraries. Good candidates for .asm libraries are functions for linked lists, heap management, cache management, calculations, algorithms, to build a base library of functions that are ready to be imported, and are stable.

2.11.2 Exporting C functions to .asm libraries

When creating your first exported <library>.asm file, it is best to start small. Define a small separate C program, that contains the functions to be exported in the .asm library, and use the new export facilities. Let us explore in-depth the C function export facilities, followed by some examples.

If you want all the C functions to be exported, then specifying #pragma asm_library at the start of the C source file suffices. Optionally, you can specify that the base name of the .asm library shall be different than the base name of the libary C source file, by specifying in the #pragma asm_library("\<library\>") pre-processor command between the name of the library as a string between parentheses.

There are two different but complementary facilities to indicate the compiler which C functions within your C program should be exported to the .asm library, by using either a #pragma asm_export pre-processor command or by using an __asm_export function directive.

IMPORTANT! It is not mandatory to specify which C functions are to be exported. When no #pragma asm_export pre-processor command or an __asm_export function directive is specified in your library C program, then ALL C functions will be exported. That also means that all C functions that are #include-ed through header files (and .c files) are ALSO added to the .asm library!!!

TIP! By carefully specifying the "interface" of your .asm library, that is, to export only those C functions that are to be used by other main C programs and keep the rest internal, allows for very efficient compilation of your .asm library! By using the register uplift of KICKC, your .asm library will utilize where possible the CPU registers for internal .asm library function calls, and will result in a severely reduced zero-page memory usage footprint!

2.11.2.1 Pre-processor command #pragma asm_export

#pragma asm_library [ ("<library>") ]    

#pragma asm_export ( <foo>, <foo>, ... )
  • #pragma asm_library : indicates that the C source file will result in an .asm library output file with the same base name as the C source file. Optionally, an other library name can be given [ ("<library>") ], when specifying the library name as a string between parentheses.

  • #pragma asm_export : indicates in your C program which functions are to be exported.

    • <foo> - The function name(s) to be exported, comma separated.

Consider again the library C source file called flight_lib.c, as introduced earlier as an example:

#pragma asm_library
#pragma asm_export(flight_init)
#pragma asm_export(flight_route, flight_progress)

#include "math.h"

void __stackcall flight_init(char flightno);
void __varcall flight_route(char flightno, int direction);
void __varcall flight_direction(char flightno, signed char distance);

void __varcall flight_internal...;

Creates after compilation a flight_lib.asm file, containing 3 exported functions flight_init, flight_route and flight_direction. The flight_init is exported using the 6502 CPU stack call calling convention while flight_route and flight_distance are exported using the zero-page var call calling convention. Only these 3 functions specify the library interface, all the other flight_internal functions are not exported, and are optimized by the compiler when the option register uplifting is used, reducing severely the zero-page footprint of the .asm library.

When the #pragma asm_library would be specified as:

#pragma asm_library("helicopter")

then the resulting base name of the .asm library file and _asm.h files would be helicopter.asm and helicopter_asm.h.

Alternatively, in order to reduce completely the zero-page footprint, you can choose to implement the library using full main memory load/store interface, both for the input/output parameters, return value and local variables within each function. For this, please refer to the #pragma var_model pre-processor command, but find here an example:

#pragma asm_library
#pragma var_model(local_mem, parameter_mem)

The #pragma var_model(local_mem, parameter_mem) pre-processor command instructs the compiler to store all C function local variables and parameters (return value is also a local parameter) into main memory. That being said, C functions can still use zero-page, for assembler instructions where zero-page is mandatory to be used (like when using pointers).

An other attention point to consider is the calling convention. Please use the #pragma calling( __varcall | __stackcall | __phicall ) to declare your C functions to use the indicated calling convention.

#pragma asm_library
#pragma asm_export(flight_init)
#pragma asm_export(flight_route, flight_progress)

#include "math.h"

#pragma calling(__stackcall)
void flight_init(char flightno);

#pragma calling(__varcall)
void flight_route(char flightno, int direction);
char flight_direction(char flightno, signed char distance);

#pragma calling(__phicall)
void flight_internal...;

The #pragma calling specifies which functions are to be compiled using the indicated calling convention.

2.11.2.2 Function directive __asm_export

#pragma asm_library [ ("<library>") ]

<type> __asm_export [ __varcall | __stack_call | __phi_call ] foo();

A function declaration can use the __asm_export directive to indicate that the function is exported to the .asm library, accompanied with the normal other function directives:

  • #pragma asm_library : indicates that the C source file will result in an .asm library output file with the same base name as the C source file. Optionally, an other library name can be given [ ("<library>") ], when specifying the library name as a string between parentheses.

  • __asm_export : indicates in your C function is exported.

  • <foo> : The function.

  • [ __varcall | __stack_call | __phi_call ] : Optionally the calling convention specified.

Consider again the library C source file called flight_lib.c, as introduced earlier as an example:

#pragma asm_library

void __asm_export __stackcall flight_init(char flightno);
void __asm_export __varcall flight_route(char flightno, int direction);
char __asm_export __varcall flight_direction(char flightno, signed char distance);

void __phicall flight_internal...;

Creates after compilation a flight_lib.asm file, containing 3 exported functions flight_init, flight_route and flight_direction. The flight_init is exported using the 6502 CPU stack call calling convention while flight_route and flight_distance are exported using the zero-page var call calling convention. Only these 3 functions specify the library interface, all the other flight_internal functions are not exported, and are optimized by the compiler when the option register uplifting is used, reducing severely the zero-page footprint of the .asm library. A new flight_lib_asm.h file will also be created, and is to be used when using the flight_lib.asm library into your main program.

TIP! The difference between the two C function export facilities is that using the #pragma __asm_export pre-processor command allows to also export C functions that are already defined within standard KickC C libraries, which are included in your program using the #include pre-processor command.

For example, it is possible to export conio C library functions to an .asm library using the #pragma asm_export pre-processor command. Imagine the conio.c file would contain:

#pragma asm_library

#pragma asm_export(conio_init, cputs, cputsxy)

#include <conio.h>

The above will result in new conio.asm and conio_asm.h files being created.

2.11.3 Import .asm libraries for usage in your C programs.

Once you have your .asm library files created (and _asm.h header files), like flight_lib.asm (see above), you are ready to import those libraries and use them in your main C programs.

The KickC compiler only provides function() directive language facilities to import C functions. This is to enforce the programmer to declare imported .asm functions with its parameter register usage prototypes, which can be through main memory locations, through zeropage registers or through cpu registers.

extern <type> __asm_import("\<library\>") [ __zp_reserved(#, #, ...) ] [__varcall | __stack_call | __phi_call] foo( __zp(#) | __mem | __register(A|X|Y) <type> <parameter>, ...)  

A C function declaration for .asm library import:

  • foo() : Each foo() must be specifically declared with an __asm_import("\<library\>") directive to indicate it is imported from an .asm library.
  • extern : Each imported function must be declared as external, because they don't contain a function definition.
  • <type> : Each function foo() must have a type declaration, following the normal C syntax.
  • [__varcall | __stack_call | __phi_call] : The foo() can be declared using one of the KickC supported calling conventions directives.
  • [ __zp_reserved(#, #, ...) ] : The foo() can have reserved zero-page memory addresses. This indicates to the compiler that these zero-page memory addresses are reserved for the foo().

Parameters:

  • Each <parameter> and its containing <type> must be declared.
  • __zp(#) : Directive specifying to the compiler which zeropage registers are being used for the <parameter>.
  • __register([A,X,Y]) : Directive specifying which CPU register is used for the <parameter>.
  • The __mem : Directive specifying that the parameter is allocated in main memory.

NOTE! As you can see, the usage as a programmer to specify all these directives can become very complicated. Fortunately, KICKC generates for you a \<library\>_asm.h header file that contains all the required directives for each C function exported in the accompanying .asm library!

Let us illustrate with an example for our flight_lib.asm library.

In the previous example(s), a flight_lib_asm.h header file was created by the compiler. It is required that this flight_lib_asm.h file gets imported in your main C program, as this header file contains the external library function declarations and register usage.

// This header file was generated by KickC and contains for each library function:
// - The library import definition
// - Function calling convention used
// - Function parameter memory location, zeropage register or cpu register allocation.

#include "flight_lib_asm.h"

int main() {
  ....
  // pre-compiled function calls
  flight_init(1); 
  flight_route(1, 90); 
  flight_progress(1, 2); 
  ...
}

In practice using .asm libraries is fairly easy, it just requires to practice a bit with it, and gain experience. To repeat, a best practice is to start small, and grow slowly the complexity as you gain experience and deeper understanding of how .asm libraries work.

2.11.4 Example and test code

The test folder in the KickC source code contains a library directory with a concrete examples, demonstrating how to export C functions and create .asm library files and import these .asm library files in your program.