diff --git a/CLI/AssemblyBuilder.cs b/CLI/AssemblyBuilder.cs index df3f270..77377c1 100755 --- a/CLI/AssemblyBuilder.cs +++ b/CLI/AssemblyBuilder.cs @@ -1,11 +1,9 @@ -using System.Collections.Concurrent; using System.Collections.Frozen; using System.Diagnostics; using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; using JCIL.CLI.Types; -using JCIL.CLI.Types.java.lang; using JCIL.CLI.Types.Java.Lang; using JCIL.Core; using JCIL.Java.Class; @@ -27,11 +25,13 @@ public sealed class AssemblyBuilder public readonly ModuleBuilder Module; private readonly Dictionary> _casts = []; private readonly Dictionary _classes = []; + internal readonly TypeLoader TypeLoader; public int CompiledMethods { get; private set; } - public AssemblyBuilder(AssemblyName name, bool runnable) + public AssemblyBuilder(TypeLoader typeLoader, AssemblyName name, bool runnable) { + TypeLoader = typeLoader; Assembly = runnable ? System.Reflection.Emit.AssemblyBuilder.DefineDynamicAssembly(name, AssemblyBuilderAccess.RunAndCollect) : new PersistedAssemblyBuilder(name, typeof(int).Assembly); @@ -114,9 +114,10 @@ public sealed class AssemblyBuilder } } - internal void CompileOpCodes(Class javaClass, MethodBase methodBase, ILGenerator il, Code code, - OpCodeReader opcodes) + internal void CompileOpCodes(Class javaClass, MethodBase methodBase, ILGenerator il, Code code) { + using var opcodes = code.CreateOpCodeReader(); + var thisType = methodBase.DeclaringType!; if (methodBase is MethodBuilder methodBuilder) { @@ -266,7 +267,6 @@ public sealed class AssemblyBuilder break; } - case OpCodeMnemonic.FLoad: case OpCodeMnemonic.FLoad0: case OpCodeMnemonic.FLoad1: diff --git a/CLI/CodeAnalysis.cs b/CLI/CodeAnalysis.cs new file mode 100644 index 0000000..013de88 --- /dev/null +++ b/CLI/CodeAnalysis.cs @@ -0,0 +1,258 @@ +using System.Diagnostics; +using System.Runtime.CompilerServices; +using JCIL.Java.Class; + +namespace JCIL.CLI; + +public sealed class Block +{ + public Class? CatchType { get; init; } + public required ReadOnlyMemory OpCodes { get; init; } + public IReadOnlyList Locals { get; private set; } = []; + public IReadOnlyList Stack { get; private set; } = []; + + public int Start => OpCodes.Span[0].Position; + + public override string ToString() + { + return $"Block_{OpCodes.Span[0].Position:X04}"; + } +} + +public sealed class CodeAnalysis +{ + public required SortedDictionary Blocks { get; init; } + + public static CodeAnalysis Analyze(TypeLoader loader, Method method) + { + if (!method.TryGetCode(out var code)) + throw new ArgumentException("Method does not have a body."); + + using var reader = code.CreateOpCodeReader(); + var opcodes = reader.ToArray(); + + Class? catchType = null; + var blocks = new SortedDictionary(); + for (int i = 0, start = 0, count = 0; i < opcodes.Length; i++, count++) + { + ref readonly var opcode = ref opcodes[i]; + switch (opcode.Mnemonic) + { + case OpCodeMnemonic.IfEq: + case OpCodeMnemonic.IfNe: + case OpCodeMnemonic.IfLt: + case OpCodeMnemonic.IfGt: + case OpCodeMnemonic.IfLe: + case OpCodeMnemonic.IfGe: + case OpCodeMnemonic.IfICmpEq: + case OpCodeMnemonic.IfICmpNe: + case OpCodeMnemonic.IfICmpLt: + case OpCodeMnemonic.IfICmpGt: + case OpCodeMnemonic.IfICmpLe: + case OpCodeMnemonic.IfICmpGe: + case OpCodeMnemonic.IfACmpEq: + case OpCodeMnemonic.IfACmpNe: + case OpCodeMnemonic.Goto: + case OpCodeMnemonic.IReturn: + case OpCodeMnemonic.LReturn: + case OpCodeMnemonic.FReturn: + case OpCodeMnemonic.DReturn: + case OpCodeMnemonic.AReturn: + case OpCodeMnemonic.Return: + case OpCodeMnemonic.AThrow: + case OpCodeMnemonic.IfNull: + case OpCodeMnemonic.IfNonNull: + case OpCodeMnemonic.TableSwitch: + case OpCodeMnemonic.LookupSwitch: + { + var blockOpCodes = opcodes.AsMemory(start, ++count); + blocks.Add(opcodes[start].Position, new Block { OpCodes = blockOpCodes, CatchType = catchType }); + + if (i != opcodes.Length - 1) + { + var nextPosition = opcodes[i + 1].Position; + catchType = code.ExceptionTable.FirstOrDefault(e => e.Handler == nextPosition).CatchType; + } + + start = i + 1; + count = -1; + break; + } + } + } + + Block ResolveBlock(int addr) + { + if (blocks.TryGetValue(addr, out var block)) + return block; + + // Split block if an instruction jumps in the middle of it + + foreach (var (key, nextBlock) in blocks) + { + if(key <= addr) block = nextBlock; + else break; + } + + var span = block!.OpCodes.Span; + for (var i = 0; i < span.Length; i++) + { + if (span[i].Position != addr) continue; + blocks[span[0].Position] = new Block + { + OpCodes = block.OpCodes[..i], + CatchType = block.CatchType, + }; + return blocks[addr] = new Block + { + OpCodes = block.OpCodes[i..], + CatchType = block.CatchType, + }; + } + + throw new UnreachableException(); + } + + foreach (ref var opcode in opcodes.AsSpan()) + { + switch (opcode.Mnemonic) + { + case OpCodeMnemonic.IfEq: + case OpCodeMnemonic.IfNe: + case OpCodeMnemonic.IfLt: + case OpCodeMnemonic.IfGt: + case OpCodeMnemonic.IfLe: + case OpCodeMnemonic.IfGe: + case OpCodeMnemonic.IfICmpEq: + case OpCodeMnemonic.IfICmpNe: + case OpCodeMnemonic.IfICmpLt: + case OpCodeMnemonic.IfICmpGt: + case OpCodeMnemonic.IfICmpLe: + case OpCodeMnemonic.IfICmpGe: + case OpCodeMnemonic.IfACmpEq: + case OpCodeMnemonic.IfACmpNe: + case OpCodeMnemonic.Goto: + case OpCodeMnemonic.IfNull: + case OpCodeMnemonic.IfNonNull: + { + opcode.P0.Object = ResolveBlock(opcode.Position + (short)opcode.P0.UShort); + break; + } + + case OpCodeMnemonic.TableSwitch: + { + var tableSwitch = (TableSwitch) opcode.P0.Object!; + var targets = new Block[tableSwitch.Offsets.Length + 1]; + opcode.P1.Object = targets; + targets[0] = ResolveBlock(opcode.Position + tableSwitch.Default); + for (var i = 0; i < tableSwitch.Offsets.Length; i++) + targets[i + 1] = ResolveBlock(opcode.Position + tableSwitch.Offsets[i]); + break; + } + + case OpCodeMnemonic.LookupSwitch: + { + var tableSwitch = (LookupSwitch) opcode.P0.Object!; + var targets = new Block[tableSwitch.Entries.Length + 1]; + opcode.P1.Object = targets; + targets[0] = ResolveBlock(opcode.Position + tableSwitch.Default); + for (var i = 0; i < tableSwitch.Entries.Length; i++) + targets[i + 1] = ResolveBlock(opcode.Position + tableSwitch.Entries[i].Value); + break; + } + } + } + + if(code.Attributes.OfType().FirstOrDefault() is not {} stackMap) + return new CodeAnalysis { Blocks = blocks }; + + foreach (var frame in stackMap.Frames) + ResolveBlock(frame.StartOffset); + + var stack = new List(code.MaxStack); + var locals = new List(code.MaxLocals); + if((method.AccessFlags & MethodAccessFlags.Static) == 0) + locals.Add(method.DeclaringClass); + + Class ResolveClass(StackMapTable.VerificationTypeInfo info) + { + switch (info.Tag) + { + case StackMapTable.VerificationTypeInfoTag.Top: return locals[^1]; + case StackMapTable.VerificationTypeInfoTag.Integer: return loader.Int; + case StackMapTable.VerificationTypeInfoTag.Float: return loader.Float; + case StackMapTable.VerificationTypeInfoTag.Double: return loader.Double; + case StackMapTable.VerificationTypeInfoTag.Long: return loader.Long; + case StackMapTable.VerificationTypeInfoTag.Object: return method.DeclaringClass.GetConstant(info.Parameter); + case StackMapTable.VerificationTypeInfoTag.Null: + case StackMapTable.VerificationTypeInfoTag.UninitializedThis: + case StackMapTable.VerificationTypeInfoTag.Uninitialized: + default: throw new NotImplementedException(info.Tag.ToString()); + } + } + + locals.AddRange(method.Signature.ParamTypes); + foreach (var block in blocks.Values) + { + if(block.Start >= stackMap.Frames[0].StartOffset) break; + LocalsRef(block) = locals.ToArray(); + } + + for (var i = 0; i < stackMap.Frames.Count; i++) + { + var frame = stackMap.Frames[i]; + var end = i == stackMap.Frames.Count - 1 ? code.Bytecode.Length : stackMap.Frames[i + 1].StartOffset; + switch (frame) + { + case StackMapTable.SameFrame: + case StackMapTable.SameFrameExtended: + stack.Clear(); + break; + + case StackMapTable.FullFrame f: + stack.Clear(); + locals.Clear(); + locals.AddRange(f.Locals.Select(ResolveClass)); + stack.AddRange(f.StackItems.Select(ResolveClass)); + break; + + case StackMapTable.ChopFrame f: + stack.Clear(); + locals.RemoveRange(locals.Count - f.Count, f.Count); + break; + + case StackMapTable.AppendFrame f: + stack.Clear(); + locals.AddRange(f.Locals.Select(ResolveClass)); + break; + + case StackMapTable.OneTempFrame f: + stack.Clear(); + stack.Add(ResolveClass(f.Entry)); + break; + + case StackMapTable.OneTempFrameExtended f: + stack.Clear(); + stack.Add(ResolveClass(f.Entry)); + break; + + default: throw new NotImplementedException(frame.ToString()); + } + foreach (var block in blocks.Values) + { + if(block.Start < frame.StartOffset) continue; + if(block.Start >= end) break; + LocalsRef(block) = locals.ToArray(); + StackRef(block) = stack.ToArray(); + } + } + + return new CodeAnalysis { Blocks = blocks }; + } + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + private static extern ref IReadOnlyList LocalsRef(Block block); + + [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "k__BackingField")] + private static extern ref IReadOnlyList StackRef(Block block); +} \ No newline at end of file diff --git a/CLI/Program.cs b/CLI/Program.cs index 1cf4702..4ce1cbc 100755 --- a/CLI/Program.cs +++ b/CLI/Program.cs @@ -16,7 +16,7 @@ public class Program var javaClass = loader.LoadClass("Main".AsMemory()); Console.WriteLine($"Load time: {(DateTime.Now - now).TotalMilliseconds}ms"); - var builder = new AssemblyBuilder(new AssemblyName("Test"), false); + var builder = new AssemblyBuilder(loader, new AssemblyName("Test"), false); var dotnetClass = builder.MakeType(javaClass); try { diff --git a/CLI/Types/New.cs b/CLI/Types/New.cs index ee6667d..6563f07 100644 --- a/CLI/Types/New.cs +++ b/CLI/Types/New.cs @@ -77,14 +77,16 @@ public class NewClass : TypeRepresentation if (original is null) return method; - if (!original.TryGetCode(out var code, out var reader)) + if (!original.TryGetCode(out var code)) return method; + + var blocks = CodeAnalysis.Analyze(Builder.TypeLoader, original); if (method is MethodBuilder methodBuilder) - Builder.CompileOpCodes(original.DeclaringClass, method, methodBuilder.GetILGenerator(), code, reader); + Builder.CompileOpCodes(original.DeclaringClass, method, methodBuilder.GetILGenerator(), code); if (method is ConstructorBuilder constructorBuilder) - Builder.CompileOpCodes(original.DeclaringClass, method, constructorBuilder.GetILGenerator(), code, reader); + Builder.CompileOpCodes(original.DeclaringClass, method, constructorBuilder.GetILGenerator(), code); return method; } @@ -184,11 +186,11 @@ public abstract class TypeSurrogate : NewClass if (original is null) return method; - if (!original.TryGetCode(out var code, out var reader)) + if (!original.TryGetCode(out var code)) return method; var methodBuilder = (MethodBuilder) method; - Builder.CompileOpCodes(Original, method, methodBuilder.GetILGenerator(), code, reader); + Builder.CompileOpCodes(Original, method, methodBuilder.GetILGenerator(), code); return method; } diff --git a/CLI/Types/java/lang/Array.cs b/CLI/Types/java/lang/Array.cs index 11ba10c..2aff1bc 100644 --- a/CLI/Types/java/lang/Array.cs +++ b/CLI/Types/java/lang/Array.cs @@ -1,6 +1,6 @@ using JCIL.Java.Class; -namespace JCIL.CLI.Types.java.lang; +namespace JCIL.CLI.Types.Java.Lang; public class Array(AssemblyBuilder builder, Class javaClass) : TypeSurrogate(builder, javaClass) { diff --git a/JavaClass/Attribute.cs b/JavaClass/Attribute.cs index b0c1a9a..d4d1b95 100755 --- a/JavaClass/Attribute.cs +++ b/JavaClass/Attribute.cs @@ -1,3 +1,5 @@ +using System.Runtime.InteropServices; + namespace JCIL.Java.Class; public abstract class Attribute @@ -100,6 +102,12 @@ public sealed class Code : Attribute }; } + public OpCodeReader CreateOpCodeReader() + { + MemoryMarshal.TryGetArray(Bytecode, out var array); + return new OpCodeReader(new MemoryStream(array.Array!, array.Offset, array.Count)); + } + public record struct ExceptionTableEntry(ushort Start, ushort End, ushort Handler, Class CatchType); } @@ -162,13 +170,13 @@ public sealed class StackMapTable : Attribute case < 128: delta = frameType - 64 + one; offset += delta; - frame = new OneLocalFrame(offset, VerificationTypeInfo.Read(reader)); + frame = new OneTempFrame(offset, VerificationTypeInfo.Read(reader)); break; case 247: delta = reader.ReadUInt16() + one; offset += delta; - frame = new OneLocalFrameExtended(offset, VerificationTypeInfo.Read(reader)); + frame = new OneTempFrameExtended(offset, VerificationTypeInfo.Read(reader)); break; case 248 or 249 or 250: @@ -236,9 +244,9 @@ public sealed class StackMapTable : Attribute public sealed record SameFrameExtended(int StartOffset) : Frame(StartOffset); - public sealed record OneLocalFrame(int StartOffset, VerificationTypeInfo Local) : Frame(StartOffset); + public sealed record OneTempFrame(int StartOffset, VerificationTypeInfo Entry) : Frame(StartOffset); - public sealed record OneLocalFrameExtended(int StartOffset, VerificationTypeInfo Local) : Frame(StartOffset); + public sealed record OneTempFrameExtended(int StartOffset, VerificationTypeInfo Entry) : Frame(StartOffset); public sealed record AppendFrame(int StartOffset, VerificationTypeInfo[] Locals) : Frame(StartOffset); diff --git a/JavaClass/Method.cs b/JavaClass/Method.cs index 9ecd6e4..f3f027d 100755 --- a/JavaClass/Method.cs +++ b/JavaClass/Method.cs @@ -1,5 +1,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace JCIL.Java.Class; @@ -53,6 +54,7 @@ public sealed class Method public readonly string Name; public readonly Class Type; public readonly IReadOnlyList Attributes; + public MethodSignature Signature => Unsafe.As(Type.SpecialClassMetadata)!; private Method(Class declaringClass, MethodAccessFlags flags, string name, Class type, Attribute[] attributes) { @@ -80,18 +82,15 @@ public sealed class Method return new Method(parent, flags, name, descriptorClass, attributes); } - public bool TryGetCode([MaybeNullWhen(false)] out Code code, out OpCodeReader opcodes) + public bool TryGetCode([MaybeNullWhen(false)] out Code code) { - if (Attributes.OfType().FirstOrDefault() is { } attr) + if (Attributes.OfType().FirstOrDefault() is {} attr) { - MemoryMarshal.TryGetArray(attr.Bytecode, out var array); code = attr; - opcodes = new OpCodeReader(new MemoryStream(array.Array!)); return true; } - + code = null!; - opcodes = default; return false; } @@ -308,6 +307,7 @@ public enum OpCodeMnemonic : byte public struct OpCode { public OpCodeMnemonic Mnemonic; + public int Position; public OpCodeParameter P0; public OpCodeParameter P1; @@ -353,20 +353,21 @@ public struct OpCodeParameter [FieldOffset(8)] public object? Object; } -public struct OpCodeReader(Stream bytecode) : IEnumerator +public sealed class OpCodeReader(Stream bytecode) : IEnumerator, IEnumerable { private OpCode _current; public Reader Reader = new(bytecode); public bool MoveNext() { + var position = Reader.Stream.Position; var b = Reader.Stream.ReadByte(); if (b == -1) { _current = default; return false; } - _current = new OpCode { Mnemonic = (OpCodeMnemonic)(byte)b }; + _current = new OpCode { Mnemonic = (OpCodeMnemonic)(byte)b, Position = (int) position }; switch (_current.Mnemonic) { case OpCodeMnemonic.PushByte: @@ -620,6 +621,15 @@ public struct OpCodeReader(Stream bytecode) : IEnumerator { Reader.Stream.Dispose(); } + + public OpCodeReader GetEnumerator() + => this; + + IEnumerator IEnumerable.GetEnumerator() + => this; + + IEnumerator IEnumerable.GetEnumerator() + => this; } public sealed class TableSwitch diff --git a/JavaClass/TypeLoader.cs b/JavaClass/TypeLoader.cs index b1ab2a8..833da28 100755 --- a/JavaClass/TypeLoader.cs +++ b/JavaClass/TypeLoader.cs @@ -11,14 +11,16 @@ public sealed class TypeLoader private readonly ConcurrentDictionary, Class> _classes = new(ReadOnlyMemoryCharComparer.Instance); - public Class Object - { - get - { - field ??= LoadClass("java/lang/Object".AsMemory()); - return field; - } - } + public Class Void => field ??= GetClass("V".AsMemory()); + public Class Char => field ??= GetClass("C".AsMemory()); + public Class Byte => field ??= GetClass("B".AsMemory()); + public Class Short => field ??= GetClass("S".AsMemory()); + public Class Int => field ??= GetClass("I".AsMemory()); + public Class Long => field ??= GetClass("J".AsMemory()); + public Class Float => field ??= GetClass("F".AsMemory()); + public Class Double => field ??= GetClass("D".AsMemory()); + public Class Boolean => field ??= GetClass("Z".AsMemory()); + public Class Object => field ??= LoadClass("java/lang/Object".AsMemory()); public void AddPath(string path) {