cmakefileclion

Undefined reference to variable in another header file, though I can't define it before runtime


I'm trying to compile a Playdate game I'm making in C, but I'm receiving errors in functions that use the pointer variables pd and pdsys, which I have declared in my global.h file (as extern const), but which I can only assign a value to in a method that runs when the Playdate API calls the initialization event (eventHandler()).

Part of the problem is that I understand why you can't use a variable without defining it, since C needs to know the size of the variable, but I don't understand why there would be a compilation error with using a variable pointer without it being defined, since the pointer has a fixed size.

From the research I've done, this compilation error is caused by having code that uses a variable with no definition, though I'm not sure how else I'm supposed to do this besides declaring it in global.h and assigning a value to it in my eventHandler(). I believe this wasn't an issue when I declared these variables inside main.c where they are being used, but as my project has grown in scope, I've found myself needing to access these variables in pretty much every other file in the project. I thought the best solution to this would be to have a file that all other files include, which contains the pd and pdsys declarations (and perhaps even more later on) so they can be accessed from any file, but if I am mistaken I'd appreciate info on how best to do this!

Here are what I believe are the relevant snippets of code. The project contains more, but I believe they're not part of the problem and would further bloat this question were I to include them:

main.c

#include "global.h"
#include "pd_api.h"

LCDSprite *pBall;
float crankTime;

int updateCallback(void *userdata) {
    crankTime += pdsys->getCrankChange() * (float)M_PI/180;
    pd->sprite->moveTo(pBall,
        sin(crankTime/1) * 16 + 400/2,
        sin(crankTime/2) * 32 + 240/2
        );
    // Updates and renders all LCDSprites
    pd->sprite->updateAndDrawSprites();
    return 1;
}

int eventHandler (PlaydateAPI *playdate, PDSystemEvent event, uint32_t arg) {

    switch (event) {

        // Game initialization!
        case kEventInit:
        pd = playdate;  //Store the playdate API struct in pd variable
        pdsys = playdate->system; //Store playdate system struct in pdsys variable
        break;

        // Default case to suppress warnings
        default:
        break;

    }
    
    playdate->system->logToConsole("eventHandler call received. %d", arg);
    return 0;
    
}

global.h

#ifndef GLOBAL_H
#define GLOBAL_H

#include "pd_api.h"

extern const PlaydateAPI* pd;
extern const struct playdate_sys* pdsys;

#endif //GLOBAL_H

The error I get when I try to build the project is as follows:

====================[ Build | all ]=============================================
make --jobs=10 all
detected_OS is "Darwin"
mkdir -p build
mkdir -p build/dep
mkdir -p `dirname build/src/main.o`
clang -g -dynamiclib -rdynamic -lm -DTARGET_SIMULATOR=1 -DTARGET_EXTENSION=1 -I . -I /Users/JaydedCompanion//Developer/PlaydateSDK//C_API -o build/pdex.dylib src/main.c src/global.c src/jaydes_pd_utils.c src/game.c src/ball.c /Users/JaydedCompanion//Developer/PlaydateSDK//C_API/buildsupport/setup.c
/usr/local/bin/arm-none-eabi-gcc -g3 -c -mthumb -mcpu=cortex-m7 -mfloat-abi=hard -mfpu=fpv5-sp-d16 -D__FPU_USED=1 -O2 -falign-functions=16 -fomit-frame-pointer -gdwarf-2 -Wall -Wno-unused -Wstrict-prototypes -Wno-unknown-pragmas -fverbose-asm -Wdouble-promotion -mword-relocations -fno-common -ffunction-sections -fdata-sections -Wa,-ahlms=build/main.lst -DTARGET_PLAYDATE=1 -DTARGET_EXTENSION=1  -MD -MP -MF build/dep/main.o.d -I . -I . -I /Users/JaydedCompanion//Developer/PlaydateSDK//C_API src/main.c -o build/src/main.o
/usr/local/bin/arm-none-eabi-gcc -g3 build/src/main.o build/src/global.o build/src/jaydes_pd_utils.o build/src/game.o build/src/ball.o build//Users/JaydedCompanion//Developer/PlaydateSDK//C_API/buildsupport/setup.o -nostartfiles -mthumb -mcpu=cortex-m7 -mfloat-abi=hard -mfpu=fpv5-sp-d16 -D__FPU_USED=1 -T/Users/JaydedCompanion//Developer/PlaydateSDK//C_API/buildsupport/link_map.ld -Wl,-Map=build/pdex.map,--cref,--gc-sections,--no-warn-mismatch,--emit-relocs    -o build/pdex.elf
Undefined symbols for architecture x86_64:
  "_pd", referenced from:
      _updateCallback in main-b239cd.o
      _eventHandler in main-b239cd.o
      _ball in ball-c3812c.o
  "_pdsys", referenced from:
      _updateCallback in main-b239cd.o
      _eventHandler in main-b239cd.o
      _game_init in game-6fe6d9.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
make: *** [build/pdex.dylib] Error 1
make: *** Waiting for unfinished jobs....
/usr/local/playdate/gcc-arm-none-eabi-9-2019-q4-major/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld: build/src/main.o: in function `updateCallback':
/Users/JaydedCompanion/Documents/GitHub/PlaydateProject/src/main.c:24: undefined reference to `pdsys'
/usr/local/playdate/gcc-arm-none-eabi-9-2019-q4-major/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld: /Users/JaydedCompanion/Documents/GitHub/PlaydateProject/src/main.c:24: undefined reference to `pd'
/usr/local/playdate/gcc-arm-none-eabi-9-2019-q4-major/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld: build/src/main.o: in function `eventHandler':
/Users/JaydedCompanion/Documents/GitHub/PlaydateProject/src/main.c:55: undefined reference to `pd'
/usr/local/playdate/gcc-arm-none-eabi-9-2019-q4-major/bin/../lib/gcc/arm-none-eabi/9.2.1/../../../../arm-none-eabi/bin/ld: /Users/JaydedCompanion/Documents/GitHub/PlaydateProject/src/main.c:55: undefined reference to `pdsys'
collect2: error: ld returned 1 exit status
make: *** [build/pdex.elf] Error 1

Process finished with exit code 2

I'm not sure the makefile is part of the problem, so I won't include it as not to make this question even longer than it already is, but I can confirm that it includes the relevant source files and hasn't been an issue so far. I will admit, it's a makefile that I sourced from the example projects included in the Playdate C API, as I have very little experience working with C and even less experience using makefiles. Additionally, the Playdate C API documentation has virtually no information on how to write a makefile for Playdate projects, and suggests people just use the example makefiles as I have done. If this is likely to be pertinent information, however, I'd be happy to edit this post to include it.


Solution

  • To link a C program, all objects needs to be defined. A pointer variable is no exception, that's why you have this error. A declaration just tells the compiler that there is the declared object somewhere else.

    However, it is not necessary to initialize a pointer variable with the pointer to an existing object. You can do this at runtime, as you intend.

    I have simplified your snippets to a complete and simple example.

    This main functions calls a loader function to assign the pointer to some data to the global pointer variable. In this case it is a simple string.

    // main.c
    
    #include <stdio.h>
    
    #include "global.h"
    #include "loader.h"
    
    int main(void) {
        load();
        printf("Loaded %s\n", pd);
    }
    

    Please do not define global variables in header files. Instead, declare them, otherwise there will be as many definitions in your program as you include the header file.

    // global.h
    
    extern const char *pd;
    

    To provide the declared pointer variable, you need another module. It is good practice to include the declaring header file to let the compiler report any discrepancies. There is no need to initialize this variable explicitly, because the global variable as a static object is automatically zero-initialized.

    // global.c
    
    #include "global.h"
    
    const char *pd;
    

    The loader has its own header file declaring the loader function:

    // loader.h
    
    void load(void);
    

    And its implementation assigns an address to the global variable. In your case you would allocate memory and fill it or do anything what you see fit.

    // loader.c
    
    #include <stdlib.h>
    
    #include "loader.h"
    #include "global.h"
    
    void load(void) {
        pd = "CONTENT";
    }
    

    I built the example by this command line:

    gcc -pedantic -Wall -s main.c global.c loader.c -o main.exe
    

    Note1: After decades of C programming I see it as code smell to provide a "global.h" with declarations of global variables, constants, and so on. Instead, think about your design and encapsulate coherent things in modules.

    Note 2: Additionally you could design your software architecture such, that the pointer to loaded data will be provided by another function of the loader module. This would encapsulate and give the responsibility to some meaningful module.