Fluid Byte hat geschrieben:Klingt sehr interessant. Auch wenn ich natürlich Kosten/Nutzen abwäge und sowas für die meisten Projekte nicht in Frage kommt
hätte ich Lust mich damit mit mal näher zu befassen. Gibt es da entsprechende Tutorials oder eine Art Crashkurs um das Ganze mal
besser zu veranschaulichen?
Da steckt nicht besonders viel dahinter: Du musst dir nur überlegen, wie dein Bytecode aussehen soll (Instruktionen und ihre Parameter) und dann besteht der Interpreter für die Virtuelle Maschine einfach daraus den Bytecode zu lesen und die entsprechenden Instruktionen auszuführen.
Kleines Beispiel:
Code: Alles auswählen
Enumeration
#ILiteral ; argument: 1-byte literal
#IPush ; no arguments
#IPop
#IAdd
#ISub
#IMul
#IDiv
EndEnumeration
Procedure Interpreter(*Code.BYTE, *CodeEnd)
Protected Dim Stack(50)
Protected Index = 0
Protected Accumulator = 0
While *Code < *CodeEnd
Select *Code\b
Case #ILiteral
Accumulator = PeekB(*Code+1)
*Code + 2
Case #IPush
If Index = 50
Debug "Stack overflow"
Break
EndIf
Index + 1
Stack(Index) = Accumulator
*Code + 1
Case #IPop
If Index = 0
Debug "Stack underflow"
Break
EndIf
Accumulator = Stack(Index)
Index - 1
*Code + 1
Case #IAdd
Accumulator = Stack(Index) + Accumulator
Index - 1
*Code + 1
Case #ISub
Accumulator = Stack(Index) - Accumulator
Index - 1
*Code + 1
Case #IMul
Accumulator = Stack(Index) * Accumulator
Index - 1
*Code + 1
Case #IDiv
Accumulator = Stack(Index) / Accumulator
Index - 1
*Code + 1
Default
Debug "Invalid Instruction"
Break
EndSelect
Wend
ProcedureReturn Accumulator
EndProcedure
Debug Interpreter(?Code1, ?CodeEnd1)
Debug Interpreter(?Code2, ?CodeEnd2)
DataSection
; Code: (((5 * 7) + (10 - 3)) * 2) = 84
;
Code1:
Data.b #ILiteral, 5
Data.b #IPush
Data.b #ILiteral, 7
Data.b #IMul
Data.b #IPush
Data.b #ILiteral, 10
Data.b #IPush
Data.b #ILiteral, 3
Data.b #ISub
Data.b #IAdd
Data.b #IPush
Data.b #ILiteral, 2
Data.b #IMul
CodeEnd1:
; Code: 2^5 = 32
Code2:
Data.b #ILiteral, 2
Data.b #IPush
Data.b #ILiteral, 2
Data.b #IMul
Data.b #IPush
Data.b #ILiteral, 2
Data.b #IMul
Data.b #IPush
Data.b #ILiteral, 2
Data.b #IMul
Data.b #IPush
Data.b #ILiteral, 2
Data.b #IMul
CodeEnd2:
EndDataSection
Der ByteCode besteht aus 1-byte für die Instruktion, und noch ein weiteres Parameter für #ILiteral, alle anderen Instruktionen haben keine Parameter in ByteCode. Damit kann der Interpreter einfach ein Select auf dem aktuellen code-byte machen um die aktuelle Instruktion zu ermitteln. Die Architektur der mini-VM ist eine Akkumulator-Maschine, das heißt ein Parameter für eine Instruktion wie #IAdd ist immer das Akkumulator-Register und da landet auch das Ergebnis. Weitere Parameter kommen von dem Stack. Man kann hier jede Architektur nachbauen die man will, ich habe diese verwendet weil sie einen sehr minimalen ByteCode braucht und schnell umzusetzen ist.
Das Beispiel macht natürlich noch nicht besonders viel, aber wenn man noch ein paar mehr Instruktionen einbaut wie zum Beispiel Sprünge und bedingte Sprünge dann kann man schnell kleine Funktionen in dem ByteCode schreiben. Vom Kern her ist das eigentlich echt keine große Sache, es kommt halt drauf an wie weit man die Sache treiben will. Wenn man das Ganze komfortabel nutzen will braucht man schon einen Assembler/Disassember dazu, wenn nicht sogar gleich einen Compiler der den ByteCode erzeugt.