神和電子

SDL3をMesonでビルドしてみた(がまだだめそう)

作成日:

概要

Simple DirectMedia Layer (SDL) というライブラリがあります。一言でいうとクロスプラットフォームのゲーム開発ライブラリのようなものです(ゲーム以外も作れますが)。UnityやUnreal Engineのような高度な機能をもったゲームエンジンではなく、GUI関係やグラフィック、音楽、入力周りなどのプラットフォーム依存部分をいいかんじに抽象化してくれるタイプの薄いライブラリです。「DXライブラリみたいなやつ」というと通りがよいでしょうか。

ずっと(Wikipediaによると2013年から)SDL2だったのですが、最近(2025年1月)メジャーバージョンが上がったSDL3がリリースされました1。SDL3での改善点・変更点については New Features in SDL3 のページに記載されています。インパクトが大きそうなのは、

  • ドキュメントの改善
  • API名の規約をより一貫性があるものに
  • mainコールバックmain()関数のよくあるループを自分で書くのではなく、用意された名前のコールバック関数の定義として処理を書けるように(ただし従来通りの書き方も可能)
  • GPU、OSのダイアログ、ファイルシステム、ストレージ、Webカメラ、ペンタブなどにアクセスするAPIの追加
  • 非同期I/O機能の追加

あたりでしょうか。あと、SDL2でも一応できたようなので新機能かというと微妙ですが、EmscriptenでWebAsemmblyにしてWebブラウザで動かせるようになっているのは面白いです。

……というかんじの面白ライブラリなのですが、若干プロジェクトのセットアップが面倒という難点がありました。Linuxならパッケージマネージャーでインストールするだけなのですが、Windowsで素朴にやろうとすると「Zipをダウンロードしてきて解凍し、Visual Studioのインクルードパスとリンカの設定を変更する」という、(いつもの)若干面倒な手順が必要です。

本稿では、Meson というビルドツールを使ってSDL3を動かしてみようと思います。Mesonとは何ぞやというのは、以下の記事を参照するとよいと思います。

要するにCMakeみたいなやつです。CMakeが使いやすければそれでよかったのですが、上記の2番目の記事にも書いてあるように、実際使ってみるとCMakeは依存解決まわりが難しすぎますし、独自言語の書き方を調べるのにもかなり苦労します。Mesonはその辺りが改善されている(と謳う)ものになります。GLibやGStreamerといったGNOME系のプロジェクトで全面的にMesonが採用されている他、systemdもMesonを使っているようです。

SDL3を使う手順

前提

Visual Studio でC++がビルドできる環境がインストールされていることを前提とします。Visual StudioではなくMinGWのC++コンパイラでも多分動かせますが、本稿では扱いません。

Mesonのインストール

リリースページ から最新のバージョンのMSIインストーラー(meson-1.8.2-64.msiなど)をダウンロードして実行し、インストールします。なお、本稿はMeson 1.7.0の環境で書かれていますが、これより新しい分にはあまり変わらないのではないかと思います。

このインストーラーにはMeson本体の他、Mesonを動かすためのPythonの処理系と、ビルド時に使うNinjaが含まれていて、一緒にインストールしてくれます。つまり、このインストーラーでインストールしたもの(とC++のコンパイラ)さえあればMesonでのビルドができます。これが本当に偉くて、WindowsでPythonを利用したアプリケーションを動かそうとしたときに発生しがちなPythonのバージョンやパッケージまわりの面倒な問題の心配をする必要がありません。

プロジェクトの作成

PowerShellを開いて適当なディレクトリに移動し、以下のようなコマンドでプロジェクトを作成します。ここでは、プロジェクト名は etude-sdl3 としました。

# フォルダを作成する
mkdir etude-sdl3
cd etude-sdl3

# プロジェクトの初期化(初期ファイルの生成)を行う
meson init -l cpp -n etude-sdl3

# プロジェクトフォルダをエクスプローラーで開く(任意)
explorer.exe .

これで、etude-sdl3フォルダの中に

  • meson.build:Mesonの設定を書くためのファイル
  • etude_sdl3.cppmain() 関数を含むC++ファイルの雛型

の2つのファイルが生成されるはずです。

この上で、以下のようなコマンドを叩くことで、ビルドと実行が行えます。

# builddir という名前のフォルダにビルド用のファイルを生成する
meson setup builddir

# builddir 内で実際にビルドを行う
meson compile -C builddir

