Jump to content

How to make library modules for CC65/CA65


ZeroByte
 Share

Recommended Posts

Making libraries in CA65 is quite straightforward, but your code needs to be configured properly for use in "multi-file" projects.

TL;DR:
I've posted a reply below with a simple set of example code and build instructions if you want to "just do it" without any further explanations.

The basics:

Creating a library is essentially just packaging up .o files into a library file. Therefore, your library routines should be written so that they would work as independent source files in any other multi-source-file project. For this howto, let's suppose you want to create a library routine "helloworld" which just uses CHROUT to print "hello world." First, let's consider how you would use this routine if it were written in a stand-alone source file.

First, you would create a file "hello.asm" which defines the routine and imports/exports the necessary symbols to perform its task and be reachable from the rest of the project. (more on symbol export/import later). With your routine in its own file, you build your project by listing all source files on the command line:
cl65 -t cx16 -o MYPROG.PRG myprog.asm hello.asm

Assuming that these assembly files know how to properly import and export the required symbols, this command will build MYPROG.PRG which can now be run. Ostensibly, somewhere in main.asm, there is a call to "helloworld" which prints that string to the screen. This command works pretty much the same for C projects:

cl65 -t cx16 -o MYPROG.PRG main.c hello.c

Of course, you can mix and match - main.c can use assembly code from assembly sources, e.g: (all-in-one method)

cl65 -t cx16 -o MYPROG.PRG myprog.c hello.asm

Keep in mind that cl65 is sort of a front-end batch processor command that performs several sub-steps in order to assemble and link your program.

Step-by-step building:

The reason you can easily mix C and assembly is that behind the scenes, cl65 first compiles/assembles each source file into an object file (.o) An object file is as close as the compiler or assembler can get to the final binary machine code of your program. The one thing that's missing during this step is the final resolution of symbols into the actual memory addresses based on where the program is set up to be loaded in memory. If "helloworld" is built at the beginning of your program, it will be in a different location in memory than it would if it were appended to the end of your program. Thus, it's not known just yet exactly what address to use for any JSR / JMP statements calling your code. Thus the symbol "helloworld:" remains unresolved.

The final step of resolving these symbols is done by the linker (ld65). It puts all of the pieces of object code together into the final program. Since the linker is what decides the final locations where everything is actually going to go, it has enough information to turn these remaining unresolved symbols into their final actual memory addresses. It does this, combines all of the object code together, and writes out the .PRG file which can now be loaded into memory and run on the X16.

Before, we performed all of these steps at once. However, you can tell cl65 to skip the linking phase on your project and do only the compiling/assembling. You do this by adding the -c command-line switch. This will cause cl65 to stop short of calling the linker to create a complete executable program. Instead, it will simply create the object files from your sources.

cl65 -t cx16 -c myprog.c
cl65 -t cx16 -c hello.asm

Running these two commands produces the files main.o and hello.o - note that you need to run them as separate commands - once for each source file.

You can then finish your build just as if these files were the source files: (precompiled method)
cl65 -t cx16 -o MYPROG.PRG myprog.o hello.o

You could even pre-assemble the helloworld module, but compile directly from source on the main .c file: (hybrid method)

cl65 -t cx16 -c hello.asm
cl65 -t cx16 -o MYPROG.PRG myprog.c hello.o

All of these methods will produce the same executable program MYPROG.PRG - the second and third methods just take multiple steps. These "granular" methods are useful in larger projects. If you compile your sources into object code, then you don't need to recompile everything whenever you only make changes to one source file. Suppose hello.asm did not change, but you made changes to myprog.c - you would recompile myprog.c as above in the (precompiled method), or using the hybrid method, but you would not need to reassemble hello.o as it has not changed. Make is designed to work with this mode of project building. Make will go through and determine which .o files are older than their source files, and only recompile those components.

If your project had more auxilliary functions such as "goodbyeworld" and "waitforkey" in their own source files, you would pre-compile them and then add their filenames to the final build command:

cl65 -t cx16 -o MYPROG.PRG myprog.c hello.o goodbye.o waitkey.o

Now we get to libraries:

A library file is simply an archive of .o files all packed together into a single file. Instead of having a bunch of .o files laying around in a directory, you simply have one library file, e.g. "mytools.lib" which includes routines like helloworld: from hello.asm, etc.

