If you have followed along with the AMOS BASIC series or my STOS tutorials, you already know how to put characters on an Amiga or Atari ST screen. This post shows you how to do the same thing in C, using the vbcc cross-compiler, so we can build up a portable codebase for the larger goal: a retro roguelike that runs on multiple platforms.
Why another “Hello World” post?
▶ Try this lesson in your browser
Run vbcc C “Hello World” in the in-browser Amiga or ST emulator. No SDK install, no disk swapping, just hit Run:
Open Amiga helloworld.c in the IDE → Open Atari ST helloworld.c in the IDE →
Although the end-goal is a full game, and to cover as many machines as possible, we have to start somewhere, and the start with C is very instructive about how the language, and the compiler, work.
Unlike BASIC, or even more modern languages such as Python, PHP, or JavaScript, C needs a bit more boilerplate before it will do much, it gets wordy real quick, especially when talking about retro systems.
Why C?
If C is a bit of a pain to get started with, why use C? People do complain about C being complex, requiring multiple tools to get one result, involving various command-line incantations. It doesn’t even make life easy with the simplest of tasks like working with strings of text.
And yet, C is the right tool for the job. No other language reaches more retro hardware:
8-bit targets are well handled by cc65 (6502 family) and z88dk (Z80 family), almost every 16-bit and 32-bit retro machine has a vbcc target. That same vbcc source compiles for AROS, classic AmigaOS, Atari TOS, and others with only small tweaks.
The four short sections of this mini-series cover the foundational building blocks the roguelike game needs.
This first post just gets one message on screen on our ST and Amiga, so we know how C programs are structured and our compile-and-run loop works. Following articles will deal with each machine individually.
The Hello World code
#include <stdio.h>
int main(void)
{
printf("Hello, World!\n");
return 0;
}
That is the whole program. Six lines, but most of them deserve a paragraph of description.
If you are coming from BASIC, AMOS, or STOS this is going to feel deeply weird, because C makes you say everything explicitly.
Let’s walk through it piece by piece.
What is #include?
#include is a preprocessor directive. Before the compiler sees your code, a tool called the preprocessor reads it top to bottom, and any line that starts with # is an instruction to that tool, not C source code.
#include says “paste the contents of this header file here“. A header is a separate file full of declarations, the names and shapes of functions and types that live in some library you want to call. Without the include, the compiler has never heard of printf and it will throw its robotic hands up in complaint.
You will sometimes hear C people say “include the header” as if it were a noun. They mean exactly this line.
Include with Angled Brackets vs Quotes
The brackets versus quotes versions tells the preprocessor where to look:
- With < and > means “search the compiler’s standard system paths“. This is the form used for libraries shipped with the compiler.
- In quotes means “search the same directory as the file being compiled first“. This is the form used for files that you wrote yourself.
In practice on a small project, both forms can find the same file, but the convention is worth following because larger projects have multiple includes with the same name and the quote form lets your own header win.
The C standard library, and why versions differ
Speaking of libraries that come bundled with the compiler, we should talk about “Standard” libraries.
stdio.h is part of the C standard library, the kit of basic functions every C compiler is supposed to ship: printf, scanf, fopen, malloc, strlen, and friends. The standard library is split into a handful of headers:
stdio.h(Standard IO) for input and outputstdlib.hfor memory and process controlstring.hfor string functionsmath.hfor maths- and so on.
The standard says what each function should do. It does not say how to do it. This is the core of why C is great for building cross-platform systems, especially as originally imagined, operating systems.
Every compiler vendor ships their own implementation. On a modern Linux box you usually get glibc or musl. On Windows you get the Microsoft CRT. On the Amiga and Atari ST you get a libc that vbcc ships, layered on top of OS calls.
Three things to keep in mind:
- Size. Embedded or retro libcs are often trimmed down to fit. A 32K Z80 machine cannot afford a 200K libc, so things get cut.
- Coverage. There is no single ‘C’ – Some libcs implement most of C99, some stop at C89, some have a few C11 bits. If a function is missing, the linker will tell you.
- Performance.
printfis implemented in plain C in some libcs and is famously chonky. On a slow machine you might preferputs, direct character output, or even inline assembler for tight loops. We will not worry about that right now, but it is worth knowing why people sometimes strenuously go to lengths to tell you to “avoid printf” in retro communities, it comes with a lot of potentially unnecessary code.
int main(void) piece by piece
This declaration at the entry point of the program can be intimidating to first time C programmers, but once you get the handle of it everything becomes clear:
intis the return type.mainreturns an integer to whatever launched the program as an error or success code. The shell uses that integer as an exit status, therefore 0 for success, anything else for failure.mainis the name. Every C program has amain. The runtime startup code looks for it right after it has set up the memory stack and global variables.(void)is the function argument parameter list. The wordvoidhere means “no parameters”. You will also see the even weirder lookingint main(int argc, char **argv), which is the form that lets the program read command-line arguments (number of arguments, and the actual text of the arguments). For now we do not need them.- The opening curly bracket
{and the matching closing}mark the body of the function. Everything between them is the codemainactually executes. Coming from BASIC or Python this will look a bit awkward, but a lot of languages adopted this convention so Java, JavaScript, PHP etc programmers will be right at home.
You will see a bunch of variations in the wild:
void main()is common in tutorials from the 1990s. It works on many compilers but is not standard. Some compilers will warn or refuse.main()with no return type is the original K&R style. C used to default the return type toint. Modern standards dropped that default.int main()(empty parens, novoid) is legal C but in older C++ literature means “unspecified arguments” rather than “no arguments”. Stick withint main(void)for safety.
printf and why it’s common
If printf is so bloated, why is it used so much? Well, first of all it is expected to be there to use, but secondly as systems got more powerful and memory less precious, that extra weight wasn’t such a burden. It’s also extremely useful. It does much more than just simply put text on the screen.
printf is the standard library’s formatted output function. Its first argument is a format string that may contain %-prefixed conversion specifiers. Any remaining arguments are values that get substituted for those specifiers.
Want to output a number from a variable, insert a string as part of your text, or insert a nicely formatted date and time into your welcome message? printf can do all that.
| Specifier | Type | Example | Output |
|---|---|---|---|
%d or %i | Signed decimal integer | printf("Score: %d", 5) | Score: 5 |
%f | Decimal floating-point | printf("%f", 3.14) | 3.140000 |
%s | String of characters | printf("Hello, %s", name) | Hello, Chris |
%c | Character | printf("%c", 'A') | A |
%x or %X | Hexadecimal | printf("%x", 255) | ff |
In this particular program there are no specifiers, just a plain string, so printf simply writes the string to standard output.
There are several alternatives to printf worth knowing:
puts("Hello there!");automatically adds a newline. It is smaller and faster thanprintfbecause it does zero formatting.fputs("Hello, again!\n", stdout);is the explicit version ofputswith no automatic newline.write(1, "Hello\n", 6);on POSIX-ish systems goes one level lower, straight to the system call. The Amiga has its owndos.librarycalls that do something similar.
We use printf here mainly because it is the function people see first in every C book and tutorial, and because we will lean on its formatting in later parts of this series.
Strings in C (the next part that puts people off)
C does not really have a string type. Yeah, I know, I know, it’s weird like that.
What it has is an array of characters with a zero byte at the end. The string "Hello, Amiga!\n" is twelve characters of greeting, one newline byte, and one terminating zero (often written \0). The terminating zero is invisible but essential at runtime: every string-handling function in the standard library uses it to know where the string ends.
This is why C strings feel cumbersome compared to BASIC strings:
- To find the length of a string, you have to call
strlen(str), which scans character by character until it finds the zero. - You cannot concatenate two strings with
+. Instead you must callstrcatand pre-allocate a buffer large enough to hold the result. - You can read or write past the end if you are not careful, which is the source of many C foot-gun security bugs.
The good news: for now you only need to know that text within double quotes is a string, and printf("...") will print it.
Newlines: \n, \r, and \r\n
The \n inside the string is an escape sequence, two characters in the source that compile to one character in the resulting string. \n is the newline character (code 10). When printf sends it to the console, the console moves the cursor to the start of the next line.
You will sometimes see \r (carriage return, code 13) or \r\n (CRLF, the pair). Different operating systems standardised on different conventions:
- Unix and modern Linux/macOS use
\n. - Old Mac OS up to version 9 used
\r. - DOS and Windows use
\r\n. - AmigaOS uses
\n. - Atari TOS uses
\r\nin some places but is forgiving.
For console output, vbcc’s libc and the Amiga shell handle \n correctly. When we get to reading files or talking to serial devices in later posts we will need to think harder. For now, \n is the right call.
There are more escape sequences beyond newlines. Something useful to know right away is if you want " to appear within your string, which if you recall is already encapsulated by quotes, you can ‘escape it’, which is to say use \" wherever you want the embedded quote symbol to appear.
getchar(); and the Atari ST console quirk
You might spot in the IDE we have added printf("Press Enter to exit...\n"); and getchar(); to the Atari ST version of the code. If we let main return immediately, GEM (the Atari ST’s window manager) closes the TOS console window instantly, and the greeting vanishes before you can read it.
getchar is the standard library function that reads one character from standard input. It blocks execution until the user types something and presses Enter. By calling it after the message, we keep the console open until the user is ready to dismiss it.
You will see this pattern again in the following Atari ST posts because every one of them prints output that we want to read before exiting. On a modern desktop you would not bother as the shell window would likely stay open by itself.
return 0;
The final statement returns 0 to whoever launched the program. By convention, 0 means “success” and any other value means “something went wrong”. A shell script can read that value with $? (Unix) or check errno style variables on the Amiga.
How the Retro IDE works
When you click ‘Run’ in the IDE:
- The browser ships your source to a vbcc toolchain running server-side with the correct parameters.
- vbcc emits an Amiga or Atari ST binary executable, wrapped in a virtual floppy disk, using the OS profile.
- The IDE loads that disk into the emulator and opens a shell.
- The shell runs the program and prints the line.
You can verify it worked by reading the line in the shell window. That is the entire happy path. If you see a vbcc error in the IDE log instead, the line and column point straight at the offending C, just like a desktop compiler.
What you would do natively
If you were doing this on a real Amiga or Atari ST, you would either:
- Load the native C floppies on the ST or Amiga (Lattice C, Pure C, or the venerable Turbo C, etc) and do the work there
- Set up vbcc on a modern host (Linux, Mac, Windows) and cross-compile using something like
vc -c99 -+ hello.c -o hello.tosbefore transferring to the machine or emulator.
Our IDE collapses both of those into a single browser tab, which means we can focus on the C and not the toolchain.
Where this fits with the roguelike project
The roguelike’s most basic operation is printing a specific character at a designated row and column. printf("@") is a clumsy way to do that, but it is a valid way, and it works on every machine that has a C compiler. The next three parts of this series add the loop, the tile grid, and the user input. By the end you will have enough to render a playable dungeon.
Next part
In the following parts we add a counted loop and start thinking about game ticks … make sure you are subscribed so you don’t miss it!
The post Programming the Amiga and Atari ST in C: Hello World with VBCC appeared first on Retro Game Coders.