# 実行する
.\builddir\etude-sdl3.exe

実行結果のスクリーンショットを貼っておきます。meson setupのところでコンパイラなどの探索とビルド用のファイル(この場合はNinjaの定義ファイル)の生成が行われ、meson compileのところで実際のコンパイル(Ninjaでのコンパイル)が行われています。

実行結果

図1

実行結果

SDL3の追加とサンプルプログラムの実行

さて、これでMeson自体の動作確認はできたので、次はいよいよSDL3の実行を行っていきます。

まず、以下のようなコマンドでSDL3のファイルをダウンロードしてきます。これはWrapDBという仕組みでライブラリを追加する手順であるため、Meson WrapDB packages のリストにあるライブラリがこの方法で追加できます。

# サブプロジェクトを入れるディレクトリを作成する
mkdir subprojects

# SDL3のファイルをダウンロードして subprojects フォルダに格納する
meson wrap install sdl3

次に、etude_sdl3.cpp公式のサンプルプログラムのひとつ の内容に書き換えます。

/*
 * This example creates an SDL window and renderer, and then draws some lines,
 * rectangles and points to it every frame.
 *
 * This code is public domain. Feel free to use it for any purpose!
 */

#define SDL_MAIN_USE_CALLBACKS 1  /* use the callbacks instead of main() */
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>

/* We will use this renderer to draw into this window every frame. */
static SDL_Window *window = NULL;
static SDL_Renderer *renderer = NULL;
static SDL_FPoint points[500];

/* This function runs once at startup. */
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    int i;

    SDL_SetAppMetadata("Example Renderer Primitives", "1.0", "com.example.renderer-primitives");

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("examples/renderer/primitives", 640, 480, 0, &window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    /* set up some random points */
    for (i = 0; i < SDL_arraysize(points); i++) {
        points[i].x = (SDL_randf() * 440.0f) + 100.0f;
        points[i].y = (SDL_randf() * 280.0f) + 100.0f;
    }

    return SDL_APP_CONTINUE;  /* carry on with the program! */
}

/* This function runs when a new event (mouse input, keypresses, etc) occurs. */
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
    if (event->type == SDL_EVENT_QUIT) {
        return SDL_APP_SUCCESS;  /* end the program, reporting success to the OS. */
    }
    return SDL_APP_CONTINUE;  /* carry on with the program! */
}

/* This function runs once per frame, and is the heart of the program. */
SDL_AppResult SDL_AppIterate(void *appstate)
{
    SDL_FRect rect;

    /* as you can see from this, rendering draws over whatever was drawn before it. */
    SDL_SetRenderDrawColor(renderer, 33, 33, 33, SDL_ALPHA_OPAQUE);  /* dark gray, full alpha */
    SDL_RenderClear(renderer);  /* start with a blank canvas. */

    /* draw a filled rectangle in the middle of the canvas. */
    SDL_SetRenderDrawColor(renderer, 0, 0, 255, SDL_ALPHA_OPAQUE);  /* blue, full alpha */
    rect.x = rect.y = 100;
    rect.w = 440;
    rect.h = 280;
    SDL_RenderFillRect(renderer, &rect);

    /* draw some points across the canvas. */
    SDL_SetRenderDrawColor(renderer, 255, 0, 0, SDL_ALPHA_OPAQUE);  /* red, full alpha */
    SDL_RenderPoints(renderer, points, SDL_arraysize(points));

    /* draw a unfilled rectangle in-set a little bit. */
    SDL_SetRenderDrawColor(renderer, 0, 255, 0, SDL_ALPHA_OPAQUE);  /* green, full alpha */
    rect.x += 30;
    rect.y += 30;
    rect.w -= 60;
    rect.h -= 60;
    SDL_RenderRect(renderer, &rect);

    /* draw two lines in an X across the whole canvas. */
    SDL_SetRenderDrawColor(renderer, 255, 255, 0, SDL_ALPHA_OPAQUE);  /* yellow, full alpha */
    SDL_RenderLine(renderer, 0, 0, 640, 480);
    SDL_RenderLine(renderer, 0, 480, 640, 0);

    SDL_RenderPresent(renderer);  /* put it all on the screen! */

    return SDL_APP_CONTINUE;  /* carry on with the program! */
}

/* This function runs once at shutdown. */
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
    /* SDL will clean up the window/renderer for us. */
}

とりあえずコピペで動かすだけなのでコードの中身を理解する必要はないのですが、新機能の「mainコールバック」を利用したコードになっていてmain()が含まれていないことなんかは一目で分かりますね。

そして、これをビルドするためには meson.buildを書き換えて、SDL3のライブラリとリンクしてやる必要があります。以下のように書きます。

project('etude-sdl3', 'cpp',
  version : '0.1',
  default_options : ['warning_level=3',
                     'cpp_std=c++14'])

# SDL3に対する依存関係定義を dependencies という変数に保存する
sdl3_dep = dependency('sdl3', version : '>=3.2', default_options: ['default_library=static'])
dependencies = [sdl3_dep]

# 実行ファイルの生成方法の定義部分
# dependencies 変数を依存として追加する
exe = executable('etude-sdl3', 'etude_sdl3.cpp',
  install : true,
  dependencies : dependencies,
  win_subsystem: 'windows')

test('basic', exe)

最初に生成されていた meson.build の雛型から追加・変更しているのは以下のような部分です。

  • dependencies 変数の定義を追加しています。
    • ここに必要な依存関係を入れます。今回はSDL3に対する依存だけなので、別途 sdl3_dep変数に定義していた依存関係だけを配列に入れています。
    • sdl3_dep 変数に入れているのが、SDL3用の定義です。default_options: ['default_library=static'] は、静的リンクするように指定するオプションです。動的リンクでも動くのですがWindowsだとDLLファイルをコピーするのが面倒であるため、静的リンクにしました。
  • exe に入れる executable() に引数を追加しています。
    • dependencies : dependencies により、実行ファイルの依存関係として dependencies 変数の内容(上で準備したSDL3の依存関係)が設定されます。
    • win_subsystem: 'windows' により、実行時にコマンドラインウインドウが開かないようにします。

以上を行った上で、次のようなコマンドを実行することでビルドと実行ができます。

# ビルド用ファイルを再生成する
meson setup builddir --reconfigure

# コンパイルする
meson compile -C builddir

# 実行する
.\builddir\etude-sdl3.exe

実行すると、次のようなウインドウが出ます。

実行結果

図2

実行結果

以上で、MesonでSDL3を動かすことができました。

SDL_image を追加する(失敗)

前節の手順でSDL3本体を動かすことができたのですが、実はSDL3本体はあまり多機能ではなく、付加的な機能がプラグイン的に別のライブラリで提供されています。SDL3/Libraries - SDL Wiki に一覧がありますが、

  • SDL_image
  • SDL_mixer
  • SDL_ttf

あたりは普通にゲームを作るなら必要になりそうです。特に、SDL_imageはPNGなどの画像を表示するライブラリで、必須と思われます。

ただ、現状Meson用のビルドファイルがWrapDBに登録されているのはSDL_imageのみで、このSDL_imageも残念ながらエラーが出て動きませんでした。ここに失敗した手順と理由を記しておきます。

SDL_imageの追加とコンパイル

まずは、sdl3_imageをWrapDBからダウンロードします。

meson wrap install sdl3_image

そして、以下のようにmeson.buildにこれを使うための記述を追加します(dependenciesの部分です)。

project('etude-sdl3', 'cpp',
  version : '0.1',
  default_options : ['warning_level=3',
                     'cpp_std=c++14'])

# SDL3とSDL3_imageに対する依存関係定義を dependencies という変数に保存する
sdl3_dep = dependency('sdl3', version : '>=3.2', default_options: ['default_library=static'])
sdl3_image_dep = dependency('sdl3_image', version : '>=3.2', default_options: ['default_library=static'])
dependencies = [sdl3_image_dep, sdl3_dep]

# 実行ファイルの生成方法の定義部分
# dependencies 変数を依存として追加する
exe = executable('etude-sdl3', 'etude_sdl3.cpp',
  install : true,
  dependencies : dependencies,
  win_subsystem: 'windows')

test('basic', exe)

etude_sdl3.cppについては、SDL_AppInit()部分を以下のように書き換えます(他の部分はそのまま)。

/* ヘッダも追加する */
#include <SDL3_image/SDL_image.h>

/* テクスチャを読み込むためのグローバル変数を追加 */
static SDL_Texture *texture1 = NULL;

