Sviluppare in C per il Commodore 64 – parte 2

Sviluppare in C per il Commodore 64

In questa seconda parte affrontiamo una tematica che riguarda più nello specifico il linguaggio C ma che è fondamentale per poter affrontare progetti più complessi, e sopratutto per poter gestire il terzo ed ultimo articolo di questa serie che affronterà l’utilizzo della grafica.
File versus Progetti
Nel primo articolo abbiamo fatto qualcosa che rasenta il magico, ma ci siamo limitati a compilare un file. Se volessimo affrontare un progetto che abbia un minimo di complessità, non potremo di sicuro mettere tutto il codice in un solo file, anche solo per motivi di gestione, pulizia e leggibilità.
Ma come si fa a distribuire il codice in più moduli? E – sopratutto – come compilo più moduli, e quindi più file, in un unico eseguibile? Questo è quello che vedremo a breve.
Un progetto di saluti al mondo
Trasformeremo il classico esempio “hallo world” visto nell’articolo precedente in un progetto, e lo compileremo. Raggiungeno lo stesso risultato ma con una struttura simile a quella che potremo usare per ottenere risultati molto più complessi.
Prima di tutto, dobbiamo rendere un po più complesso il nostro ambiente. Creiamo una cartella di nome Hallo World
ed al suo interno creiamo un’ulteriore cartella di nome src
. Dentro quest’ultima inseriremo tutti i file sorgente del nostro progetto.
E senza perdere tempo creiamo il file halloworld.c
dentro src
con il seguente codice:
#include <stdio.h>
void halloworld(void) {
printf("Hello, world!\n");
}
Abbiamo trasferito la nostra “Business Logic” (Logica di Business, cioè la logica operativa di un programma) dentro un modulo (che appunto in C corrisponde ad un file) a se stante.
Ora ci serve sempre il codice principale, quello che avvia l’applicazione, per il quale creiamo il file main.c
sempre dentro src
. Tutti ci staremo immaginando una cosa del genere:
void main(void) {
halloworld();
}
Siamo sicuri? Proviamo.
Make deus ex machina
Mi direte, si proviamo, ma come? I comandi necessari per compilare un progetto con più moduli sono molto più complessi e diversi, e cambiano col numero ed il tipo di moduli. Per questo di solito non si scrivono a mano ogni volta.
Fin dagli albori infatti in aiuto dei programmatori c’era il programma make
, papà di tutti i software di “Build Automation”1 che esegue in file di comandi, di nome Makefile
ed automatizza la compilazione. Noi faremo addirittura un passo oltre, ci creeremo un Makefile
generico che vada bene per tutti i progetti che affronteremo, per lo meno fino a quando non avremo talmente dimestichezza e conoscenza del mezzo da avere la necessità ma anche la capacità di intervenire specificatamente nel processo di compilazione con parametri particolari.
I creatori di “CC65”, il sistema che stiamo usando, ci sono venuti incontro, e ne hanno creato uno per noi2. Personalmente ne ho creato una copia di un file che ho chiamato Makefile.orig
e che uso come partenza per i progetti. Fatelo anche voi e mettetelo da qualche parte dove tenete i vostri progetti. Per comodità lo riporto di seguito:
###############################################################################
### Generic Makefile for cc65 projects - full version with abstract options ###
### V1.3.0(w) 2010 - 2013 Oliver Schmidt & Patryk "Silver Dream !" Łogiewa ###
###############################################################################
###############################################################################
### In order to override defaults - values can be assigned to the variables ###
###############################################################################
# Space or comma separated list of cc65 supported target platforms to build for.
# Default: c64 (lowercase!)
TARGETS :=
# Name of the final, single-file executable.
# Default: name of the current dir with target name appended
PROGRAM :=
# Path(s) to additional libraries required for linking the program
# Use only if you don't want to place copies of the libraries in SRCDIR
# Default: none
LIBS :=
# Custom linker configuration file
# Use only if you don't want to place it in SRCDIR
# Default: none
CONFIG :=
# Additional C compiler flags and options.
# Default: none
CFLAGS =
# Additional assembler flags and options.
# Default: none
ASFLAGS =
# Additional linker flags and options.
# Default: none
LDFLAGS =
# Path to the directory containing C and ASM sources.
# Default: src
SRCDIR :=
# Path to the directory where object files are to be stored (inside respective target subdirectories).
# Default: obj
OBJDIR :=
# Command used to run the emulator.
# Default: depending on target platform. For default (c64) target: x64 -kernal kernal -VICIIdsize -autoload
EMUCMD :=
# Optional commands used before starting the emulation process, and after finishing it.
# Default: none
# Examples
#PREEMUCMD := osascript -e "tell application \"System Events\" to set isRunning to (name of processes) contains \"X11.bin\"" -e "if isRunning is true then tell application \"X11\" to activate"
#PREEMUCMD := osascript -e "tell application \"X11\" to activate"
#POSTEMUCMD := osascript -e "tell application \"System Events\" to tell process \"X11\" to set visible to false"
#POSTEMUCMD := osascript -e "tell application \"Terminal\" to activate"
PREEMUCMD :=
POSTEMUCMD :=
# On Windows machines VICE emulators may not be available in the PATH by default.
# In such case, please set the variable below to point to directory containing
# VICE emulators.
#VICE_HOME := "C:\Program Files\WinVICE-2.2-x86\"
VICE_HOME :=
# Options state file name. You should not need to change this, but for those
# rare cases when you feel you really need to name it differently - here you are
STATEFILE := Makefile.options
###################################################################################
#### DO NOT EDIT BELOW THIS LINE, UNLESS YOU REALLY KNOW WHAT YOU ARE DOING! ####
###################################################################################
###################################################################################
### Mapping abstract options to the actual compiler, assembler and linker flags ###
### Predefined compiler, assembler and linker flags, used with abstract options ###
### valid for 2.14.x. Consult the documentation of your cc65 version before use ###
###################################################################################
# Compiler flags used to tell the compiler to optimise for SPEED
define _optspeed_
CFLAGS += -Oris
endef
# Compiler flags used to tell the compiler to optimise for SIZE
define _optsize_
CFLAGS += -Or
endef
# Compiler and assembler flags for generating listings
define _listing_
CFLAGS += --listing $$(@:.o=.lst)
ASFLAGS += --listing $$(@:.o=.lst)
REMOVES += $(addsuffix .lst,$(basename $(OBJECTS)))
endef
# Linker flags for generating map file
define _mapfile_
LDFLAGS += --mapfile $$@.map
REMOVES += $(PROGRAM).map
endef
# Linker flags for generating VICE label file
define _labelfile_
LDFLAGS += -Ln $$@.lbl
REMOVES += $(PROGRAM).lbl
endef
# Linker flags for generating a debug file
define _debugfile_
LDFLAGS += -Wl --dbgfile,$$@.dbg
REMOVES += $(PROGRAM).dbg
endef
###############################################################################
### Defaults to be used if nothing defined in the editable sections above ###
###############################################################################
# Presume the C64 target like the cl65 compile & link utility does.
# Set TARGETS to override.
ifeq ($(TARGETS),)
TARGETS := c64
endif
# Presume we're in a project directory so name the program like the current
# directory. Set PROGRAM to override.
ifeq ($(PROGRAM),)
PROGRAM := $(notdir $(CURDIR))
endif
# Presume the C and asm source files to be located in the subdirectory 'src'.
# Set SRCDIR to override.
ifeq ($(SRCDIR),)
SRCDIR := src
endif
# Presume the object and dependency files to be located in the subdirectory
# 'obj' (which will be created). Set OBJDIR to override.
ifeq ($(OBJDIR),)
OBJDIR := obj
endif
TARGETOBJDIR := $(OBJDIR)/$(TARGETS)
# Default emulator commands and options for particular targets.
# Set EMUCMD to override.
c64_EMUCMD := $(VICE_HOME)x64 -kernal kernal -VICIIdsize -autoload
c128_EMUCMD := $(VICE_HOME)x128 -kernal kernal -VICIIdsize -autoload
vic20_EMUCMD := $(VICE_HOME)xvic -kernal kernal -VICdsize -autoload
pet_EMUCMD := $(VICE_HOME)xpet -Crtcdsize -autoload
plus4_EMUCMD := $(VICE_HOME)xplus4 -TEDdsize -autoload
# So far there is no x16 emulator in VICE (why??) so we have to use xplus4 with -memsize option
c16_EMUCMD := $(VICE_HOME)xplus4 -ramsize 16 -TEDdsize -autoload
cbm510_EMUCMD := $(VICE_HOME)xcbm2 -model 510 -VICIIdsize -autoload
cbm610_EMUCMD := $(VICE_HOME)xcbm2 -model 610 -Crtcdsize -autoload
atari_EMUCMD := atari800 -windowed -xl -pal -nopatchall -run
ifeq ($(EMUCMD),)
EMUCMD = $($(CC65TARGET)_EMUCMD)
endif
###############################################################################
### The magic begins ###
###############################################################################
# The "Native Win32" GNU Make contains quite some workarounds to get along with
# cmd.exe as shell. However it does not provide means to determine that it does
# actually activate those workarounds. Especially $(SHELL) does NOT contain the
# value 'cmd.exe'. So the usual way to determine if cmd.exe is being used is to
# execute the command 'echo' without any parameters. Only cmd.exe will return a
# non-empty string - saying 'ECHO is on/off'.
#
# Many "Native Win32" programs accept '/' as directory delimiter just fine. How-
# ever the internal commands of cmd.exe generally require '\' to be used.
#
# cmd.exe has an internal command 'mkdir' that doesn't understand nor require a
# '-p' to create parent directories as needed.
#
# cmd.exe has an internal command 'del' that reports a syntax error if executed
# without any file so make sure to call it only if there's an actual argument.
ifeq ($(shell echo),)
MKDIR = mkdir -p $1
RMDIR = rmdir $1
RMFILES = $(RM) $1
else
MKDIR = mkdir $(subst /,\,$1)
RMDIR = rmdir $(subst /,\,$1)
RMFILES = $(if $1,del /f $(subst /,\,$1))
endif
COMMA := ,
SPACE := $(N/A) $(N/A)
define NEWLINE
endef
# Note: Do not remove any of the two empty lines above !
TARGETLIST := $(subst $(COMMA),$(SPACE),$(TARGETS))
ifeq ($(words $(TARGETLIST)),1)
# Set PROGRAM to something like 'myprog.c64'.
override PROGRAM := $(PROGRAM).$(TARGETLIST)
# Set SOURCES to something like 'src/foo.c src/bar.s'.
# Use of assembler files with names ending differently than .s is deprecated!
SOURCES := $(wildcard $(SRCDIR)/*.c)
SOURCES += $(wildcard $(SRCDIR)/*.s)
SOURCES += $(wildcard $(SRCDIR)/*.asm)
SOURCES += $(wildcard $(SRCDIR)/*.a65)
# Add to SOURCES something like 'src/c64/me.c src/c64/too.s'.
# Use of assembler files with names ending differently than .s is deprecated!
SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.c)
SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.s)
SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.asm)
SOURCES += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.a65)
# Set OBJECTS to something like 'obj/c64/foo.o obj/c64/bar.o'.
OBJECTS := $(addsuffix .o,$(basename $(addprefix $(TARGETOBJDIR)/,$(notdir $(SOURCES)))))
# Set DEPENDS to something like 'obj/c64/foo.d obj/c64/bar.d'.
DEPENDS := $(OBJECTS:.o=.d)
# Add to LIBS something like 'src/foo.lib src/c64/bar.lib'.
LIBS += $(wildcard $(SRCDIR)/*.lib)
LIBS += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.lib)
# Add to CONFIG something like 'src/c64/bar.cfg src/foo.cfg'.
CONFIG += $(wildcard $(SRCDIR)/$(TARGETLIST)/*.cfg)
CONFIG += $(wildcard $(SRCDIR)/*.cfg)
# Select CONFIG file to use. Target specific configs have higher priority.
ifneq ($(word 2,$(CONFIG)),)
CONFIG := $(firstword $(CONFIG))
$(info Using config file $(CONFIG) for linking)
endif
.SUFFIXES:
.PHONY: all test clean zap love
all: $(PROGRAM)
-include $(DEPENDS)
-include $(STATEFILE)
# If OPTIONS are given on the command line then save them to STATEFILE
# if (and only if) they have actually changed. But if OPTIONS are not
# given on the command line then load them from STATEFILE. Have object
# files depend on STATEFILE only if it actually exists.
ifeq ($(origin OPTIONS),command line)
ifneq ($(OPTIONS),$(_OPTIONS_))
ifeq ($(OPTIONS),)
$(info Removing OPTIONS)
$(shell $(RM) $(STATEFILE))
$(eval $(STATEFILE):)
else
$(info Saving OPTIONS=$(OPTIONS))
$(shell echo _OPTIONS_=$(OPTIONS) > $(STATEFILE))
endif
$(eval $(OBJECTS): $(STATEFILE))
endif
else
ifeq ($(origin _OPTIONS_),file)
$(info Using saved OPTIONS=$(_OPTIONS_))
OPTIONS = $(_OPTIONS_)
$(eval $(OBJECTS): $(STATEFILE))
endif
endif
# Transform the abstract OPTIONS to the actual cc65 options.
$(foreach o,$(subst $(COMMA),$(SPACE),$(OPTIONS)),$(eval $(_$o_)))
# Strip potential variant suffix from the actual cc65 target.
CC65TARGET := $(firstword $(subst .,$(SPACE),$(TARGETLIST)))
# The remaining targets.
$(TARGETOBJDIR):
$(call MKDIR,$@)
vpath %.c $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
$(TARGETOBJDIR)/%.o: %.c | $(TARGETOBJDIR)
cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(CFLAGS) -o $@ $<
vpath %.s $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
$(TARGETOBJDIR)/%.o: %.s | $(TARGETOBJDIR)
cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
vpath %.asm $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
$(TARGETOBJDIR)/%.o: %.asm | $(TARGETOBJDIR)
cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
vpath %.a65 $(SRCDIR)/$(TARGETLIST) $(SRCDIR)
$(TARGETOBJDIR)/%.o: %.a65 | $(TARGETOBJDIR)
cl65 -t $(CC65TARGET) -c --create-dep $(@:.o=.d) $(ASFLAGS) -o $@ $<
$(PROGRAM): $(CONFIG) $(OBJECTS) $(LIBS)
cl65 -t $(CC65TARGET) $(LDFLAGS) -o $@ $(patsubst %.cfg,-C %.cfg,$^)
test: $(PROGRAM)
$(PREEMUCMD)
$(EMUCMD) $<
$(POSTEMUCMD)
clean:
$(call RMFILES,$(OBJECTS))
$(call RMFILES,$(DEPENDS))
$(call RMFILES,$(REMOVES))
$(call RMFILES,$(PROGRAM))
else # $(words $(TARGETLIST)),1
all test clean:
$(foreach t,$(TARGETLIST),$(MAKE) TARGETS=$t $@$(NEWLINE))
endif # $(words $(TARGETLIST)),1
OBJDIRLIST := $(wildcard $(OBJDIR)/*)
zap:
$(foreach o,$(OBJDIRLIST),-$(call RMFILES,$o/*.o $o/*.d $o/*.lst)$(NEWLINE))
$(foreach o,$(OBJDIRLIST),-$(call RMDIR,$o)$(NEWLINE))
-$(call RMDIR,$(OBJDIR))
-$(call RMFILES,$(basename $(PROGRAM)).* $(STATEFILE))
love:
@echo "Not war, eh?"
###################################################################
### Place your additional targets in the additional Makefiles ###
### in the same directory - their names have to end with ".mk"! ###
###################################################################
-include *.mk
Poi prepariamolo per il nostro progetto. Facciamo una copia di Makefile.orig
chiamandolo Makefile
dentro la cartella Hallo World
e non dentro src
.
Prepariamo il Makefile
Ora modifichiamolo per i nostri scopi, e approfittiamone per vederne le parti principali.
Alla riga 12 troviamo
TARGETS :=
Nell’articolo precedente abbiamo visto cosa sono i target, e questo è il posto dove indicarli usando delle stringhe definite dal progetto “CC65”3 e separate da virgola. Ad esempio aggiungendo apple2,c64
verrà creato un programma per Apple2 ed uno per Commodore 64. Se non inseriamo nulla, invece, come faremo noi ora, si intenderà di default il solo Commodore 64.
Alla riga 16 troviamo
PROGRAM :=
Questo parametro serve per dare il nome all’eseguibile finale, e noi scriviamoci hw
.
Seguono parametri che permettono di indicare librerie particolari da usare, opzioni particolari per il compilatore, l’assembler ed il linker, ma che per ora non useremo.
E poi alla riga 42 troviamo
SRCDIR :=
Se non la modifichiamo, di default cercherà i file sorgenti dentro la cartella src
, come abbiamo fatto nell’esempio. Se vogliamo cambiare il nome di questa cartella, dovremo indicarlo quì.
Per ora questi sono tutti i parametri che ci interessano. Da linea di comando, all’interno della cartella Hallo World
diamo il comando
make
Verrà interpretato ed eseguito il Makefile
ed avremo… un errore?
Vedrete una cosa simile a
cl65 -t c64 -c --create-dep obj/c64/halloworld.d -o obj/c64/halloworld.o src/halloworld.c
cl65 -t c64 -c --create-dep obj/c64/main.d -o obj/c64/main.o src/main.c src/main.c(2): Error: Call to undefined function `halloworld'
make: *** [obj/c64/main.o] Error 1
Funzione mia non ti conosco
Il contenuto di main.c
non è solo quello che abbiamo immaginato prima, perchè quando al suo interno invochiamo la funzione halloworld
quel specifico modulo non sa cosa sia, ed infatti otteniamo un errore di “chiamata a funzione non definita”. Perchè la definizione c’è, ma è dentro il modulo “halloworld”, dobbiamo fare in modo che ne venga a conoscenza anche il modulo “main”: e ci vengo in aiuto i file headers, quello che terminano con .h
.
Proviamo; creiamo dentro src
un file che chiameremo halloworld.h
il cui contenuto sarà:
#ifndef HALLOWORLD
#define HALLOWORLD
void halloworld(void);
#endif
Le direttive che sono precedute da #
sono eseguite dal preprocessore – uno dei comandi della catena di compilazione – ed in questo caso servono a fare in modo che se anche questo file header venga incluso più di una volta, il suo contenuto sarà processato una volta sola. La parte centrale definisce la nostra funzione halloworld
per fare in modo che un modulo ne sia a conoscenza. Ma come fa il nostro moulo “main” ad usare questa definizione? Vediamo il file main.c
corretto:
#include "halloworld.h"
void main(void) {
halloworld();
}
Vedrete che ora compilando col comando make
non avremo errori.
Salutiamo il mondo
Nella nostra cartella avremo ora in file hw.c64
, chiamato così perchè prima abbiamo indicato di voler chiamare hw
i nostri eseguibile, e .c64
per indicare il target per il quale è valido il programma. Facciamone una copia chiamandolo hw.prg
ed eseguiamolo, et voilà!