So this means that to create it, all you need to do is first compile or assemble your sources into object files, and then add the object files to your library archive using the archive tool ar65.

ar65 a mytools.lib hello.o goodbye.o waitkey.o

Note that these must be object files, not the uncompiled sources. Now, you no longer need to specify all of your individual source files:

cl65 -t cx16 -o MYPROG.PRG myprog.c hello.o goodbye.o waitkey.o

Instead, you can just reference the library file:

cl65 -t cx16 -o MYPROG.PRG myprog.c mytools.lib

ld65 will go through the library archive when it needs to resolve symbols such as the names of your functions and variables defined in the archive.

What about the symbols?

ld65 is able to find the symbols in the archive and use them for any code that needs them. However, the archive only contains the symbols that the sources told the compiler to export. Export means to make this symbol available from outside of this particular source file. If you do not export "helloworld" then that symbol can't be called from myprog.prg.

C vs Assembly symbols: The cc65/ca65 suite uses the convention that any symbols in C will be presented to assembly with an underscore _ prepended to them. Thus a variable "foo" in C would be seen as the symbol "_foo" in assembly. Likewise, any symbols with leading underscores in assembly will be presented in C with the leading underscore removed.

Thus, your helloworld function could also have another symbol: "_helloworld" which points to the same code. Doing this, you may now call "helloworld()" from C just as you would from assembly.

In myprog.c, you would declare it as follows:
"extern void helloworld();"

That's all you need to do to access a simple function like helloworld: that was actually written in assembly.

Had helloworld been written in C, then you could still access it from assembly projects:

.import _helloworld
...
jsr _helloworld

If you would like to be able to refer to the same function as helloworld: in assembly and as helloworld() in C, you can simply create a second symbol using the := syntax in your assembly module, and export both symbols:

.export _helloworld
.export helloworld

_helloworld := helloworld

.proc helloworld: near
  ;code
.endproc

Variables:

Lastly, I should point out that importing and exporting variables works in exactly the same way. Just remember that "variables" and "types" don't exist in assembly, at least not in the sense that they do in C. In assembly, everything is just a reference to an address. So if you reserve some space and give it a symbol name in assembly, and export that symbol, this is enough to pass it between C and assembly. (the _ rules still apply, obviously)

.export foo
.export _foo

.segment "BSS"
foo:  .res 2 ; reserve 2 bytes of memory for a 16-bit value, foo
_foo := foo ; alias it as _foo for importing into C projects

In case you're wondering, BSS segment is for uninitialized variables. I.e.: no values are emitted into the binary created by the build process. It is expected that programs will write values into this memory during run time. Basically, it just reserves memory space without adding size to the actual code. Other segments where you might put data:

RODATA : for constants such as strings, lookup tables, etc.

.segment "RODATA"
message: .asciiz "hello world"

DATA : for initialized variables:

.segment "DATA"
bar:  .word  $1000

Any concept of type is defined in C, as assembly just doesn't care. In C, you would define foo: "extern uint16_t foo;" or if it's a pointer of some sort, it could be "extern char* foo;" etc.

Nutshell:

 

  1. Create the sources for your library just as any other components of a project
  2. Compile/assemble them as object files
    • cl65 -t cx16 -s hello.asm
  3. Combine the objects into a single archive
    • ar65 a mytools.lib hello.o
  4. Create an include file for easy access to the required symbols (optional but recommended)
    • .import helloworld  (assembly .inc style)
    • extern void helloworld();  (C .h style - be sure to export _helloworld if the source is assembly)

Conclusion:

These are just the basics, obviously. If you need to pass parameters between C and assembly, you should consult the cc65 documentation for more details on how that's done.

While this howto is written as a mixed assembly / C example, there is no reason that your libraries need to be written in assembly.
You could have some complicated routines, such as trig.c which creates SIN() COS() and TAN() functions, written entirely in C.
Just build trig.c into trig.o and add trig.o to your archive, and it will work. The main thing to remember is to keep the underscores in mind if you're mixing assembly. If pure C, don't worry about it.

 

I hope the community finds this helpful. Cheers!

