simplest start of a jvm
This commit is contained in:
parent
b06a20a7b1
commit
148f515d33
6 changed files with 186 additions and 54 deletions
BIN
Dummy.class
BIN
Dummy.class
Binary file not shown.
20
Dummy.java
20
Dummy.java
|
|
@ -1,22 +1,6 @@
|
||||||
package dummy;
|
|
||||||
|
|
||||||
public class Dummy {
|
public class Dummy {
|
||||||
|
|
||||||
private final static String constant = "meh";
|
public static int get(){
|
||||||
private final int integer = 57;
|
return 42;
|
||||||
|
|
||||||
private final String name;
|
|
||||||
|
|
||||||
public Dummy(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void print(){
|
|
||||||
System.out.println(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
src/lib.rs
31
src/lib.rs
|
|
@ -1,6 +1,8 @@
|
||||||
pub mod types;
|
pub mod types;
|
||||||
mod io;
|
mod io;
|
||||||
|
pub mod opcodes;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use crate::io::{read_f32, read_f64, read_i32, read_i64, read_u16, read_u32};
|
use crate::io::{read_f32, read_f64, read_i32, read_i64, read_u16, read_u32};
|
||||||
use crate::types::{AttributeType, Class, MethodCode, Exception, Field, Method};
|
use crate::types::{AttributeType, Class, MethodCode, Exception, Field, Method};
|
||||||
|
|
@ -38,18 +40,19 @@ pub fn get_class(bytecode: Vec<u8>) -> Option<Class> {
|
||||||
|
|
||||||
let methods_count = read_u16(&bytecode, index);
|
let methods_count = read_u16(&bytecode, index);
|
||||||
index += 2;
|
index += 2;
|
||||||
let mut methods = vec![];
|
let mut methods = HashMap::new();
|
||||||
for _ in 0..methods_count {
|
for _ in 0..methods_count {
|
||||||
methods.push(read_method(constant_pool.clone(), &mut index, &bytecode));
|
let m = read_method(constant_pool.clone(), &mut index, &bytecode);
|
||||||
|
methods.insert(m.name(), m);
|
||||||
}
|
}
|
||||||
|
|
||||||
let attributes_count = read_u16(&bytecode, index);
|
let attributes_count = read_u16(&bytecode, index);
|
||||||
index += 2;
|
index += 2;
|
||||||
let mut attributes = vec![];
|
let mut attributes = HashMap::new();
|
||||||
for _ in 0..attributes_count {
|
for _ in 0..attributes_count {
|
||||||
let some = read_attribute(constant_pool.clone(), &bytecode, &mut index);
|
let some = read_attribute(constant_pool.clone(), &bytecode, &mut index);
|
||||||
if let Some(att) = some {
|
if let Some(att) = some {
|
||||||
attributes.push(att);
|
attributes.insert(att.0, att.1);
|
||||||
} else {
|
} else {
|
||||||
panic!(); // bug/not-implemented
|
panic!(); // bug/not-implemented
|
||||||
}
|
}
|
||||||
|
|
@ -154,10 +157,10 @@ fn read_field(constant_pool: Rc<Vec<CpEntry>>, index: &mut usize, bytecode: &[u8
|
||||||
let descriptor_index = read_u16(bytecode, *index + 4) as usize;
|
let descriptor_index = read_u16(bytecode, *index + 4) as usize;
|
||||||
let attributes_count = read_u16(bytecode, *index + 6);
|
let attributes_count = read_u16(bytecode, *index + 6);
|
||||||
*index += 8;
|
*index += 8;
|
||||||
let mut attributes = vec![];
|
let mut attributes = HashMap::new();
|
||||||
for _ in 0..attributes_count {
|
for _ in 0..attributes_count {
|
||||||
if let Some(att) = read_attribute(constant_pool.clone(), bytecode, index) {
|
if let Some(att) = read_attribute(constant_pool.clone(), bytecode, index) {
|
||||||
attributes.push(att);
|
attributes.insert(att.0, att.1);
|
||||||
} else {
|
} else {
|
||||||
panic!(); // bug/not-implemented
|
panic!(); // bug/not-implemented
|
||||||
}
|
}
|
||||||
|
|
@ -178,10 +181,10 @@ fn read_method(constant_pool: Rc<Vec<CpEntry>>, index: &mut usize, bytecode: &[u
|
||||||
let attributes_count = read_u16(bytecode, *index + 6);
|
let attributes_count = read_u16(bytecode, *index + 6);
|
||||||
*index += 8;
|
*index += 8;
|
||||||
|
|
||||||
let mut attributes = vec![];
|
let mut attributes = HashMap::new();
|
||||||
for _ in 0..attributes_count {
|
for _ in 0..attributes_count {
|
||||||
if let Some(att) = read_attribute(constant_pool.clone(), bytecode, index) {
|
if let Some(att) = read_attribute(constant_pool.clone(), bytecode, index) {
|
||||||
attributes.push(att);
|
attributes.insert(att.0, att.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,7 +197,7 @@ fn read_method(constant_pool: Rc<Vec<CpEntry>>, index: &mut usize, bytecode: &[u
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_attribute(constant_pool: Rc<Vec<CpEntry>>, bytecode: &[u8], index: &mut usize) -> Option<AttributeType> {
|
fn read_attribute(constant_pool: Rc<Vec<CpEntry>>, bytecode: &[u8], index: &mut usize) -> Option<(String, AttributeType)> {
|
||||||
let attribute_name_index = read_u16(bytecode, *index) as usize;
|
let attribute_name_index = read_u16(bytecode, *index) as usize;
|
||||||
*index += 2;
|
*index += 2;
|
||||||
let attribute_length = read_u32(bytecode, *index) as usize;
|
let attribute_length = read_u32(bytecode, *index) as usize;
|
||||||
|
|
@ -208,7 +211,7 @@ fn read_attribute(constant_pool: Rc<Vec<CpEntry>>, bytecode: &[u8], index: &mut
|
||||||
return match s.as_str() {
|
return match s.as_str() {
|
||||||
"ConstantValue" => {
|
"ConstantValue" => {
|
||||||
assert_eq!(info.len(), 2);
|
assert_eq!(info.len(), 2);
|
||||||
Some(AttributeType::ConstantValue(read_u16(&info, 0)))
|
Some(("ConstantValue".into(), AttributeType::ConstantValue(read_u16(&info, 0))))
|
||||||
}
|
}
|
||||||
"Code" => {
|
"Code" => {
|
||||||
let max_stack = read_u16(&info, 0);
|
let max_stack = read_u16(&info, 0);
|
||||||
|
|
@ -225,15 +228,15 @@ fn read_attribute(constant_pool: Rc<Vec<CpEntry>>, bytecode: &[u8], index: &mut
|
||||||
}
|
}
|
||||||
let attribute_count = read_u16(&info, code_index);
|
let attribute_count = read_u16(&info, code_index);
|
||||||
code_index += 2;
|
code_index += 2;
|
||||||
let mut code_attributes = vec![];
|
let mut code_attributes = HashMap::new();
|
||||||
for _ in 0..attribute_count {
|
for _ in 0..attribute_count {
|
||||||
if let Some(att) = read_attribute(constant_pool.clone(), &info, &mut code_index) {
|
if let Some(att) = read_attribute(constant_pool.clone(), &info, &mut code_index) {
|
||||||
code_attributes.push(att);
|
code_attributes.insert(att.0, att.1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(AttributeType::Code(MethodCode::new(max_stack, max_locals, code, exception_table, code_attributes)))
|
Some(("Code".into(), AttributeType::Code(MethodCode::new(max_stack, max_locals, code, exception_table, code_attributes))))
|
||||||
}
|
}
|
||||||
"SourceFile" => Some(AttributeType::SourceFile),
|
"SourceFile" => Some(("SourceFile".into(), AttributeType::SourceFile)),
|
||||||
_ => None
|
_ => None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@ use std::io::Read;
|
||||||
fn main() {
|
fn main() {
|
||||||
let bytecode = read_class_file("./Dummy.class");
|
let bytecode = read_class_file("./Dummy.class");
|
||||||
if let Some(class) = classfile_reader::get_class(bytecode){
|
if let Some(class) = classfile_reader::get_class(bytecode){
|
||||||
println!("{:?}", class);
|
let ret = class.execute("public static get()I");
|
||||||
|
println!("{:?}", ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_class_file(name: &str) -> Vec<u8> {
|
fn read_class_file(name: &str) -> Vec<u8> {
|
||||||
|
|
|
||||||
97
src/opcodes.rs
Normal file
97
src/opcodes.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
pub const aconst_null: u8 = 1; // (0x01)
|
||||||
|
|
||||||
|
pub const fconst_0: u8 = 11; // (0xb) Push float 0
|
||||||
|
pub const fconst_1: u8 = 12; // (0xc) Push float 1
|
||||||
|
pub const fconst_2: u8 = 13; // (0xd) Push float 2
|
||||||
|
pub const dconst_0:u8 = 14; // (0xe) push double 0
|
||||||
|
pub const dconst_1:u8 = 15; // (0xf) push double 1
|
||||||
|
pub const bipush:u8 = 16; // (0x10) Push byte
|
||||||
|
|
||||||
|
pub const ldc: u8 = 18; // (0x12) Push item from run-time pub constant pool
|
||||||
|
pub const fload:u8 = 23; // (0x17) Load float from local variable
|
||||||
|
pub const dload:u8 = 24; // (0x18) load double from local variable
|
||||||
|
pub const aload:u8 = 25; //0x19
|
||||||
|
|
||||||
|
pub const fload_0:u8 = 34; // (0x22) Load float 0 from local variable
|
||||||
|
pub const fload_1:u8 = 35; // (0x23) Load float 1 from local variable
|
||||||
|
pub const fload_2:u8 = 36; // (0x24) Load float 2 from local variable
|
||||||
|
pub const fload_3:u8 = 37; // (0x25) Load float 3 from local variable
|
||||||
|
pub const dload_0:u8 = 38; // (0x26) Load double 0 from local variable
|
||||||
|
pub const dload_1:u8 = 39; // (0x27) Load double 1 from local variable
|
||||||
|
pub const dload_2:u8 = 40; // (0x28) Load double 2 from local variable
|
||||||
|
pub const dload_3:u8 = 41; // (0x29) Load double 3 from local variable
|
||||||
|
pub const aload_0:u8 = 42;// (0x2a)
|
||||||
|
pub const aload_1:u8 = 43;// (0x2a)
|
||||||
|
pub const aload_2:u8 = 44;// (0x2b)
|
||||||
|
pub const aload_3:u8 = 45;// (0x2c)
|
||||||
|
|
||||||
|
pub const faload: u8 = 48; // (0x30) Load float from array
|
||||||
|
pub const daload:u8 = 49; // (0x31) load double from array
|
||||||
|
pub const aaload: u8 = 50; // (0x3d)
|
||||||
|
pub const baload: u8 = 51; //(0x33)
|
||||||
|
pub const caload: u8 = 52; // (0x34)
|
||||||
|
|
||||||
|
pub const fstore: u8 = 56; // (0x38) Store float into local variable
|
||||||
|
pub const dstore: u8 = 57; // (0x39) store double in local variable
|
||||||
|
pub const astore:u8 = 58; // (0x3a)
|
||||||
|
|
||||||
|
pub const dstore_0: u8 = 71; // (0x47) store double 0 in local variable
|
||||||
|
pub const dstore_1: u8 = 72; // (0x48) store double 1 in local variable
|
||||||
|
pub const dstore_2: u8 = 73; // (0x49) store double 2 in local variable
|
||||||
|
pub const dstore_3: u8 = 74; // (0x4a) store double 3 in local variable
|
||||||
|
pub const astore_0: u8 = 75; // (0x4b)
|
||||||
|
pub const astore_1: u8 = 76; // (0x4c)
|
||||||
|
pub const astore_2: u8 = 77; // (0x4d)
|
||||||
|
pub const astore_3: u8 = 78; // (0x4e)
|
||||||
|
pub const fastore: u8 = 81; // (0x51) Store into float array
|
||||||
|
pub const dastore:u8 = 82; //(0x52) store into double array
|
||||||
|
pub const aastore: u8 = 83; // (0x53)
|
||||||
|
|
||||||
|
pub const bastore:u8 = 84; // (0x54)
|
||||||
|
|
||||||
|
pub const castore:u8 = 85; // (0x55)
|
||||||
|
pub const dup:u8 = 89; // (0x59) duplicate the top operand stack value
|
||||||
|
pub const dup_x1: u8 = 90; // (0x5a) Duplicate the top operand stack value and insert two values down
|
||||||
|
pub const dup_x2: u8 = 91; // (0x5b) Duplicate the top operand stack value and insert two or three values down
|
||||||
|
pub const dup2: u8 = 92; // (0x5c) Duplicate the top one or two operand stack values
|
||||||
|
pub const dup2_x1: u8 = 93; //(0x5d) Duplicate the top one or two operand stack values and insert two or three values down
|
||||||
|
pub const dup2_x2:u8 = 94; // (0x5e) Duplicate the top one or two operand stack values and insert two, three, or four values down
|
||||||
|
pub const fadd: u8 = 98; // (0x62) Add float
|
||||||
|
pub const dadd: u8 = 99; // (0x63) add double
|
||||||
|
|
||||||
|
pub const dsub:u8 = 103; // (0x67) subtract double
|
||||||
|
pub const fmul: u8 = 106; // (0x6a) Multiply float
|
||||||
|
pub const dmul: u8 = 107; // (0x6b) Multiply double
|
||||||
|
|
||||||
|
pub const fdiv: u8 = 110; // (0x6e) Divide float
|
||||||
|
pub const ddiv:u8 = 111; // (0x6f) divide double
|
||||||
|
pub const frem: u8 = 114; // (0x72) Remainder float
|
||||||
|
pub const drem: u8 = 115; // (0x73) remainder double
|
||||||
|
pub const fneg: u8 = 118; // (0x76) Negate float
|
||||||
|
pub const dneg: u8 = 119; // (0x77) Negate double
|
||||||
|
pub const f2i: u8 = 139; // (0x8b) Convert float to int
|
||||||
|
pub const f2l: u8 = 140; // (0x8c) Convert float to long
|
||||||
|
pub const f2d: u8 = 141; // (0x8d) Convert float to double
|
||||||
|
pub const d2i:u8 = 142; // (0x8e) double to int
|
||||||
|
pub const d2l:u8 = 143; // (0x8f) double to long
|
||||||
|
pub const d2f: u8 = 144; // (0x90) double to float
|
||||||
|
pub const fcmpl:u8 = 149; // (0x95) Compare float (less than)
|
||||||
|
pub const fcmpg: u8 = 150; // (0x96) Compare float (greater than)
|
||||||
|
pub const dcmpl:u8 = 151; // (0x97) compare double (less than)
|
||||||
|
pub const dcmpg:u8 = 152; // (0x98) compare double (greater than)
|
||||||
|
|
||||||
|
pub const ireturn: u8 = 172; // (0xac) ireturn
|
||||||
|
pub const freturn: u8 = 174; // (0xae) Return float from method
|
||||||
|
pub const dreturn: u8 = 175; // (0xaf) Return double from method
|
||||||
|
pub const areturn: u8 = 176; //(0xb0) return reference
|
||||||
|
pub const return_v: u8 = 177; // (0xb1) Return void from method (actually 'return' but that's a keyword)
|
||||||
|
pub const invokevirtual: u8 = 182; // (0xb6) Invoke instance method; dispatch based on class
|
||||||
|
|
||||||
|
pub const getstatic: u8 = 178; // (0xb2) Get static field from class
|
||||||
|
pub const anewarray: u8 = 189; // (0xbd)
|
||||||
|
|
||||||
|
pub const arraylength: u8 = 190; // (0xbe)
|
||||||
|
|
||||||
|
pub const athrow: u8 = 191; // (0xbf)
|
||||||
|
|
||||||
|
pub const checkcast: u8 = 192; // (0xc0)
|
||||||
88
src/types.rs
88
src/types.rs
|
|
@ -1,5 +1,6 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use crate::CpEntry;
|
use crate::{CpEntry, opcodes};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
//TODO create factory function
|
//TODO create factory function
|
||||||
|
|
@ -12,14 +13,19 @@ pub struct Class {
|
||||||
pub super_class: u16,
|
pub super_class: u16,
|
||||||
pub interfaces: Vec<u16>,
|
pub interfaces: Vec<u16>,
|
||||||
pub fields: Vec<Field>,
|
pub fields: Vec<Field>,
|
||||||
pub methods: Vec<Method>,
|
pub methods: HashMap<String, Method>,
|
||||||
pub attributes: Vec<AttributeType>,
|
pub attributes: HashMap<String, AttributeType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Class {
|
impl Class {
|
||||||
pub fn get_version(&self) -> (u16, u16) {
|
pub fn get_version(&self) -> (u16, u16) {
|
||||||
(self.major_version, self.minor_version)
|
(self.major_version, self.minor_version)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn execute(&self, method_name: &str) -> Value {
|
||||||
|
let m = self.methods.get(method_name).unwrap();
|
||||||
|
m.execute().unwrap() //TODO remove unwrap
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Method {
|
pub struct Method {
|
||||||
|
|
@ -27,7 +33,7 @@ pub struct Method {
|
||||||
access_flags: u16,
|
access_flags: u16,
|
||||||
name_index: usize,
|
name_index: usize,
|
||||||
descriptor_index: usize,
|
descriptor_index: usize,
|
||||||
attributes: Vec<AttributeType>,
|
attributes: HashMap<String, AttributeType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Method {
|
impl fmt::Debug for Method {
|
||||||
|
|
@ -42,7 +48,7 @@ impl Method {
|
||||||
access_flags: u16,
|
access_flags: u16,
|
||||||
name_index: usize,
|
name_index: usize,
|
||||||
descriptor_index: usize,
|
descriptor_index: usize,
|
||||||
attributes: Vec<AttributeType>, ) -> Self {
|
attributes: HashMap<String, AttributeType>, ) -> Self {
|
||||||
Method { constant_pool, access_flags, name_index, descriptor_index, attributes }
|
Method { constant_pool, access_flags, name_index, descriptor_index, attributes }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,16 +65,29 @@ impl Method {
|
||||||
full_name
|
full_name
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn get_code(&self) {
|
pub fn execute(&self) -> Option<Value> {
|
||||||
// for att in &self.attributes {
|
if let AttributeType::Code(code) = self.attributes.get("Code").unwrap() {
|
||||||
// if let CpEntry::Utf8(_, str) = &self.constant_pool[&att.attribute_name_index - 1] {
|
let mut stack = Stack::new();
|
||||||
// println!("{}", str);
|
let mut pc: usize = 0;
|
||||||
// if str == "Code" {
|
while pc < code.opcodes.len() {
|
||||||
// println!("{:?}", att.info);
|
let opcode = &code.opcodes[pc];
|
||||||
// }
|
match opcode {
|
||||||
// }
|
&opcodes::bipush => {
|
||||||
// }
|
pc += 1;
|
||||||
// }
|
let c = code.opcodes[pc] as i32;
|
||||||
|
stack.push(Value::I32(c));
|
||||||
|
},
|
||||||
|
&opcodes::ireturn => {
|
||||||
|
return stack.pop();
|
||||||
|
},
|
||||||
|
//TODO implement all opcodes
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pc += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None // TODO error situation
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
|
|
@ -76,10 +95,11 @@ pub struct Field {
|
||||||
access_flags: u16,
|
access_flags: u16,
|
||||||
name_index: usize,
|
name_index: usize,
|
||||||
descriptor_index: usize,
|
descriptor_index: usize,
|
||||||
attributes: Vec<AttributeType>,
|
attributes: HashMap<String, AttributeType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::hash::Hash;
|
||||||
use crate::io::read_u16;
|
use crate::io::read_u16;
|
||||||
|
|
||||||
impl fmt::Debug for Field {
|
impl fmt::Debug for Field {
|
||||||
|
|
@ -94,7 +114,7 @@ impl Field {
|
||||||
access_flags: u16,
|
access_flags: u16,
|
||||||
name_index: usize,
|
name_index: usize,
|
||||||
descriptor_index: usize,
|
descriptor_index: usize,
|
||||||
attributes: Vec<AttributeType>, ) -> Self {
|
attributes: HashMap<String, AttributeType>, ) -> Self {
|
||||||
Field { constant_pool, access_flags, name_index, descriptor_index, attributes: attributes }
|
Field { constant_pool, access_flags, name_index, descriptor_index, attributes: attributes }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -193,16 +213,42 @@ impl Exception {
|
||||||
pub struct MethodCode {
|
pub struct MethodCode {
|
||||||
max_stack: u16,
|
max_stack: u16,
|
||||||
max_locals: u16,
|
max_locals: u16,
|
||||||
code: Vec<u8>,
|
opcodes: Vec<u8>,
|
||||||
exception_table: Vec<Exception>,
|
exception_table: Vec<Exception>,
|
||||||
code_attributes: Vec<AttributeType>,
|
code_attributes: HashMap<String, AttributeType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MethodCode {
|
impl MethodCode {
|
||||||
pub(crate) fn new(max_stack: u16, max_locals: u16,
|
pub(crate) fn new(max_stack: u16, max_locals: u16,
|
||||||
code: Vec<u8>,
|
code: Vec<u8>,
|
||||||
exception_table: Vec<Exception>,
|
exception_table: Vec<Exception>,
|
||||||
code_attributes: Vec<AttributeType>) -> Self {
|
code_attributes: HashMap<String, AttributeType>) -> Self {
|
||||||
Self { max_stack, max_locals, code, exception_table, code_attributes }
|
Self { max_stack, max_locals, opcodes: code, exception_table, code_attributes }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Stack {
|
||||||
|
data: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stack {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
data: vec![]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push(&mut self, val: Value) {
|
||||||
|
self.data.push(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop(&mut self) -> Option<Value> {
|
||||||
|
self.data.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Value {
|
||||||
|
Void,
|
||||||
|
I32(i32),
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue