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:
Create the sources for your library just as any other components of a project
Compile/assemble them as object files
cl65 -t cx16 -s hello.asm
Combine the objects into a single archive
ar65 a mytools.lib hello.o
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!