OpenGL nativ und im Browser nutzen

Kolumne: The Good Parts
Kommentare

JavaScript ist mittlerweile viel mehr als nur eine Sprache – es ist eine Plattform. Unterschiedlichste Sprachen können automatisiert in JavaScript übersetzt werden. Zur Entwicklung von Programmen im Browser ist daher der Einsatz von JavaScript keine zwingende Voraussetzung mehr. Durch diesen Ansatz werden moderne Webapplikationen nicht nur unabhängig von der Sprache, es ist außerdem möglich, Applikationen durch einen Cross-Compiler sowohl nativ als auch fürs Web zu übersetzen.

Eine Codebasis, sie alle zu knechten

Epic hat es in Kooperation mit Mozilla vorgemacht: Ihre Epic-Citadel-Demo ist eine mittels Emscripten für den Browser übersetzte Version der Unreal-Engine. Mit relativ moderaten Anpassungen war es Epic möglich, ihre Technologiedemo für den Browser zu kompilieren. Ergebnis: Eine Codebasis, ursprünglich gedacht für die native Ausführung auf einer Plattform, ist nun im Browser auf jedem aktuellen System einsetzbar.

Denkt man diese Idee weiter, so erkennt man schnell, dass dieser technologische Ansatz im Bereich von 3-D-Anwendungen großes Potenzial besitzt. Ein Spiel z. B. könnte so aus der gleichen Codebasis nicht nur auf aktuelle Mobile-Plattformen wie iOS und Android, Desktopsysteme wie Linux, Windows und Mac OS, sondern auch auf jeden modernen Browser deployt werden.

Ein Beispiel sagt mehr als tausend Worte

Im Folgenden wird die schrittweise Erstellung einer OpenGL-Applikation gezeigt. Ziel ist es, sie anschließend nativ für Windows, Linux und Mac OS zu kompilieren und sie mithilfe von Emscripten für den Browser zu übersetzen. Die Applikation selbst gestaltet sich sehr einfach: Sie soll ein Fenster öffnen, in dem ein dreidimensionaler Würfel dargestellt wird, der sich um alle drei Achsen dreht. Auf allen Seiten des Würfels soll das Qafoo-Firmenlogo angezeigt werden, welches zuvor als Textur aus einer PNG-Datei geladen wurde. Abbildung 1 zeigt das Ergebnis der fertigen Applikation ausgeführt unter Mac OS.

Abb. 1: OpenGL-Applikation in Aktion – ein sich drehender Qafoo-Würfel

Die Entscheidung für die Entwicklungssprache fällt auf C. Vornehmlich aus zwei Gründen: Erstens unterstützt Emscripten als Ausgangssprache nur C und C++. Dies schränkt die Auswahl natürlich bereits maßgeblich ein. Auch wenn es noch andere Transpiler z. B. für die .NET-Sprachenfamilie gibt, so ist Emscripten derzeit doch eines der interessantesten Projekte. Zweitens fällt die Entscheidung auf C, weil diese Sprache für das gewählte Beispiel einfacher verständlich ist als C++.

Nach einer Evaluation der Applikationsanforderungen ist schnell klar: Es müssen drei maßgebliche Aufgaben bewältigt werden:

1. Bereitstellung eines OpenGL-Kontexts, der in einem Fenster dargestellt wird. 2. Laden des Qafoo-Logos als Textur aus einer PNG-Datei. 3. Zeichnen des rotierenden Würfels mit entsprechender Textur.

Das Rad muss nicht neu erfunden werden

Zunächst wird die JavaScript-Umsetzung ausgeblendet. Somit fällt für die ersten beiden Punkte die Entscheidung, bereits existierende Libraries einzusetzen. Die einfache Bereitstellung eines OpenGL-Kontexts wird GLFW erledigen, wohingegen das Laden der PNG-Textur mit der Image-Bibliothek von SDL vollzogen wird. Die Hauptaufgabe – das Zeichnen des Würfels – kann natürlich nicht von einer fertigen Komponente übernommen werden, auch wenn OpenGL sich um die korrekte Geometrieberechnung und Bereitstellung der Graphik-Pipeline kümmert.

