2026年5月1日

【C 言語】Tiny C Compiler で作る SDL2 開発環境

前回からのつづきです。Tiny C Compiler (以下 TinyCC) 自体のセットアップは終わったので、今回は SDL2 プログラムをコンパイルできるようにします。

はじめに

SDL の最新バージョンは 3.x 系であり、2.x 系は安定版としてメンテナンスモードに入っているみたいです。ざっとググったところ、TinyCC & SDL3 という事例はあんまりなさそうだったので、ここではバージョン 2.x 系を使うことにしました。

セットアップ手順

セットアップの手順は、TCC_TinyCCompilerWithSDL2 という GitHub リポジトリの README を参考にさせていただきました。

手順は次のとおりです:

  1. ダウンロード:
    https://github.com/libsdl-org/SDL/releases/tag/release-2.32.10/
    から SDL2-devel-2.32.10-VC.zip をゲットして解凍します。
  2. SDL2-2.32.10\lib\x64\SDL2.dll を自分の作ったプログラムの実行ファイル (.exe ファイル) と同じフォルダにコピーします。
    ※ 別のフォルダでも構いませんが、その場合はそのフォルダにパスを通してください。ここでは %HOME%\lib に置くことにして、前回 作ったバッチファイルに次の 1 行を追加しました。
    starttcc.cmd
     @echo off
     set PATH=%HOME%\bin\tcc;%PATH%
    +set PATH=%HOME%\lib;%PATH%
     cmd /k cls
    
  3. 前回 作ったバッチファイルをダブルクリックしてプロンプトを開きます。
  4. 次のコマンドを実行して .def ファイルを作成します。
    > tcc -impdef path\to\SDL2.dll
    
  5. 手順 4 を実行したフォルダに SDL2.def が作成されているので、それを tcc フォルダの下の lib フォルダ (前回の手順どおりなら %HOME%\bin\tcc\lib) に置きます。
  6. SDL2-2.32.10\include フォルダを丸ごと TinyCC インストールフォルダの include フォルダの下にコピーして、フォルダ名を SDL2 にリネームします。結果、次のようなフォルダ構造になります:
    %HOME%\bin\tcc\
        include\
            SDL2\
                begin_code.h
                close_code.h
                SDL.h
                ...
    

SDL2 のセットアップはこれで終了です。参考にした README によれば SDL_Config.h の修正も必要みたいですが、今回は 2.x 系の最新版だからか?特に修正しなくても支障はなさそうでした。

サンプルプログラムのコンパイルと実行

ここではサンプルとして下記の Gist にある test_sdl2.c を使わせていただきます。

https://gist.github.com/sol-prog/77661ef89bc22b47d0d84029b5c199a5

早速コンパイルといきたいところですが、TinyCC でコンパイルする場合は test_sdl2.c の 10 ~ 12 行目にあるように、SDL.h をインクルードした直後のところで main を undef する必要があります。サンプルの test_sdl2.c はすでにそうなっていますが、自分で一から書く場合は要注意です。

#include <SDL2/SDL.h>
// Normally SDL2 will redefine the main entry point of the program for Windows applications
// this doesn't seem to play nice with TCC, so we just undefine the redefinition
#ifdef __TINYC__
    #undef main
#endif
出典: sol-prog/test_sdl2.c (Gist)

そしたらコンパイルして、実行します。

> tcc test_sdl2.c -lSDL2
> test_sdl2.exe

最初はウィンドウの背景が真っ赤です。直視すると目をやられるかも (図 1)。C キーを押すたびにウィンドウの背景色が変われば OK です。

図 1
図 1

別の例: -DSDLCALL= が必要な場合

調子に乗って別の例もコンパイルしてみましょう。

https://github.com/xyproto/sdl2-examples/

このリポジトリの c99/main.c を先ほどと同じコマンドでコンパイルすると、エラーになります:

SDL_platform.h:265: error: ';' expected (got "SDL_GetPlatform")

