Manipulacja kodem bajtowym z JiteScript

W moim przypadku praca z kodem bajtowym zawsze uczy mnie czegoś nowego. Postanowiłem dodać ten wpis, aby zachęcić was do eksperymentów. Aktualnie kompilator oraz JIT wykonuje za nas całą robotę. Prowadzi to do sytuacji, w których bardzo wiele aspektów języka przyjmujemy na zasadzie “no bo tak jest”. Odczarujmy trochę ten kod bajtowy!

JiteScript

Na rynku istnieje kilka popularnych bibliotek do manipulacji kodem bajtowym. Jedną z nich jest biblioteka JiteScript. Patrząc na statystyki Maven’a projekt przestał być rozwijany koło 2016 roku. Jednakże moim zdaniem nadal jest to narzędzie godne uwagi. JiteScript pod spodem wykorzystuje ASM, o którym wpis na pewno ukaże się w przyszłości.

Maven

Dodajemy zależności do bibloteki JiteScript:

<dependency>
    <groupId>me.qmx.jitescript</groupId>
    <artifactId>jitescript</artifactId>
    <version>0.4.1</version>
</dependency>

Zaczynamy

Zaczniemy od stworzenia klasy. Wykorzystamy do tego klasę JiteClass, która w konstruktorze może przyjąć informację o nazwie klasy, nazwie klasy po której ma dziedziczyć oraz nazwach interfejsów jeśli takowe implementuje:

JiteClass someClass = new JiteClass("SomeClass");

W naszym przypadku tworzymy klasę SomeClass, która dziedziczy po klasie Object. Następnie musimy dodać domyślny konstruktor. Normalnie robi to za nas kompilator, ale w tym przypadku to my musimy stworzyć go wprost:

someClass.defineDefaultConstructor();

Po dodaniu konstruktora pora stworzyć nową instancję tej klasy. Aby to zrobić musimy najpierw stworzyć własny ClassLoader:

class CustomClassLoader extends ClassLoader {
    
    public Class<?> defineClass(String name, byte[] data) {
        return super.defineClass(name, data, 0, data.length);
    }

}

Po utworzeniu ClassLoader’a, możemy stworzyć naszą instancję na podstawie kodu bajtowego:

byte[] bytes = someClass.toBytes(JDKVersion.V1_8);
CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.defineClass("SomeClass", bytes);
clazz.newInstance();

HelloWorld

Poprzedni przykład był mało efektowny. Stworzyliśmy tam prostą klasę z domyślnym konstruktorem. Tym razem w konstruktorze chciałbym wypisać słynne HelloWorld. Ponownie wykorzystujemy klasę JiteClass:

JiteClass helloWorld = new JiteClass("HelloWorld");

Tym razem nie dodajemy domyślnego konstruktora tylko sami go tworzymy. Możemy wykorzystać do tego zwykłe API i tak zwane fluent API. W tym przykładzie przedstawię przykład “zwykłego” API:

CodeBlock constructor = new CodeBlock();
constructor.aload(0);
constructor.invokespecial(p(Object.class), "<init>", sig(void.class));
constructor.getstatic(p(System.class), "out", ci(PrintStream.class));
constructor.ldc("HelloWorld");
constructor.invokevirtual(p(PrintStream.class), "println", sig(void.class, Object.class));
constructor.voidreturn();

Zaczynamy od metody aload(0). Ładuje ona na stos referencje do obiektu this. Następnie na referencji obiektu wywołujemy metodę <init>. Jest to nazwa konstruktora, ponieważ konstruktor sam w sobie nie ma nazwy. Bardzo często zapominamy o tym, iż w naszym konstruktorze pod spodem dodawane jest super(). Tym razem nie mamy kompilatora, więc musimy zrobić to sami. Następnie na stos ładujemy statyczne pole System.out oraz stałą "HelloWorld". Po załadowaniu danych na stosie uruchamiamy metodę println, która pobiera wartości ze stosu. Na koniec musimy wywołać metodę return. Po stworzeniu konstruktora musimy dodać go do klasy HelloWorld:

helloWorld.defineMethod("<init>", JiteClass.ACC_PUBLIC, sig(Void.TYPE), constructor);

Ponownie tworzymy nową instancję, tym razem powinniśmy ujrzeć na konsoli napis HelloWorld:

byte[] bytes = helloWorld.toBytes(JDKVersion.V1_8);

CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.defineClass("HelloWorld", bytes);
clazz.newInstance();

Konstruktor z parametrem

W tym przykładzie ponownie stworzymy konstruktor. Tym razem jednak będzie on przyjmował parametr typu String, który następnie zostanie wypisany. Zaczynamy od dodania klasy:

JiteClass helloText = new JiteClass("HelloText");

Po dodaniu klasy dodajemy konstruktor (tym razem korzystając z fluent API):

helloText.defineMethod("<init>", JiteClass.ACC_PUBLIC, sig(Void.TYPE, String.class),
        newCodeBlock()
                .aload(0)
                .invokespecial(p(Object.class), "<init>", sig(void.class))
                .getstatic(p(System.class), "out", ci(PrintStream.class))
                .aload(1)
                .invokevirtual(p(PrintStream.class), "println", sig(void.class, Object.class))
                .voidreturn());

W metodzie sig() dodaliśmy parametr typu String.class. Tym razem nie ładowaliśmy stałej korzystając z ldc(), tylko załadowaliśmy zmienną z tak zwanej tablicy zmiennych lokalnych. Teraz możemy wywołać nasz nowy konstruktor:

byte[] bytes = helloText.toBytes(JDKVersion.V1_8);

CustomClassLoader customClassLoader = new CustomClassLoader();
Class<?> clazz = customClassLoader.defineClass("HelloText", bytes);
clazz.newInstance();
clazz.getConstructors()[1].newInstance("CodeCouple");

A na konsoli powinien pokazać się napis CodeCouple.

Github

Całość jak zawsze na Github’ie.