Sind die beiden Bibliotheken SDL und GLFW installiert, so kann es auch schon losgehen. Zu Beginn wird eine Datei mit dem Namen qafoo.c erstellt (Listing 1). Zunächst richtet sich das Augenmerk auf die Funktion main. Hierbei handelt es sich um den Haupteintrittspunkt der Applikation, also jenen Teil, der bei einem Aufruf als Erstes ausgeführt wird. Der Inhalt dieser Funktion ist bis auf wenige Ausnahmen nicht sonderlich spannend. Als Erstes wird die GLFW-Bibliothek mittels glfwInit initialisiert. Wie bei allen weiteren Aufrufen wird das Programm im Fehlerfall einfach beendet. Anschließend wird ein 640 x 480 Pixel großes Fenster mit einem OpenGL-Kontext geöffnet. Die erste Anforderung an die Applikation ist somit bereits umgesetzt: Öffnen eines Fensters, das einen OpenGL-Kontext beinhaltet.

#include 

void initGlScene();
void renderGlScene(double delta);
void doRenderingLoop();

double lastSceneRendered, currentSceneRendered;

void renderFrame() {
  currentSceneRendered = glfwGetTime();
  renderGlScene(currentSceneRendered - lastSceneRendered);
  lastSceneRendered = currentSceneRendered;
  glfwSwapBuffers();
}

int main(void)
{
  if (!glfwInit()) {
    return -1;
  }

  if(glfwOpenWindow(640, 480, 0,0,0,0,16,0, GLFW_WINDOW) != GL_TRUE)
  {
    glfwTerminate();
    return -1;
  }

  initGlScene();
  lastSceneRendered = glfwGetTime();

  doRenderingLoop(&renderFrame);

  glfwTerminate();
  return 0;
}

Der nächste Aufruf, initGlScene, stammt nicht aus einer fertigen Bibliothek, sondern wird im Laufe dieses Artikels noch implementiert. initGlScene wird sich unter anderem um den zweiten Punkt der Anforderungen kümmern: Laden der Qafoo-Textur. Bevor dieser Punkt allerdings nähere Betrachtung findet, noch einige Worte zur main-Funktion. In Zeile 31 wird die Funktion doRenderingLoop aufgerufen, der ein Pointer auf die Funktion renderFrame übergeben wird. Die Aufgabe der doRenderingLoop-Funktion ist, die ihr übergebene Funktion aufzurufen, wann immer ein neues Frame der Animation gezeichnet werden muss. Dies geschieht so lange, bis das Programm beendet wird. Der Inhalt der renderFrame-Funktion ist in den Zeilen 9–14 in Listing 1 zu sehen. Der Inhalt dieses Programmabschnitts ist schnell erklärt: Zunächst wird die glfwGetTime-Funktion eingesetzt, um die aktuelle Systemzeit in Millisekunden zu erhalten. Anschließend wird renderGlScene aufgerufen; diesem wird die vergangene Zeit seit dem letzten Aufruf übergeben. Dem Namen nach unschwer zu erkennen, ist es die Aufgabe von renderGlScene, die eigentliche Szene, also den Würfel in der aktuellen Animationsposition, in den OpenGL-Kontext zu zeichnen. Die Zeiterfassung ist notwendig, da in unterschiedlichen Umgebungen unterschiedlich viele Bilder pro Sekunde gezeichnet werden können. Um also auf allen Systemen eine gleichmäßige Animation zu gewährleisten, wird die vergangene Zeit als Indikator verwendet, nicht die Anzahl gerenderter Frames. Der abschließende Aufruf von glfwSwapBuffers teilt dem System mit, dass ein neues Frame vollständig bereit steht und nun auf dem Bildschirm dargestellt werden soll.

Aufmacherbild: The word opengl against futuristic black and blue background von Shutterstock / Urheberrecht: wavebreakmedia

[ header = Seite 2: Initialisieren und Rendern der Scene ]

Initialisieren und Rendern der Scene

