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
  • 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
  • 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

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