/* SDL_AppInit() を以下に置き換える */
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
    int i;

    SDL_SetAppMetadata("Example Renderer Primitives", "1.0", "com.example.renderer-primitives");

    if (!SDL_Init(SDL_INIT_VIDEO)) {
        SDL_Log("Couldn't initialize SDL: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    if (!SDL_CreateWindowAndRenderer("examples/renderer/primitives", 640, 480, 0, &window, &renderer)) {
        SDL_Log("Couldn't create window/renderer: %s", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    /* テクスチャを読み込む */
    texture1 = IMG_LoadTexture(renderer, "image1.png");
    if (!texture1) {
        SDL_Log("Couldn't load texture: %s\n", SDL_GetError());
        return SDL_APP_FAILURE;
    }

    return SDL_APP_CONTINUE;  /* carry on with the program! */
}

この状態で以下のようにしてビルドします。

# ビルド用ファイルを再生成する
meson setup builddir --reconfigure

# コンパイルする
meson compile -C builddir

しかし、meson compileしたときに以下のようなエラーが出ます。

> meson compile -C builddir
Activating VS 17.12.3
INFO: automatically activated MSVC compiler environment
INFO: autodetecting backend as ninja
INFO: calculating backend command to run: "C:\Program Files\Meson\ninja.EXE" -C C:/Users/suzusime/sandbox/etude-sdl3/builddir
ninja: Entering directory `C:/Users/suzusime/sandbox/etude-sdl3/builddir'
[1/2] Compiling C++ object etude-sdl3.exe.p/etude_sdl3.cpp.obj
../etude_sdl3.cpp(22): warning C4100: 'argv': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(22): warning C4100: 'argc': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(22): warning C4100: 'appstate': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(24): warning C4101: 'i': ローカル変数は 1 度も使われていません。
../etude_sdl3.cpp(49): warning C4100: 'appstate': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(58): warning C4100: 'appstate': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(96): warning C4100: 'result': 引数は関数の本体部で 1 度も参照されません。
../etude_sdl3.cpp(96): warning C4100: 'appstate': 引数は関数の本体部で 1 度も参照されません。
[2/2] Linking target etude-sdl3.exe
FAILED: etude-sdl3.exe etude-sdl3.pdb
"link"  /MACHINE:x64 /OUT:etude-sdl3.exe etude-sdl3.exe.p/etude_sdl3.cpp.obj "/release" "/nologo" "/DEBUG" "/PDB:etude-sdl3.pdb" "subprojects/SDL3_image-3.2.4/libsdl3_image.a" "subprojects/SDL3-3.2.10/libSDL3.a" "version.lib" "imm32.lib" "setupapi.lib" "winmm.lib" "/SUBSYSTEM:WINDOWS" "kernel32.lib" "user32.lib" "gdi32.lib" "winspool.lib" "shell32.lib" "ole32.lib" "oleaut32.lib" "uuid.lib" "comdlg32.lib" "advapi32.lib"
etude_sdl3.cpp.obj : error LNK2019: 未解決の外部シンボル IMG_LoadTexture が関数 SDL_AppInit で参照されました
etude-sdl3.exe : fatal error LNK1120: 1 件の未解決の外部参照
ninja: build stopped: subcommand failed.

これはリンクエラーで、要するにIMG_LoadTexture()関数がライブラリの中に見つからないと言っています。静的リンクしようとしているのがだめなのかもしれないと思い動的リンクも試してみましたが、それでもだめでした。

原因

WrapDBのリポジトリに以下のプルリクエストが立っていました。

IMG.cがビルド対象に含まれていなかったということで、それは確かにリンクエラーになるよねというところです。

このプルリクエストはまだマージされていません。恐らく何らかの方法でこの修正の内容を使ってビルドができるはずですが、すぐには調べられなかったので今日はここまでとします。

まとめ

Mesonを使ってSDL3本体をビルドして動かすことができました。しかし、拡張であるSDL_imageはバグにより動かせませんでした。

Mesonはわりといいかんじに見えるのですが、SDL3は元がMesonを使っていないためまだ対応がしきれていないというところでした。WrapDBというものは結局meson.buildを各ディレクトリに外部から差し込むようなソリューションであるため、軽い気持ちで自分で書けるようなものかというと微妙で、「誰か書いてくれ頼む~」となってしまい、OSSただ乗りの申し訳なさが生まれてきますね……。


  1. SDL 3.2.0 が3.0系の公式リリースの最初のバージョンで、3.1まではプレビュー版のようです。 ↩︎



© 神和電子 2017-2025