Das Grundgerüst ist fertig. Nach einer kurzen Rekapitulation gilt es, im nächsten Schritt die Funktionen initGlScene und renderGlScene mit Leben zu füllen. Deren Inhalt wird in der Datei rendering.c abgelegt, die auszugsweise in Listing 2 und 3 abgedruckt ist. Die relevanten Aufrufe zur Initialisierung der Textur sind in Listing 2 zu sehen. Neben der Definition der Hintergrundfarbe (glClearColor) sowie diversen weiteren OpenGL-spezifischen Initialisierungen, die der Einfachheit halber hier entfernt wurden, ist lediglich eine weitere Sache innerhalb der Funktion initGlScene interessant: der Aufruf von initTextures. Die Aufgabe dieser Funktion ist das Laden des Qafoo-Logos zur Verwendung als Textur für den Würfel. Wie zuvor festgelegt wird hierfür die SDL-Bibliothek herangezogen. Ein schlichter Aufruf von IMG_Load ermöglicht es so, alle notwendigen Daten des Bilds aus dem Dateisystem zu laden. Als Ergebnis wird eine Datenstruktur erzeugt, die neben den Metadaten des Bilds, wie Breite, Höhe und Farbtiefe, ebenfalls die dekodierten Pixelfarbwerte des PNG enthält. SDL hat das PNG-Dateiformat also korrekt gelesen und die ganze schwere Arbeit bereits erledigt. Nachdem nun die Pixelwerte im Speicher vorhanden sind, kann daraus mittels glTexImage2D eine OpenGL-Textur erstellt werden. Zur Vorbereitung dieses Aufrufs sind noch einige weitere OpenGL-spezifische Schritte notwendig, die hier jedoch der Übersichtlichkeit halber nicht abgedruckt sind. Durch den Einsatz der SDL-Bibliothek ist nun auch die zweite Anforderung der Applikation erfüllt: Laden eines PNG-Bilds aus dem Dateisystem zur Nutzung als Textur für den Würfel.

void initTextures() {
  SDL_Surface *image;
  if(!(image = IMG_Load("data/qafoo.png"))) {
    exit(-1);
  }

  // ...
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image->w, image->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, image->pixels);
  // ...
}

void initGlScene() {
  initTextures();
  glClearColor(.41f, 0.71f, 0.4f, 0.0f);
  // ...
}

Bisher fehlt jedoch die Funktion renderGlScene, die den gewünschten Würfel zeichnet und rotiert. Sie ist als weiterer Bestandteil der rendering.c-Datei in Listing 3 zu sehen. Zu Beginn jedes neuen Frames der Animation wird zunächst mittels glClear das vorherige Bild komplett entfernt und die gesamte Szene wieder auf die definierte Hintergrundfarbe (Grün) zurückgesetzt. Anschließend wird das gesamte Koordinatensystem mit glRotatef um die Achsen X, Y und Z gedreht. Anstatt also den Würfel zu bewegen, wird die Welt um den Würfel herum gedreht. Diese Art und Weise der Positionierung ist der einfachste und gängigste Weg, das Gewünschte in OpenGL umzusetzen. Nachdem die Ausrichtung für den Würfel nun festgelegt ist, aktiviert glBindTexture die gewünschte Textur. Anschließend folgt eine Reihe von glVertex3f-Kommandos. Mit diesen werden die Eckpunkte jeder Würfelseite beschrieben. Schließlich muss OpenGL wissen, was es zeichnen soll. Etwaige glTexCoord2f-Aufrufe dienen lediglich dazu, die Textur richtig herum auf den einzelnen Würfelseiten anzubringen. Natürlich müssen nicht nur die Eckpunkte der Vorderseite des Würfels definiert werden, sondern ebenfalls die der Rückseite, der beiden Seitenteile, sowie der Ober- und Unterseite. Um dieses Beispiel nicht unnötig aufzublähen, beinhaltet Listing 3 nur den Quelltext für die Zeichnung der Vorderseite sowie eine Andeutung, wie die Rückseite aussehen kann.

void renderGlScene(double delta) {
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  // ...
  glRotatef(rotationX,1.0f,0.0f,0.0f);
  glRotatef(rotationY,0.0f,1.0f,0.0f);
  glRotatef(rotationZ,0.0f,0.0f,1.0f);

  glBindTexture(GL_TEXTURE_2D, textures[0]);

  glBegin(GL_QUADS);

  // Front Face
  glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f,  1.0f);	// Bottom Left
  glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, -1.0f,  1.0f);	// Bottom Right
  glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f,  1.0f,  1.0f);	// Top Right
  glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f,  1.0f,  1.0f);	// Top Left

  // Back Face
  glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f);
  // ...
    
  glEnd();

  rotationX += delta * 25.0f;
  rotationY += delta * 25.0f;
  rotationZ += delta * 25.0f;
}

