├── LICENSE ├── Main.class ├── Main.java ├── README.md ├── jello.py └── thumbnails ├── thumbnail-01.png └── thumbnail-02.png /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Alexey Kutepov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Main.class: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/JelloVM/7c512a27b9a590fd91bd2e59304436cca92656a1/Main.class -------------------------------------------------------------------------------- /Main.java: -------------------------------------------------------------------------------- 1 | public abstract class Main { 2 | public static void foo(int x) { } 3 | public static void foo(float x) { } 4 | 5 | public static void main(String[] args) { 6 | System.out.println("Hello, World"); 7 | System.out.println(34 + 35); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JelloVM 2 | 3 | JVM in Python that can only run ["Hello, World"](https://en.wikipedia.org/wiki/%22Hello,_World!%22_program). 4 | 5 | ## Quick Start 6 | 7 | ```console 8 | $ javac Main.java 9 | $ ./jello.py Main.class 10 | ``` 11 | 12 | ## Screencasts 13 | 14 | This was developed on a livestream: 15 | 16 | [![episode-01](./thumbnails/thumbnail-01.png)](https://www.youtube.com/watch?v=67FmRyv8jTM) 17 | [![episode-02](./thumbnails/thumbnail-02.png)](https://www.youtube.com/watch?v=anOidUQcv1w) 18 | 19 | ## References 20 | 21 | - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html 22 | - https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html 23 | -------------------------------------------------------------------------------- /jello.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import pprint 4 | import sys 5 | import io 6 | 7 | pp = pprint.PrettyPrinter() 8 | 9 | CONSTANT_Class = 7 10 | CONSTANT_Fieldref = 9 11 | CONSTANT_Methodref = 10 12 | CONSTANT_InterfaceMethodref = 11 13 | CONSTANT_String = 8 14 | CONSTANT_Integer = 3 15 | CONSTANT_Float = 4 16 | CONSTANT_Long = 5 17 | CONSTANT_Double = 6 18 | CONSTANT_NameAndType = 12 19 | CONSTANT_Utf8 = 1 20 | CONSTANT_MethodHandle = 15 21 | CONSTANT_MethodType = 16 22 | CONSTANT_InvokeDynamic = 18 23 | 24 | class_access_flags = [ 25 | ("ACC_PUBLIC" , 0x0001), 26 | ("ACC_FINAL" , 0x0010), 27 | ("ACC_SUPER" , 0x0020), 28 | ("ACC_INTERFACE" , 0x0200), 29 | ("ACC_ABSTRACT" , 0x0400), 30 | ("ACC_SYNTHETIC" , 0x1000), 31 | ("ACC_ANNOTATION" , 0x2000), 32 | ("ACC_ENUM" , 0x4000) 33 | ] 34 | 35 | method_access_flags = [ 36 | ("ACC_PUBLIC", 0x0001), 37 | ("ACC_PRIVATE", 0x0002), 38 | ("ACC_PROTECTED", 0x0004), 39 | ("ACC_STATIC", 0x0008), 40 | ("ACC_FINAL", 0x0010), 41 | ("ACC_SYNCHRONIZED", 0x0020), 42 | ("ACC_BRIDGE", 0x0040), 43 | ("ACC_VARARGS", 0x0080), 44 | ("ACC_NATIVE", 0x0100), 45 | ("ACC_ABSTRACT", 0x0400), 46 | ("ACC_STRICT", 0x0800), 47 | ("ACC_SYNTHETIC", 0x1000), 48 | ] 49 | 50 | def parse_flags(value: int, flags: [(str, int)]) -> [str]: 51 | return [name for (name, mask) in flags if (value&mask) != 0] 52 | 53 | def parse_u1(f): return int.from_bytes(f.read(1), 'big') 54 | def parse_u2(f): return int.from_bytes(f.read(2), 'big') 55 | def parse_u4(f): return int.from_bytes(f.read(4), 'big') 56 | 57 | def parse_attributes(f, count): 58 | attributes = [] 59 | for j in range(count): 60 | # attribute_info { 61 | # u2 attribute_name_index; 62 | # u4 attribute_length; 63 | # u1 info[attribute_length]; 64 | # } 65 | attribute = {} 66 | attribute['attribute_name_index'] = parse_u2(f) 67 | attribute_length = parse_u4(f) 68 | attribute['info'] = f.read(attribute_length) 69 | attributes.append(attribute) 70 | return attributes 71 | 72 | def parse_class_file(file_path): 73 | with open(file_path, "rb") as f: 74 | clazz = {} 75 | clazz['magic'] = hex(parse_u4(f)) 76 | clazz['minor'] = parse_u2(f) 77 | clazz['major'] = parse_u2(f) 78 | constant_pool_count = parse_u2(f) 79 | constant_pool = [] 80 | for i in range(constant_pool_count-1): 81 | cp_info = {} 82 | tag = parse_u1(f) 83 | if tag == CONSTANT_Methodref: 84 | cp_info['tag'] = 'CONSTANT_Methodref' 85 | cp_info['class_index'] = parse_u2(f) 86 | cp_info['name_and_type_index'] = parse_u2(f) 87 | elif tag == CONSTANT_Class: 88 | cp_info['tag'] = 'CONSTANT_Class' 89 | cp_info['name_index'] = parse_u2(f) 90 | elif tag == CONSTANT_NameAndType: 91 | cp_info['tag'] = 'CONSTANT_NameAndType' 92 | cp_info['name_index'] = parse_u2(f) 93 | cp_info['descriptor_index'] = parse_u2(f) 94 | elif tag == CONSTANT_Utf8: 95 | cp_info['tag'] = 'CONSTANT_Utf8' 96 | length = parse_u2(f); 97 | cp_info['bytes'] = f.read(length) 98 | elif tag == CONSTANT_Fieldref: 99 | cp_info['tag'] = 'CONSTANT_Fieldref' 100 | cp_info['class_index'] = parse_u2(f) 101 | cp_info['name_and_type_index'] = parse_u2(f) 102 | elif tag == CONSTANT_String: 103 | cp_info['tag'] = 'CONSTANT_String' 104 | cp_info['string_index'] = parse_u2(f) 105 | else: 106 | raise NotImplementedError(f"Unexpected constant tag {tag} in class file {file_path}") 107 | constant_pool.append(cp_info) 108 | clazz['constant_pool'] = constant_pool 109 | clazz['access_flags'] = parse_flags(parse_u2(f), class_access_flags) 110 | clazz['this_class'] = parse_u2(f) 111 | clazz['super_class'] = parse_u2(f) 112 | interfaces_count = parse_u2(f) 113 | interfaces = [] 114 | for i in range(interfaces_count): 115 | raise NotImplementedError("We don't support interfaces") 116 | clazz['interfaces'] = interfaces 117 | fields_count = parse_u2(f) 118 | fields = [] 119 | for i in range(fields_count): 120 | raise NotImplementedError("We don't support fields") 121 | clazz['fields'] = fields 122 | methods_count = parse_u2(f) 123 | methods = [] 124 | for i in range(methods_count): 125 | # u2 access_flags; 126 | # u2 name_index; 127 | # u2 descriptor_index; 128 | # u2 attributes_count; 129 | # attribute_info attributes[attributes_count]; 130 | method = {} 131 | method['access_flags'] = parse_flags(parse_u2(f), method_access_flags) 132 | method['name_index'] = parse_u2(f) 133 | method['descriptor_index'] = parse_u2(f) 134 | attributes_count = parse_u2(f) 135 | method['attributes'] = parse_attributes(f, attributes_count) 136 | methods.append(method) 137 | clazz['methods'] = methods 138 | attributes_count = parse_u2(f) 139 | clazz['attributes'] = parse_attributes(f, attributes_count) 140 | return clazz 141 | 142 | def find_methods_by_name(clazz, name: bytes): 143 | return [method 144 | for method in clazz['methods'] 145 | if clazz['constant_pool'][method['name_index'] - 1]['bytes'] == name] 146 | 147 | def find_attributes_by_name(clazz, attributes, name: bytes): 148 | return [attr 149 | for attr in attributes 150 | if clazz['constant_pool'][attr['attribute_name_index'] - 1]['bytes'] == name] 151 | 152 | def parse_code_info(info: bytes): 153 | code = {} 154 | with io.BytesIO(info) as f: 155 | # Code_attribute { 156 | # u2 attribute_name_index; 157 | # u4 attribute_length; 158 | # u2 max_stack; 159 | # u2 max_locals; 160 | # u4 code_length; 161 | # u1 code[code_length]; 162 | # u2 exception_table_length; 163 | # { u2 start_pc; 164 | # u2 end_pc; 165 | # u2 handler_pc; 166 | # u2 catch_type; 167 | # } exception_table[exception_table_length]; 168 | # u2 attributes_count; 169 | # attribute_info attributes[attributes_count]; 170 | # } 171 | code['max_stack'] = parse_u2(f) 172 | code['max_locals'] = parse_u2(f) 173 | code_length = parse_u4(f) 174 | code['code'] = f.read(code_length) 175 | exception_table_length = parse_u2(f) 176 | # NOTE: parsing the code attribute is not finished 177 | return code 178 | 179 | getstatic_opcode = 0xB2 180 | ldc_opcode = 0x12 181 | invokevirtual_opcode = 0xB6 182 | return_opcode = 0xB1 183 | bipush_opcode = 0x10 184 | 185 | def get_name_of_class(clazz, class_index: int) -> str: 186 | return clazz['constant_pool'][clazz['constant_pool'][class_index - 1]['name_index'] - 1]['bytes'].decode('utf-8') 187 | 188 | def get_name_of_member(clazz, name_and_type_index: int) -> str: 189 | return clazz['constant_pool'][clazz['constant_pool'][name_and_type_index - 1]['name_index'] - 1]['bytes'].decode('utf-8') 190 | 191 | def execute_code(clazz, code: bytes): 192 | stack = [] 193 | with io.BytesIO(code) as f: 194 | while f.tell() < len(code): 195 | opcode = parse_u1(f) 196 | if opcode == getstatic_opcode: 197 | index = parse_u2(f) 198 | fieldref = clazz['constant_pool'][index - 1] 199 | name_of_class = get_name_of_class(clazz, fieldref['class_index']) 200 | name_of_member = get_name_of_member(clazz, fieldref['name_and_type_index']) 201 | if name_of_class == 'java/lang/System' and name_of_member == 'out': 202 | stack.append({'type': 'FakePrintStream'}) 203 | else: 204 | raise NotImplementedError(f"Unsupported member {name_of_class}/{name_of_member} in getstatic instruction") 205 | elif opcode == ldc_opcode: 206 | index = parse_u1(f) 207 | stack.append({'type': 'Constant', 'const': clazz['constant_pool'][index - 1]}) 208 | elif opcode == invokevirtual_opcode: 209 | index = parse_u2(f) 210 | methodref = clazz['constant_pool'][index - 1] 211 | name_of_class = get_name_of_class(clazz, methodref['class_index']) 212 | name_of_member = get_name_of_member(clazz, methodref['name_and_type_index']); 213 | if name_of_class == 'java/io/PrintStream' and name_of_member == 'println': 214 | n = len(stack) 215 | if len(stack) < 2: 216 | raise RuntimeError('{name_of_class}/{name_of_member} expectes 2 arguments, but provided {n}') 217 | obj = stack[len(stack) - 2] 218 | if obj['type'] != 'FakePrintStream': 219 | raise NotImplementedError(f"Unsupported stream type {obj['type']}") 220 | arg = stack[len(stack) - 1] 221 | if arg['type'] == 'Constant': 222 | if arg['const']['tag'] == 'CONSTANT_String': 223 | print(clazz['constant_pool'][arg['const']['string_index'] - 1]['bytes'].decode('utf-8')) 224 | else: 225 | raise NotImplementedError(f"println for {arg['const']['tag']} is not implemented") 226 | elif arg['type'] == 'Integer': 227 | print(arg['value']) 228 | else: 229 | raise NotImplementedError(f"Support for {arg['type']} is not implemented") 230 | else: 231 | raise NotImplementedError(f"Unknown method {name_of_class}/{name_of_member} in invokevirtual instruction") 232 | elif opcode == return_opcode: 233 | return 234 | elif opcode == bipush_opcode: 235 | byte = parse_u1(f) 236 | stack.append({'type': 'Integer', 'value': byte}) 237 | else: 238 | raise NotImplementedError(f"Unknown opcode {hex(opcode)}") 239 | 240 | if __name__ == '__main__': 241 | program, *args = sys.argv 242 | if len(args) == 0: 243 | print(f"Usage: {program} ") 244 | print(f"ERROR: no path to Main.class was provided") 245 | exit(1) 246 | file_path, *args = args 247 | clazz = parse_class_file(file_path) 248 | [main] = find_methods_by_name(clazz, b'main') 249 | [code] = find_attributes_by_name(clazz, main['attributes'], b'Code') 250 | code_attrib = parse_code_info(code['info']) 251 | execute_code(clazz, code_attrib['code']) 252 | -------------------------------------------------------------------------------- /thumbnails/thumbnail-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/JelloVM/7c512a27b9a590fd91bd2e59304436cca92656a1/thumbnails/thumbnail-01.png -------------------------------------------------------------------------------- /thumbnails/thumbnail-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsoding/JelloVM/7c512a27b9a590fd91bd2e59304436cca92656a1/thumbnails/thumbnail-02.png --------------------------------------------------------------------------------