In diesem Artikel zur Entstehung von Jaspy, einem Python Interpreter in JavaScript, geht es um den Python Bytecode und die virtuelle Maschine, die diesen ausführt. Zum vorherigen Artikel.

Geschriebener Python-Quellcode wird nicht direkt vom Interpreter ausgeführt, sondern zunächst, in einem Zwischenschritt, zu Bytecode übersetzt. Dies ist vergleichbar mit dem Kompilierungsvorgang für einen Prozessor, der eine bestimmte Menge an Instruktionen versteht. Der resultierende Bytecode wird anschließend innerhalb einer virtuellen Maschine (VM), die eben jenen Befehlssatz versteht, ausgeführt.

Theoretisches Modell

Allgemein gibt es verschiedene Möglichkeiten eine Maschine, wie zum Beispiel einen herkömmlichen Computer, zu konstruieren. Bei x86- oder MIPS-Prozessoren handelt es sich beispielsweise um Registermaschinen. Es gibt also eine Menge an Registern und eine Menge an Instruktionen, die bestimmte Operationen auf den Registern durchführen. Zusätzlich dazu existiert ebenfalls noch ein Speicher, in den Werte geschrieben oder aus dem heraus Werte gelesen werden können.

Ein Programm ist dann eine Folge von Instruktionen für eine gegebene Maschine. Wird ein Programm ausgeführt, wird es zunächst Instruktion für Instruktion abgearbeitet. Um auch Sprünge zu ermöglichen gibt es einen Zähler (Program Counter, PC) der die Position im Programm angibt, an der sich die Maschine aktuell befindet. Ein Sprungbefehl verändert nun einfach den Program Counter entsprechend.

Bei der virtuellen Maschine von Python handelt es sich um eine Stapel-Maschine mit Speicher. Es gibt also einen Stapel (im Folgenden Stack) auf dem Objekte abgelegt werden können, eine Menge an Instruktionen, die dann Operationen auf diesen Objekten durchführen, sowie einen Speicher in dem Objekte unter einem bestimmten Bezeichner abgelegt werden können. Betrachten wir dazu einmal folgendes Beispiel:

3 + x

Übersetzt in Instruktionen für eine entsprechende Stapel-Maschine sieht das dann folgendermaßen aus:

LOAD_CONST       3
LOAD_NAME        x
BINARY_ADD

Bei der Ausführung dieses Programms wird also zunächst die Konstante „3“ geladen und danach das an den Bezeichner „x“ gebundene Objekt, beispielsweise die Zahl Vier. Nach der Ausführung der ersten beiden Instruktionen sieht der Stack dann so aus:

[3, 4]

Die Instruktion BINARY_ADD nimmt nun die beiden oberen Objekte vom Stack herunter, addiert sie und legt das Ergebnis anschließend wieder auf dem Stack ab, der dann also abschließend so aussieht:

[7]

Eine Instruktion für die Stapel-Maschine kann ein Argument besitzen, wie bei LOAD_CONST und LOAD_NAME, oder nicht, wie bei BINARY_ADD.

Soweit zum theoretischen Modell einer Stapel-Maschine.

Bytecode

Jedes Python-Programm wird vom CPython Interpreter also zunächst in eine entsprechende Folge an Instruktionen übersetzt. Die dann in eine binäre Form, den Bytecode, kodiert wird. Jedes Code-Objekt hat ein Feld co_code, das den Bytecode enthält:

>>> compile('3 + x', '<string>', 'eval').co_code
b'd\x00\x00e\x00\x00\x17S'

Mithilfe des Moduls dis lässt sich der Bytecode dekodieren:

>>> dis.dis(compile('3 + x', '<string>', 'eval'))
  1           0 LOAD_CONST               0 (3)
              3 LOAD_NAME                0 (x)
              6 BINARY_ADD
              7 RETURN_VALUE

In unserem JavaScript Python-Code-Objekt steht uns der Bytecode durch das Feld bytecode als Zeichenkette zur Verfügung. Um diesen später auch ausführen zu können, müssen wir allerdings zunächst verstehen, wie die Folge an Instruktionen kodiert wird.

Wie bereits festgestellt kann eine Instruktion ein Argument haben. Sie besteht also maximal aus zwei Teilen, dem Opcode (LOAD_CONST, …), der angibt was getan werden soll und dem optionalen Argument. Die Opcodes sind alle durchnummeriert (siehe opcode.py), wobei alle Opcodes ab der Nummer dis.HAVE_ARGUMENT ein Argument besitzen. Diese Konstanten werden in Jaspy in der Datei jaspy/constants.js vorgehalten.

Eine Instruktion wird dann wie folgt kodiert. Das erste Byte gibt den Opcode der Instruktion an. Hat eine Instruktion ein Argument, so geben die beiden nachfolgenden Bytes den Wert des Arguments an (Kodierung erfolgt in Little-Endian Reihenfolge). Ein Argument ist also erst einmal nur eine Zahl. Hier kommen dann die zusätzlichen Informationen aus dem Code-Objekt ins Spiel. Beispielsweise gibt im Fall von LOAD_NAME das Argument den Index an unter welchem der eigentliche Bezeichner im Feld names (bzw. co_names in CPython) des Code-Objekts zu finden ist.

Der Bytecode ergibt sich schließlich durch die Aneinanderreihung der Kodierungen der einzelnen Instruktion.

In Jaspy kümmert sich die Funktion disassemble in der Datei jaspy/dis.js darum den Bytecode in einzelne Instruktionen zu zerlegen. Die Ziele von Sprungbefehlen werden dabei ebenfalls entsprechend angepasst, sodass sie den Index der Ziel Instruktion angeben anstelle der Position in der binären Kodierung.

Wir erweitern außerdem die Python-Code-Objekte noch um ein Feld um die zerlegten Instruktionen zu speichern:

this.instructions = disassemble(this);

Bevor wir nun die virtuelle Maschine bauen können, die die Instruktionen letztendlich ausführt, müssen wir uns im nächsten Schritt zuerst noch überlegen, wie Python-Objekte in JavaScript repräsentiert werden sollen.