Nachdem nun der Würfel auf den Bildschirm gezeichnet ist, muss noch die Rotationsbewegung für das nächste Frame aktualisiert werden. Hierzu wird die vergangene Zeit seit dem letzten Bild (delta) herangezogen. Das sorgt, wie zuvor bereits erwähnt, für eine gleichmäßige Bewegung, egal wie viele Bilder pro Sekunde das Zielsystem in der Lage ist, zu zeichnen.

Fast bereit für den ersten Testlauf

Damit ist die dritte Anforderung an die Applikation nun vollständig implementiert: Rendering und Animation des Würfels samt Textur. Eigentlich bräuchte man jetzt nur noch den Quelltext zu übersetzen und es sollte sich etwas auf dem Bildschirm tun. Auf Linux- bzw. Mac-OS-X-basierten Systemen kann gcc weiterhelfen:

gcc qafoo.c rendering.c -lglfw -lSDL -lSDL_image -lGL -lGLU -o qafoo3d

Neben der Anweisung, diverse Bibliotheken für den Übersetzungsvorgang heranzuziehen – GLFW, SDL (inklusive SDL_Image) und OpenGL – teilt diese Kommandozeile dem Compiler mit, dass die beiden Dateien qafoo.c und rendering.c zu einer ausführbaren Datei mit dem Namen qafoo3d verarbeitet werden sollen. Nach der Ausführung des gezeigten Befehls gibt es jedoch eine kleine Überraschung:

Undefined symbols for architecture x86_64:
"_doRenderingLoop", referenced from:
    _main in ccqlVkba.o

Eine Fehlermeldung dieser Art wird auf der Shell präsentiert. Was ist passiert? Die Antwort ist recht einfach: Die Implementierung der anfänglich aufgerufenen Funktion doRenderingLoop fehlt aktuell noch. Ein Beinbruch ist das natürlich nicht, ist die Implementierung doch leicht nachzuholen. Der zugehörige Quelltext wird in der Datei native_loop.c verstaut (Listing 4).

// ...
void doRenderingLoop(renderFunc doRendering) {
  while (glfwGetWindowParam( GLFW_OPENED ))
  {
    doRendering();
  }
}

Eingesetzt wird eine einfache while-Schleife, die bei jedem Durchlauf unter Verwendung von glfwGetWindowParam prüft, ob das Fenster, in dem der Würfel gerendert wird, noch geöffnet ist. Währenddessen wird die übergebene Funktion doRendering aufgerufen, um das nächste Frame der Animation zu zeichnen. Ist diese zusätzliche Datei in die oben beschriebene Befehlszeile eingesetzt, lässt sich die Applikation fehlerfrei übersetzen und liefert den in Abbildung 1 dargestellten Output:

gcc qafoo.c native_loop.c rendering.c -lglfw -lSDL -lSDL_image -lGL -lGLU -o qafoo3d

Die Applikation ist jetzt fertig. Sie kann nun unter Windows, Linux und Mac OS X übersetzt und zur Ausführung gebracht werden. Doch was ist mit dem Browser? Hierzu wird Emscripten herangezogen.

Übersetzen für den Browser

Emscripten ist ein Compiler, der unter Einsatz von LLVM C und C++ Quelltext in JavaScript übersetzen kann. Dieser ist anschließend mit minimalem Zutun direkt im Browser ausführbar. Zusätzlich zu besagtem Compiler enthält das Emscripten-Projekt mittlerweile auch viele optimierte Implementierungen einiger Bibliotheken. So z. B. auch eine Umsetzung von OpenGL in WebGL sowie auf den Browser optimierte Varianten von SDL und GLFW. Die für das Beispiel benutzten Libraries sind also mehr als nur kompatibel mit Emscripten. Bei der Erstellung des Beispiels wurde auf diese spezielle Form der Umsetzung allerdings tatsächlich nicht geachtet. Hätte eine der eingesetzten Bibliotheken keine direkte Umsetzung in Emscripten gehabt, so hätte man diese einfach ebenfalls durch den Übersetzer geschickt. Funktioniert hätte die Applikation dann auch, lediglich die Menge an generiertem JavaScript-Code wäre größer geworden – sowie einige der Routinen eventuell etwas langsamer. Eine sehr detaillierte Installationsanleitung für Emscripten finden Sie hier. Ist das SDK installiert, so steht der Emscripten-C-Compiler emcc bereit. Dieser dient auch in dem hier gezeigten Beispiel zur Übersetzung des Quelltexts:

emcc qafoo.c native_loop.c rendering.c -s LEGACY_GL_EMULATION=1 -O2 -o qafoo3d.html

Emscripten bekommt zum einen wie der gcc mitgeteilt, aus welchen Dateien der Quelltext zu beziehen ist (qafoo.c, native_loop.c und rendering.c), und zum anderen, was das Ausgabeziel sein soll, was in diesem Fall eine HTML-Seite mit dem Namen qafoo3d.html ist. Diese Seite beinhaltet anschließend ein HTML-Grundgerüst inklusive Canvas zum Rendering sowie den erzeugten JavaScript-Code. Emscripten erkennt anhand der Ausgabedatei, welches Format gewünscht ist. Ein -o qafoo3d.js würde demnach den puren JavaScript-Code erzeugen, der dann in ein bestehendes Projekt integriert werden kann. Die Option -s LEGACY_GL_EMULATION=1 teilt dem Compiler mit, dass eine bestimmte Art der OpenGL-Umsetzung geladen werden soll, die es ermöglicht, auch nicht-OpenGL-ES-kompatiblen Code zu übersetzen. Aus Gründen der Einfachheit wurde dieser für das Beispiel gewählt. -O2 setzt ein Optimierungslevel fest, durch das unter anderem die Größe des generierten JavaScript-Outputs optimiert wird. Eine Angabe der Libraries GLFW, OpenGL und SDL ist nicht vonnöten, da sie wie erwähnt Bestandteile von Emscripten sind.

[ header = Seite 3: Ein Würfel im Browser ]

Ein Würfel im Browser

Ist der Übersetzungsvorgang abgeschlossen, findet sich eine Datei mit dem Namen qafoo3d.html im aktuellen Arbeitsverzeichnis. Im Browser aufgerufen stellt sich zunächst Ernüchterung ein. Zu sehen ist Emscriptens HTML-Grundgerüst. Von dem rotierenden Würfel auf grünem Hintergrund jedoch fehlt jede Spur (Abb. 2). Ein Blick in die JavaScript-Konsole (STRG + ALT + J in Chrome) sagt uns, warum:

Cannot find preloaded image /data/qafoo.png

Abb. 2: Erster, fehlerbehafteter Emscripten-Versuch

Der Fehler ist recht offensichtlich. Die native Version der Applikation lädt die Textur des Würfels aus dem Dateisystem. Das von Emscripten für den Browser generierte JavaScript kann natürlich nicht auf das Dateisystem zugreifen, aus diesem Grund muss eine Alternative her. Zum Glück stellt Emscripten ein virtuelles Dateisystem bereit, mit dem sich das Lesen und Schreiben von Dateien simulieren lässt, um den Quelltext einer Applikation nicht für diesen Spezialfall anpassen zu müssen. Eine kleine Veränderung in der Kommandozeile zum Erstellen der genutzten HTML-Datei kann hier Wunder bewirken:

emcc qafoo.c native_loop.c rendering.c -s LEGACY_GL_EMULATION=1 -O2 -o qafoo3d.html –-preload-file data/qafoo.png

Durch die Definition von –preload-file wird der Toolchain mitgeteilt, dass die übersetzte Applikation auf die Datei /data/qafoo.png zugreifen wird und dass sie auch innerhalb des Browsers bereit gehalten werden muss. Die Datei wird von Emscripten nun automatisch in einem simulierten Dateisystem abgelegt und ist für die Applikation vollkommen transparent erreichbar. Die erforderlichen Daten werden automatisch in einer Datei mit dem Namen qafoo3d.data abgelegt, die vor der Ausführung des Programms geladen wird. Ein erneutes Öffnen der qafoo3d.html führt leider noch immer nicht zum Ziel. Die Fehlermeldungen sind nun zwar verschwunden, leider scheint der ganze Browser mit seiner Darstellung in einer Endlosschleife zu hängen, ohne dabei den eigentlichen Würfel zu zeichnen.