Edited by ZeroByte
.import / .export do not use quotes around the symbol names (fixed examples)
  • Like 2
  • Thanks 1
Link to comment
Share on other sites

A little more about what's going on:

Let's unpack this C statement:
"extern void helloworld();"

What you've done is told the compiler "okay, so there's this function helloworld. It doesn't return anything, and it needs no parameters. Furthermore, I'm not going to define it here. It will come from some other source. (that's what the extern means). So - this is enough information for the compiler to work with. When it compiles this file, anywhere that it sees a call to helloworld(), it knows that all it needs to do is compile it to the assembly code: "JSR _helloworld" which is then assembled as "$20 {_helloworld}". That's what goes in the .o file - the opcode for JSR ($20) and a marker "some symbol from somewhere - linker, please fill in this blank when you do your thing."

Basically, you're telling the compiler - here's how to use it. Do as much as you can. Someone else will come along and supply that code later. The compiler then trusts this and gets as far as it can go, leaving that {_helloworld} as filler.

Later, the linker sees this mark, and then adds _helloworld to the list of symbols needed. As it goes through all of the .o files, whenever it sees some file saying "I have this _helloworld symbol in case anyone needs it" - that symbol gets plugged in. (This is what .export does in assembly - it tells the linker "if anyone wants to use my symbol _helloworld, give them my address please."

What happens if you fail to supply the code as promised? Say you forget to put "mytools.lib" in your list of files for cl65. What will happen is that ld65 will get to the end of the list and discover that nothing ever said what this _helloworld symbol actually is. It won't be able to build the executable because it can't resolve that symbol. It will throw an error: "Unresolved symbol _helloworld" and quit. Unresolved symbol errors typically mean that either you forgot to put all of your project's required modules into the cl65 command's list of files, or else cl65 can't find the file - for instance let's say mytools.lib is not in this project's directory, but somewhere else. If you didn't specify a complete path to the library, cl65 won't be able to find it, and will throw an error - and that one's usually pretty easy to spot as it will say that it can't find file "mytools.lib" - copy it into the current directory, specify a full path to it (e.g. ../mylibs/mytools.lib) or else add its directory to the build's search path using the -L command line option "-L ../mylibs"

 

  • Like 1
Link to comment
Share on other sites

Example code:

mytools.h:
 

Quote

extern void helloworld();

myprog.c:
 

Quote

#include "mytools.h"

int main() {
  helloworld();
  return 0;
}


hello.asm:

Quote

.export _helloworld ; make this function available to C programs
.export helloworld ; make this function available to other assembly programs

.segment "CODE"

_helloworld := helloworld ; make _helloworld be the same address as helloworld for C compatibility
CHROUT := $FFD2 ; kernal routine to print a character to the screen

.proc helloworld: near ; create a scope so local symbols are hidden from the rest of the project
   ldx #0
nextchar:
   lda str_hello
   beq done ; check for null character marking end-of-string
   jsr CHROUT
   inx
   bra nextchar
done:
   rts
.endproc

.segment "RODATA"
; place the actual string data into the "RODATA" region of memory
; this symbol is NOT exported, so programs will not be able to refer to it directly.
; this module, though, knows the symbol since it's in the same file, so it can be
; referenced here and printed out to the screen.
str_hello: .asciiz "hello world!"
 

Building the library:
cl65 -t cx16 -c hello.asm
ca65 a mytools.lib hello.o

Using the library:
cl65 -t cx16 -o MYPROG.PRG myprog.c mytoools.lib

Edited by ZeroByte
removed quote marks from .import and .export
  • Thanks 1
Link to comment
Share on other sites

Thanks so much! This is a very good explanation.

For information on how to write C-callable functions with parameters and return values in assembly, the reader can refer to https://cc65.github.io/doc/cc65-intern.html

I have a general question: can I use zero page variables for my library at the same time as C code is running? Is it sufficient to declare my zero page variables in a ".zeropage" segment, and the linker will sort everything out?

Edited by kliepatsch
  • Like 1
Link to comment
Share on other sites

I've got some ZP variables defined in my zsound library project, and they go to the right place when I use the library in an assembly project. I assume that C would do the same, as under the hood, it first compiles to assembly, and then assembles to object code.

Modules within the library need to use .segment "ZEROPAGE" before .import "ZPvariable" - if you don't specify Zeropage segment, the assembler throws a warning that while the address will be in zeropage, the assembler will use absolute addressing modes to use them. I don't import those variables in the client code (they're not even listed in zsound.inc) but I presume the same thing would apply there. Thus, C probably has the same constraints. Not sure if "extern uint8_t ZPvariable;" would honor the ZP or not. I suspect that a global "register uint8_t ZPvariable" would try to EXPORT a ZP symbol.

I'll try messing around to see what results I get.

  • Thanks 1
Link to comment
Share on other sites

Posted (edited)

One thing that's missing from this howto is alluded to by @kliepatsch:

Parameter passing / return values.

cc65 has "regular mode" and "__fastcall__ mode." For regular mode, C will push all parameters onto the stack (C's stack, not the CPU stack) in order from left to right and then jump into the subroutine. Fastcall is the same as regular, except that the rightmost parameter is passed in via the .A and .X registers for 8/16 bit values, and for a 32bit parameter, it places the upper two bytes in a temporary ZP space called sreg. To access sreg, just .import sreg in your assembly file.

Your assembly program can import SP which is a zeropage pointer to the current top of the stack. Use it as with any other ZP pointer - with (ZP),y for offsets into the stack. It is your code's responsibility to remove the parameters from the stack prior to returning. Otherwise, you will create a memory leak. You can accomplish this by either incrementing ZP to point above the parameters passed in, or else use one of the various utility functions in cc65 such as popa, popax, etc. I currently prefer to use popa / popax as they include some code to determine whether the top-of-stack has an 8bit value that's been promoted to 16bit size, etc.

Return values are similar - .AX returns the lower 16 bits of the return value. If returning an 8bit value, it is promoted to 16bit anyway, so you will need to have a zero in .X or you will get strange results, especially if returning a signed value. If the return value is 32bits, the upper two bytes are returned in sreg.

Example:

in C :
extern char __fastcall__ max(char b, char a);

in Assembly:

.import popa
.export _max
.segment "RSS"
tmp: .res 1 ; create TMP space for the char a parameter.
_max:
  STA tmp ; store parameter "char a" in tmp
  JSR popa ; popa clobbers .Y but leaves .X alone
  ; the stack has now been cleared of the passed-in parameters.
  LDX #0 ; clear the upper 8 bits of the return value, as we are returning a char.
  CMP tmp
  BCS exit   ; .A >= tmp - return .A
  LDA tmp  ; tmp > .A return tmp
exit:
  RTS

Of course this could be done by comparing directly with the parameter b still remaining on the stack and not using a temp location, but this example is meant to be more straightforward and illustrate the use of popa to clear the stack.

If your library's routine is written primarily for use by assembly and has other calling conventions, then you can do both at the same time by way of the fact that C and assembly have different symbols.

.export foo
.export _foo

foo:
  ... ; main code for the routine - use whatever parameter passing methodology you want
  rts

_foo:
  ... ; pop the C parameters off the stack and arrange them in whatever manner is required for assembly call to foo
  jmp foo

Of course, if foo also returns values in a manner inconsistent with C's return mechanism (.AXsreg), you would not JMP into foo but JSR and then convert the return into C's format. Suppose foo returns .C set = fail, clear = success. You must store this bit in .AX in order to return that to the caller in C. Furthermore, you may wish to present foo() to C such that it returns true on success and false on error. Here is an example:
_foo:
  ...
  jsr foo ; returns .C set = fail, clear = success
  ldx #0
  lda #0
  rol A ; move .C into bit0 of return value
  eor #1 ; invert logic so true=success, false=fail
  rts

I'm still learning about this myself, so I don't have any examples yet for going the other way - calling a C routine from assembly, but the basis would be to place the arguments onto the stack from left to right and then JSR _function. (don't forget to .import _function) If the C function happens to be defined as a fastcall, you would pass the rightmost parameter with .AX and sreg as required.

One final note - C handles preservation of the registers, so you do not need to worry about whether your code clobbers them or not. C assumes you'll be clobbering everything.

Edited by ZeroByte
  • Like 1
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

×
×
  • Create New...

Important Information

Please review our Terms of Use