これを回避するには、-DSDLCALL= を付けてコンパイルします。

> tcc main.c -lSDL2 -DSDLCALL=
> main.exe

めでたしめでたし。あとはおまけで、-DSDLCALL= をつけることになった経緯を書き留めておきますので、気になる方はご覧ください。

おまけ: -DSDLCALL= をつけることになった経緯

エラーが発生した SDL_platform.h の 265 行目は次のようになっています:

extern DECLSPEC const char * SDLCALL SDL_GetPlatform (void);

DECLSPEC と SDLCALL がどう展開されるのかを調べるために、tcc に -E オプションをつけてプリプロセスだけを実行します。

> tcc test_sdl2.c -lSDL2 -E

その出力を「SDL_GetPlatform」で検索すると、こうなってました:

extern  const char * __cdecl SDL_GetPlatform (void);

DECLSPEC は消えてなくなり、SDLCALL は __cdecl に展開されました。__cdecl というのは C 言語の呼び出し規約 (引数をスタックに積む順序だとか、スタックのクリーンアップを関数の呼び出し元が行うか、呼ばれたほうが行うか、など) のひとつらしいです。TinyCC のドキュメントによれば、TinyCC はデフォルトでこの規約にしたがうとのことです。

だからそもそも __cdecl というキーワード自体を知らないんだと思います。となると SDLCALL には何もすることなく消えてほしい。これが -DSDLCALL= をつけた理由です。

ちなみに、SDL2 のソースコードの中で SDLCALL を定義している場所は、begin_code.h の中ほどにある次の部分です。

begin_code.h (抜粋)
/* By default SDL uses the C calling convention */
#ifndef SDLCALL
#if (defined(__WIN32__) || defined(__WINRT__) || defined(__GDK__)) && !defined(__GNUC__)
#define SDLCALL __cdecl
#elif defined(__OS2__) || defined(__EMX__)
#define SDLCALL _System
# if defined (__GNUC__) && !defined(_System)
#  define _System /* for old EMX/GCC compat.  */
# endif
#else
#define SDLCALL
#endif
#endif /* SDLCALL */
出典: libsdl-org/SDL (GitHub)

これを見るとなんとなく 4 行目に落ち着くんだろうなあという感じはします。確認のために次のようなチェック用プログラムを作ってみました。

check_platform.c
#include <SDL2/SDL.h>
#ifdef __TINYC__
    #undef main
#endif

#include <stdio.h>

int main(int argc, char **argv) {
    SDL_Init(SDL_INIT_VIDEO);

#if defined(STDCALL)
    printf("STDCALL\n");
#endif
#if defined(__WIN32__)
    printf("__WIN32__\n");
#endif
#if defined(__WINRT__)
    printf("__WINRT__\n");
#endif
#if defined(__GDK__)
    printf("__GDK__\n");
#endif
#if defined(__GNUC)
    printf("__GNUC__\n");
#endif

    printf("SDL_GetPlatform: %s\n", SDL_GetPlatform());

    SDL_Quit();

    return 0;
}

コンパイルして実行したら、次のような結果になりました。__WIN32__ だけが define で、その他は undef みたいです。

> tcc check_platform.c -lSDL2 -DSDLCALL=
> check_platform.exe
__WIN32__
SDL_GetPlatform: Windows

P.S. その後いろいろ試したところ、SDL2/SDL.h より前で stdio.h をインクルードすれば、-DSDLCALL= をつけなくてもコンパイルが通ることがわかりました。

#include <stdio.h>
#include <SDL2/SDL.h>
#ifdef __TINYC__
    #undef main
#endif
...

理由はよくわかりません……まあ小っちゃいことは気にすんなってことで、放置したいと思います。まとめると、

  • stdio.h → SDL2/SDL.h の順にインクルードするなら -DSDLCALL= は不要。
  • SDL2/SDL.h → stdio.h の順にインクルードするなら -DSDLCALL= は必要。

ということのようでした。

関連記事