Ganz ohne Anpassung geht es nicht

Ein Browser ist ein massiv anderer Ausführungskontext als eine native Applikation. Aus diesem Grund ist es durchaus beeindruckend, wie weit Emscripten ohne jegliche Anpassung des Quelltexts gekommen ist. Die angesprochene Endlosschleife ist übrigens keine Einbildung. Nein, sie steht sogar ganz deutlich im Quelltext der Applikation (Listing 4). Es ist die doRenderingLoop-Funktion, die in einer while-Schleife ein Frame nach dem nächsten zeichnet. Das Problem hierbei ist, dass ein Browser den JavaScript-Code in einem einzigen Thread ausführt. Dadurch werden die einzelnen Frames zwar gezeichnet, der Browser hat jedoch überhaupt keine Möglichkeit, sie auf dem Bildschirm darzustellen. Um dem Browser zwischen jedem Bild die nötige Verschnaufpause zu gönnen, existiert in JavaScript requestAnimationFrame, das asynchron immer dann eine Funktion aufruft, wenn ein weiteres Frame auf den Bildschirm gezeichnet werden soll. Emscripten transportiert praktischerweise eine Implementierung für Eventloops, wie die in dem Beispiel genutzte while-Schleife, in die C-Welt hinüber, die genau diese Technik verwendet. Hier der nun leicht modifizierte Quelltext der doRenderingLoop-Funktion, angepasst für Emscripten:

// ...
#include 

void doRenderingLoop(renderFunc doRendering) {
  emscripten_set_main_loop(doRendering, 0, 1);
}

emscripten_set_main_loop wird eingesetzt, um jedes Mal ein Frame zu zeichnen, wenn der Browser wieder bereit ist. Die Quelltextanpassung wird in der Datei emscripten-loop.c gespeichert. Übersetzt man nun die Applikation und ersetzt den zuvor verwendeten native-loop.c gegen die neu erstellte Funktion, erhält man eine funktionierende Version der Anwendung für den Browser:

emcc qafoo.c emscripten_loop.c rendering.c -s LEGACY_GL_EMULATION=1 -O2 -o qafoo3d.html –-preload-file data/qafoo.png

Im Browser geöffnet lässt sich der Würfel nun auch dort bestaunen (Abb. 3). Und das nahezu ohne Anpassungen des Quelltexts. Den vollständigen Quelltext der Applikation zum Anschauen, Kompilieren und Ausprobieren findet man auf GitHub.

Abb. 3: Der Qafoo-Würfel im Browser

Fazit

Es ist an der Zeit, den Browser – und damit JavaScript – als Zielplattform für diverse Anwendungen mit einzubeziehen. Es ist davon auszugehen, dass in Zukunft Applikationen, die bisher nur als native Desktopanwendungen verfügbar waren, auch im Browser auftauchen. Das gilt dank der beginnenden Verbreitung von WebGL natürlich besonders für Spiele. Denn was für das hier gezeigte einfache Beispiel wahr ist, gilt auch für deutlich komplexere grafische Anwendungen, wie Epic mit ihrer erwähnten Demo sehr erfolgreich gezeigt hat. Mit minimalen Anpassungen könnte die gezeigte Würfelapplikation auch für Android oder iOS übersetzt werden. Warum sollten sich Hersteller entsprechender Spiele also die Vermarktung im Browser entgehen lassen, wenn dadurch nur minimale Mehrkosten entstehen? JavaScript als Plattform war lange nur ein Gedankenspiel. Mittlerweile ist dieses Konzept, wenn es auch nach wie vor seltsam anmutet, Realität. JavaScript hat seinem absurden, aus Marketinggründen gewählten Namen, alle Ehre gemacht. Es hat das geschafft, was Sun mit Java immer anstrebte: eine Plattform für alle Systeme. Uns allen steht eine interessante und vielfältige Zukunft bevor, die ich mit Spannung erwarte. Denn JavaScript ist dem Traum, mit einer Sprache alle Systeme zu erreichen, näher als jede andere Sprache zuvor.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -