mruby in CMake-Buildsystem integrieren
Marvin Gülker · 10.05.2015
Wie man mruby in ein ansonsten von CMake gebautes Projekt einfügt.
Kategorien: Cpp, C, Ruby, Software
Einführung
mruby ist eine minimale Implementation der Programmiersprache Ruby. Zur Zeit des letzten Artikels zu mruby auf diesem Blog befand es sich noch in der Entwicklungsphase, mittlerweile sind sogar schon zwei Veröffentlichungen durchgeführt worden. Aktuell ist die Version 1.1.0, und erfreut sich zunehmender Beliebtheit.
Allerdings lässt die Dokumentation noch immer zu wünschen übrig. Es ist nach wie vor empfehlenswert, zumindest grundlegendes Wissen über die Erstellung von C-Extensions für den MRI zu besitzen, da man sich ansonsten in der doch recht komplexen API zu sehr verliert. Wer sich diesbezüglich informieren möchte, dem sei die Datei README.EXT aus dem offiziellen Ruby-Repositorium empfohlen.
TSC, das von mir geleitete 2D-Jump’n’Run-Spiel, dürfte das erste umfangreiche Spiel sein, das mruby als Scripting-Engine einsetzt. Dieser Beitrag isoliert die Einbindung von mruby aus TSC. Wer möchte, kann sich die Einbindung von mruby aber im Repositorium ansehen. TSC nutzt das Build-System CMake, mruby dagegen setzt auf ein eigenes, rake-basiertes System. Dementsprechend ist es nicht ganz einfach, beides zu vereinen.
CMake besitzt zu diesem Zwecke ein eigenes Modul namens
„ExternalProject“, welches es ermöglicht, praktisch jedes Build-System
an ein CMake-Projekt anzubinden. Dieses muss man zunächst explizit in
der CMakeLists.txt
des Projekts einbinden:
include(ExternalProject)
Git-Submodule
Obwohl „ExternalProject“ auch in der Lage ist, das gewünschte externe Projekt selbst in Form eines Tarballs herunterzuladen, zu verifizieren, und zu entpacken, sollte man von einem solchen Vorgehen absehen, wenn es sich beim eigenen und beim externen Projekt um mithilfe von Git verwaltete Projekte handelt, da Git mit Submodulen eine Funktionalität besitzt, die eine weit granularere Kontrolle ermöglicht. Bei einem Submodul handelt es sich im Grunde lediglich um ein Git-Repositorium, welches sich innerhalb eines anderen Git-Repositoriums befindet. Rekursives Git, wenn man so will. Git erkennt Submodule automatisch, d. h. wenn man die üblichen Git-Kommandos innerhalb eines Submodulverzeichnisses ausführt, so arbeiten sie ausschließlich mit diesem, ganz so, als ob das Eltern-Repositorium nicht existieren würde. Wechselt man dann zurück ins Elternrepositorium, so wird das Submodul als eine einzige Datei behandelt.
Für mruby wird ein Submodul im Verzeichnis mruby/
wie folgt angelegt:
$ git submodule add git://github.com/mruby/mruby.git mruby $ cd mruby $ git fetch origin --tags $ git checkout 1.1.0 $ cd .. $ git add mruby $ git commit
Innerhalb des Submoduls setzt man den HEAD-Pointer auf das gewünschte Tag. Git würde ansonsten schlicht den aktuellen HEAD des Standard-Branchs benutzen, was normalerweise aus Stabilitätsgründen nicht erwünscht ist. 1.1.0 ist die aktuelle stabile Version von mruby, und das Tag heißt genauso, sodass sich die obige Kommandofolge ergibt.
Checkt man das Repositorium später aus, so muss man git submodule init
und git submodule update
durchführen, ansonsten ist das Verzeichnis
leer.
MRuby-Konfiguration
In der CMakeLists.txt
sollte man zunächst auf mrubys eigene
Abhängigkeiten prüfen. mruby selbst besitzt zwar keine
Laufzeitabhängigkeiten, zur Kompilation aber sind Ruby und Bison
erforderlich. Für letzteres besietzt CMake ein eigenes Modul, für
ersteres kann das generiche find_program()
-Makro herangezogen werden:
find_program(RUBY_EXECUTABLE ruby) find_package(BISON REQUIRED) if (RUBY_EXECUTABLE) message(STATUS "Found ruby: ${RUBY_EXECUTABLE}") else() message(SEND_ERROR "ruby executable not found.") endif()
mruby verwaltet seine Build-Konfiguration mithilfe einer eigenen Datei,
bei der es sich (wie so oft in Ruby) um lauffähigen Ruby-Code handelt
(interne
DSL). mruby bringt eine Beispielkonfiguration in der Datei
build_config.rb
mit, die allerdings recht ungewöhnlich ist. TSC
benötigt weder die mruby
- und mirb
-Executables, noch will man auf
UTF-8-Support in der Klasse String verzichten. Genau das aber wird durch
die „Gembox“ default
konfiguriert. mruby bietet zwecks Vereinfachung
von Konfiguration diese sogenannten Gemboxen an, die nichts weiter sind
als eine Gruppe von einzelnen Bauanweisungen. Daher wird eine eigene
Konfiguration erstellt.
Dazu kopiert man zunächst die Beispielkonfiguration in ein Verzeichnis außerhalb des Submoduls.
cp mruby/build_config.rb mruby_build_config.rb
Danach entfernt man die Zeile
conf.gembox 'default'
und ersetzt sie durch alle conf.gem
-Zeilen aus der Datei
mrbgems/default.gembox
. Danach entfernt man die Zeilen für die
Executables mruby
und mirb
und fügt eine Zeile für den UTF-8-Support
hinzu:
conf.gem :core => "mruby-string-utf8"
Den Rest der Datei kann man auskommentieren, wenn man keine Debug-Builds
und keine Kreuzkompilation benötigt. Danach muss man mruby noch
mitteilen, dass man nicht innerhalb des Standardverzeichnisses bauen
will. Das Build-System selbst kennt dafür leider keine Parameter,
allerdings kann man in der Build-Konfiguration eine entsprechende
Einstellung vornehmen. Weil es sich bei der Build-Konfiguration um
gewöhnlichen Ruby-Code handelt und das Rake-basierte Build-System auf
der Kommandozeile übergebene Variablen in den ENV
-Hash schreibt, kann
man mit folgendem kleinen Hack das Build-Verzeichnis dynamisch durch
Kommandozeilenargument verändern:
conf.build_dir = ENV["MRUBY_BUILD_DIR"] || raise("MRUBY_BUILD_DIR undefined!")
Schließlich sollte die Datei mruby_build_config.rb
(ohne Kommentare)
so aussehen:
MRuby::Build.new do |conf| if ENV['VisualStudioVersion'] || ENV['VSINSTALLDIR'] toolchain :visualcpp else toolchain :gcc end conf.build_dir = ENV["MRUBY_BUILD_DIR"] || raise("MRUBY_BUILD_DIR undefined!") conf.gem :core => "mruby-sprintf" conf.gem :core => "mruby-print" conf.gem :core => "mruby-math" conf.gem :core => "mruby-time" conf.gem :core => "mruby-struct" conf.gem :core => "mruby-enum-ext" conf.gem :core => "mruby-string-ext" conf.gem :core => "mruby-numeric-ext" conf.gem :core => "mruby-array-ext" conf.gem :core => "mruby-hash-ext" conf.gem :core => "mruby-range-ext" conf.gem :core => "mruby-proc-ext" conf.gem :core => "mruby-symbol-ext" conf.gem :core => "mruby-random" conf.gem :core => "mruby-object-ext" conf.gem :core => "mruby-objectspace" conf.gem :core => "mruby-fiber" conf.gem :core => "mruby-enumerator" conf.gem :core => "mruby-enum-lazy" conf.gem :core => "mruby-toplevel-ext" conf.gem :core => "mruby-bin-strip" conf.gem :core => "mruby-kernel-ext" conf.gem :core => "mruby-string-utf8" end
mrubys Build-System basiert wie beschrieben auf Rake, genauer gesagt,
auf einer eigenen Minimalversion von Rake, minirake
. Diese verhält
sich für den Zweck des Kompilierens genauso wie ein gewöhnliches Rake;
vor allem wandelt es Kommandozeilenargumente der Form
GROSSBUCHSTABE=Wert
in Umgebungsvariablen um, auf die man mit ENV
zugreifen kann, was den oben gezeigten Hack ermöglicht. So kann man
mruby danach grundsätzlich so kompilieren:
$ cd mruby $ ruby ./minirake MRUBY_BUILD_DIR=/tmp/meinbuilddir MRUBY_CONFIG=../mruby_build_config.rb
Der Parameter MRUBY_CONFIG
gibt dabei die gewünschte
Konfigurationsdatei an. Ließe man ihn weg, so würde das Build-System die
Datei build_config.rb
im mruby-Verzeichnis benutzen. Den Parameter
MRUBY_BUILD_DIR
, dessen Verwendung bereits weiter oben beschrieben
wurde, muss man natürlich anpassen.
Integration in CMake
Die eigentliche Integration in das Build-System des Hauptprojekts
erfolgt über das Makro ExternalProject_Add()
wie folgt:
ExternalProject_Add(mruby PREFIX "${CMAKE_BINARY_DIR}/mruby" DOWNLOAD_COMMAND "" UPDATE_COMMAND "" BUILD_IN_SOURCE 1 SOURCE_DIR "${CMAKE_SOURCE_DIR}/../mruby" CONFIGURE_COMMAND "" BUILD_COMMAND ${RUBY_EXECUTABLE} minirake MRUBY_BUILD_DIR=${CMAKE_BINARY_DIR}/mruby MRUBY_CONFIG=${CMAKE_SOURCE_DIR}/mruby_build_config.rb INSTALL_COMMAND "") include_directories("${CMAKE_SOURCE_DIR}/../mruby/include") set(MRUBY_LIBRARIES "${CMAKE_BINARY_DIR}/mruby/lib/libmruby.a" "${CMAKE_BINARY_DIR}/mruby/lib/libmruby_core.a")
Der Reihe nach. Definiert wird durch dieses Makro ein CMake-Target
mruby
, auf das später noch einmal zurückzukommen sein wird. PREFIX
gibt das intendierte Build-Verzeichnis an, welches zu erstellen CMake
Sorge tragen wird. DOWNLOAD_COMMAND
und UPDATE_COMMAND
wären
Kommandos, die zum Herunterladen und Aktualisieren eines externen
Projekts erforderlich wären, welche aber aufgrund der hier vorgestellten
Submodul-Konfiguration unnötig sind und welche daher durch Angabe eines
Leerstrings abgeschaltet werden. BUILD_IN_SOURCE
weist CMake an, vor
der Ausführung des eigentlichen Baukommandos in das mit SOURCE_DIR
angegebene Verzeichnis zu wechseln. mruby benötigt keinen
Konfigurationsschritt, daher kann man ihn ebenfalls abschalten, dasselbe
gilt für einen Installationsschritt. Kern ist der Baubefehl,
BUILD_COMMAND
, der auf die zuvor gefundene Ruby-Executable zugreift
damit im wesentlichen das Kommando ausführt, welches oben für einen
manuellen Bau angeführt wurde. Angepasst wird lediglich das
MRUBY_BUILD_DIR
, sodass es mit dem von CMake erstellen PREFIX
übereinstimmt, sowie eine dynamischere Angabe der Build-Konfiguration.
Anschließend wird das Verzeichnis mruby/include
in die Liste der vom
C-Präprozessor zu durchsuchenden Verzeichnisse aufgenommen, damit das
Hauptprogramm auch in der Lage ist, die mruby-Header zu finden.
mruby besteht aus zwei statisch gelinkten Programmbibliotheken,
libmruby.a
und libmruby_core.a
, welche unterhalb des angegebenen
Build-Verzeichnisses im Verzeichnis lib/
abgelegt werden. Diese werden
als absolute Pfade in der Variablen MRUBY_LIBRARIES
abgespeichert.
Dies ist erforderlich, weil sie in die Executable des Hauptprogramms
gelinkt werden müssen, welche zum jetzigen Zeitpunkt noch gar nicht
definiert ist, denn diese Definition findet sich üblicherweise gegen
Ende der CMakeLists.txt
. Eine typische Definition eines Targets für
eine Executable sieht so aus:
add_executable(foobar ${foobar_sources}) target_link_libraries(foobar ${SOME_LIBRARIES})
Dies wird nun in zweifacher Hinsicht ergänzt, indem den zu linkenden
Bibliotheken die zwei mruby-Bibliotheken hinzugefügt werden (unter
Rückgriff auf die vorhin definierte Variable MRUBY_LIBRARIES
). Da
CMake in ein externes Projekt nicht hineinschauen und seinen Zweck
erkennen kann — hier die Erstellung der MRUBY_LIBRARIES
—, muss
die Abhängigkeit der Executable vom externen Target mruby
noch
explizit deklariert werden. Ansonsten käme es bei einem parallelen Build
mit mehreren Tasks gleichzeitig zu einem Wettrennen zwischen den
einzelnen Bauschritten. Es ist aber erforderlich, dass das Target
mruby
vollständig abgeschlossen ist, bevor das Target foobar
gelinkt
werden kann (sonst sind ja die mruby-Bibliotheken noch gar nicht
fertig). Das geschieht mithilfe des Makros add_dependencies()
, sodass
folgende Abfolge ensteht:
add_executable(foobar ${foobar_sources}) target_link_libraries(foobar ${SOME_LIBRARIES} ${MRUBY_LIBRARIES}) add_dependencies(foobar mruby)
Von hier an kann man mit dem Projekt verfahren wie mit jedem anderen CMake-Projekt auch.
$ mkdir build $ cd build $ cmake -DCMAKE_BUILD_TYPE=Debug .. $ make -j2 $ ./foobar