Menciptakan Bahasa Dinamis Di Java: Part 4 – Memanggil Java Libraries

Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Pada artikel sebelumnya, saya memisahkan antara proses kompilasi (untuk menghasilkan file class) dan proses menjalankan output (hasil kompilasi). Tujuannya adalah agar perbedaan ini terlihat jelas: kode program perlu di-compile terlebih dahulu sebelum dijalankan. Akan tetapi dari sisi kepraktisan, menjalankan dua perintah yang berbeda adalah sesuatu yang merepotkan. Oleh sebab itu, kali ini saya akan mencoba menggabungkan kedua proses tersebut menjadi satu. Saya akan mengubah class com.wordpress.thesolidsnake.bahasaku.Main menjadi seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuLexer;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        if (args.length==0) {
            System.out.println("Tool ini dipakai untuk menjalankan program BahasaKu\n\n" +
                "Contoh penggunaan:\n" +
                "bahasaku namafilesource.baku\n" +
                "bahasaku source1.baku source2.baku\n" +
                "bahasaku -trace namafilesource.baku\n");
            return;
        }

        boolean enableTrace = false;
        List sourceFiles = new ArrayList();

        // Melakukan parsing argumen
        // Dapat melakukan kompilasi lebih dari satu file
        for (String arg: args) {
            if ("-trace".equals(arg)) {
                enableTrace = true;
            } else {
                sourceFiles.add(arg);
            }
        }

        // Memulai proses kompilasi
        for (String sourceFile: sourceFiles) {
            Path file = Paths.get(sourceFile);
            if (!file.toFile().exists()) {
                System.out.println("Tidak dapat menemukan file: [" + file + "]");
                continue;
            }

            try {

                // Front-end
                ANTLRInputStream antlrInputStream = new ANTLRInputStream(Files.newInputStream(file));
                BahasaKuLexer lexer = new BahasaKuLexer(antlrInputStream);
                CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
                BahasaKuParser parser = new BahasaKuParser(commonTokenStream);
                ParseTree parseTree = parser.file();

                // Back-end
                BahasaKuVisitor jvmVisitor = new BahasaKuVisitor(file.toFile(), enableTrace);
                jvmVisitor.visit(parseTree);
                jvmVisitor.generateOutput();

                // Menjalankan Program
                String namaFileLengkap = file.toFile().getName();
                String namaClass = namaFileLengkap.substring(0, namaFileLengkap.lastIndexOf('.'));
                ProcessBuilder pb = new ProcessBuilder(System.getProperty("java.home") + "/bin/java",
                    "-classpath",System.getProperty("java.class.path") + File.pathSeparatorChar + file.getParent().toAbsolutePath().toFile().toString(), namaClass);
                if (enableTrace) {
                    System.out.println("Mengerjakan " + pb.command());
                }
                pb.inheritIO();
                pb.start().waitFor();


            } catch (Exception ex) {
                System.out.println("Terjadi kesalahan [" + ex.getMessage() + "]");
            }
        }

    }

}

Agar lebih nyaman lagi, saya akan membuat sebuah kode program BahasaKu di lokasi proyek, misalnya di src\test\resources\Latihan.baku. Kemudian saya memodifikasi build.grade menjadi seperti berikut ini:

...
ext {
   ...
   sourceForRun = 'src/test/resources/Latihan.baku'
}
...
run {
    args sourceForRun
}

Hal ini akan menyebabkan setiap kali pemanggilan task run, Gradle akan menjalankan compiler BahasaKu dengan argumen berupa isi dari variabel sourceForRun, yaitu lokasi source code BahasaKu. Dengan demikian, mulai sekarang, saya dapat menguji compiler tanpa harus meninggalkan IntelliJ IDEA dan membuka command prompt lagi, seperti yang terlihat pada gambar berikut ini:

Menjalankan Program BahasaKu Langsung Dari IDEA

Menjalankan Program BahasaKu Langsung Dari IDEA

Selanjutnya, saya akan mengimplementasikan bagian yang membuat instance object baru. Pada BahasaKu, saya memakai keyword buat untuk membuat instance dari sebuah class (sama seperti new di bahasa Java/C). BahasaKu bukanlah bahasa yang OOP. Saya tidak dapat mendefinisikan class baru melalui BahasaKu tapi ia dapat mengakses dan memanggil method untuk class yang sudah ada. Java memiliki banyak kumpulan class siap pakai yang disebutnya sebagai Java Class Library (JCL) yang meliputi Swing (GUI), JDBC (database), collection, dan masih banyak lagi. Informasi mengenai class library yang disediakan oleh Java dapat dilihat di http://docs.oracle.com/javase/7/docs/technotes/guides/index.html#otherbase. Sebuah bahasa pemograman, walaupun dirancang dengan baik, tidak akan begitu berguna bila tidak memiliki dukungan library yang matang dan stabil. Programmer akan menghabiskan waktunya mengimplementasikan segala sesuatunya sendiri ketimbang berkonsentrasi pada permasalahan yang hendak diungkapkannya melalui kode! Menariknya, karena Java sudah berusia tua dan memiliki komunitas besar, ada banyak library lain yang dibuat oleh pihak ketiga (biasanya open-source) untuk menutupi kekurangan fitur yang tidak disediakan oleh Java. Sebagai contoh, library pihak ketiga yang umum dipakai untuk keperluan aplikasi bisnis adalah library untuk membuat, menampilkan dan mencetak laporan. Java secara bawaan tidak memiliki fasilitas yang berkaitan dengan laporan, tapi JasperReports menyediakan library untuk keperluan tersebut.

Untuk memungkinkan BahasaKu membuat instance baru dari sebuah class, saya akan men-override method visitBuatObject() sehingga terlihat seperti pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle CONSTRUCTOR_HANDLE;

    static {
        ...
        CONSTRUCTOR_HANDLE = new Handle(H_INVOKESTATIC,
            "com/wordpress/thesolidsnake/bahasaku/target/jvm/MethodCallBootstrap",
            "constructorBootstrap",
            "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;");
    }

    ...

    @Override
    public Object visitBuatObject(@NotNull BahasaKuParser.BuatObjectContext ctx) {
        String namaClass = ctx.qualifiedName().getText();
        int jumlahArgumen = 0;
        if (ctx.arguments().exprList()!=null) {
            jumlahArgumen = ctx.arguments().exprList().expr().size();
        }
        mv.visitTypeInsn(NEW, namaClass.replace('.', '/'));
        mv.visitInsn(DUP);
        visit(ctx.arguments());
        mv.visitInvokeDynamicInsn("buat", MethodType.genericMethodType(jumlahArgumen).toMethodDescriptorString(),
            CONSTRUCTOR_HANDLE, namaClass);
        return null;
    }

    ...

}

Saya kembali memakai instruksi dynamicinvoke pada kode program di atas. Oleh sebab itu, saya perlu membuat kode program bootstrap yang akan menghubungkannya dengan sebuah call site. Saya segera membuat class com.wordpress.thesolidsnake.bahasaku.target.jvm.MethodCallBootstrap yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku.target.jvm;

import java.lang.invoke.*;
import java.lang.reflect.Constructor;
import java.util.Arrays;

public class MethodCallBootstrap {

    private static final MethodHandle CONSTRUCTOR;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            CONSTRUCTOR = lookup.findStatic(MethodCallBootstrap.class, "constructor",
                    MethodType.methodType(Object.class, String.class, Object[].class));
        } catch (Exception ex) {
            throw new Error("Terjadi kesalahan tak terduga [" + ex.getMessage() + "]");
        }
    }

    public static Class getClassForName(String namaClass) throws Error {
        try {
            return Class.forName(namaClass);
        } catch (ClassNotFoundException ex) {
            throw new Error("Class Java tidak ditemukan: [" + namaClass + "]");
        }
    }

    public static boolean periksaPrimitif(Class primitiveClass, Object arg) {
        if (primitiveClass==long.class) return Long.class.isInstance(arg);
        if (primitiveClass==int.class) return Integer.class.isInstance(arg);
        if (primitiveClass==boolean.class) return Boolean.class.isInstance(arg);
        return false;
    }

    public static boolean isParameterSesuai(Class[] parameterTypes, Object[] args) {
        if (parameterTypes.length != args.length) return false;
        for (int i=0; i<parameterTypes.length; i++) {
            if (parameterTypes[i].isPrimitive()) {
                if (!periksaPrimitif(parameterTypes[i], args[i])) return false;
            } else if (!parameterTypes[i].isInstance(args[i])) {
                return false;
            }
        }
        return true;
    }

    public static Object constructor(String namaClass, Object[] args) {
        Class cls = getClassForName(namaClass);
        for (Constructor c: cls.getConstructors()) {
            if (isParameterSesuai(c.getParameterTypes(), args)) {
                try {
                    return MethodHandles.lookup().unreflectConstructor(c).invokeWithArguments(args);
                } catch (Throwable ex) {
                    throw new RuntimeException("Gagal membuat instance baru: " + ex.getMessage());
                }
            }
        }
        throw new Error("Tidak menemukan constructor yang tepat untuk " +
            namaClass+ " dengan argumen " + Arrays.deepToString(args));
    }

    public static CallSite constructorBootstrap(MethodHandles.Lookup lookup, String name, MethodType type, String namaClass) throws Exception {
        return new ConstantCallSite(CONSTRUCTOR.bindTo(namaClass).asCollector(Object[].class, type.parameterCount()).asType(type));
    }
}

Sekarang, saya bisa mencoba membuat instance dari sebuah class Java dan menampilkan object tersebut di BahasaKu, seperti yang terlihat pada gambar berikut ini:

Membuat Instance Dari Class

Tentu saja bila hanya membuat instance dari class akan terasa tidak lengkap bila tidak diikuti dengan kemampuan untuk memanggil method pada instance tersebut. Oleh sebab itu, saya men-overide method visitPanggilMethod() dengan kode program seperti yang terlihat berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle METHODCALL_HANDLE;

    static {
        ...
        METHODCALL_HANDLE = new Handle(H_INVOKESTATIC,
            "com/wordpress/thesolidsnake/bahasaku/target/jvm/MethodCallBootstrap",
            "methodCallBootstrap",
            "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;)Ljava/lang/invoke/CallSite;I");
    }

    ...

    @Override
    public Object visitPanggilMethod(@NotNull BahasaKuParser.PanggilMethodContext ctx) {
        String namaMethod = ctx.IDENTIFIER().getText();
        int jumlahArgumen = 0;
        if (ctx.arguments().exprList()!=null) {
            jumlahArgumen = ctx.arguments().exprList().expr().size();
        }
        visit(ctx.expr());
        visit(ctx.arguments());
        int type = (ctx.op.getType() == BahasaKuParser.SELF)? 1: 0;
        mv.visitInvokeDynamicInsn("panggil", MethodType.genericMethodType(jumlahArgumen+1).toMethodDescriptorString(),
            METHODCALL_HANDLE, namaMethod, type);
        return null;
    }   

    ...    

}

Pada kode program di atas, karena saya menggunakan instruksi invokedynamic, maka saya perlu membuat kode program bootstrapnya. Method di atas akan menangani dua jenis pemanggilan method, yaitu yang melalui operator titik (.) dan operator sharp (#). Saya akan memakai class bootstrap yang sudah saya buat sebelumnya, yaitu com.wordpress.thesolidsnake.bahasaku.target.jvm.MethodCallBootstrap. Yang perlu saya lakukan adalah menambahkan method bootstrap methodCallBootstrap() seperti yang terlihat pada kode program berikut ini:

public class MethodCallBootstrap {

    ...

    private static final MethodHandle METHODCALL;
    private static final MethodHandle METHODCALL_RETURNSELF;

    static {
        try {
            ...
            METHODCALL = lookup.findStatic(MethodCallBootstrap.class, "methodCall",
                MethodType.methodType(Object.class, String.class, Object[].class));
            METHODCALL_RETURNSELF= lookup.findStatic(MethodCallBootstrap.class, "methodCallReturnSelf",
                MethodType.methodType(Object.class, String.class, Object[].class));
        } catch (Exception ex) {
            throw new Error("Terjadi kesalahan tak terduga [" + ex.getMessage() + "]");
        }
    }

    ...

    public static Object methodCall(String namaMethod, Object[] args) {
        Class cls = args[0].getClass();
        Object[] realArgs = Arrays.copyOfRange(args, 1, args.length);
        for (Method m: cls.getMethods()) {
            if (m.getName().equals(namaMethod) && isParameterSesuai(m.getParameterTypes(), realArgs)) {
                try {
                    MethodHandle currentMethod = MethodHandles.lookup().unreflect(m);
                    return currentMethod.bindTo(args[0]).invokeWithArguments(realArgs);
                } catch (Throwable ex) {
                    throw new RuntimeException("Gagal mengerjakan method: " + ex);
                }
            }
        }
        throw new RuntimeException("Tidak menemukan method " + namaMethod + " untuk object " +
            args[0]+ " (" + args[0].getClass() + ") dengan argumen " + Arrays.deepToString(args));
    }

    public static Object methodCallReturnSelf(String namaMethod, Object[] args) {
        methodCall(namaMethod, args);
        return args[0];
    }

    public static CallSite methodCallBootstrap(MethodHandles.Lookup lookup, String name, MethodType type,
           String namaMethod, int returnSelf) throws Exception {
        if (returnSelf==1) {
            return new ConstantCallSite(METHODCALL_RETURNSELF.bindTo(namaMethod).
                asCollector(Object[].class, type.parameterCount()).asType(type));
        } else {
            return new ConstantCallSite(METHODCALL.bindTo(namaMethod).
                asCollector(Object[].class, type.parameterCount()).asType(type));
        }
    }

}

Untuk menguji fungsi pemanggilan method, saya membuat kode program BahasaKu yang terlihat seperti pada gambar berikut ini:

Memanggil Method

Memanggil Method

Pada kode program di atas, saya menggunakan class java.util.ArrayList untuk menampung data dan class java.util.Random untuk menghasilkan bilangan acak. Ini adalah salah satu keuntungan membuat bahasa pemograman di platform Java, yaitu dapat memanfaatkan class library yang sudah ada.

Walaupun memakai class library yang sama seperti pada bahasa pemograman Java, sebuah bahasa baru di JVM bisa saja memberikan ‘sensasi’ yang berbeda. Sebagai contoh, BahasaKu memiliki operator # untuk pemanggilan method yang akan selalu mengembalikan object saat ini. Tujuannya adalah melakukan chaining (pemanggilan berantai). Ini adalah sesuatu yang tidak mungkin terjadi pada bahasa pemograman Java bila method tidak mengembalikan object yang dimaksud. Tapi, BahasaKu memungkinkan chaining terjadi tak peduli apapun hasil kembalian dari method, seperti yang terlihat pada kode program berikut ini:

list <- buat java.util.ArrayList()
random <- buat java.util.Random()
list#add(random.nextInt(10))#add(random.nextInt(10))#add(random.nextInt(10))
tampilkan 'Angka acak #1: ' + list.get(0)
tampilkan 'Angka acak #2: ' + list.get(1)
tampilkan 'Angka acak #3: ' + list.get(2)

Contoh lainnya, berikut ini adalah kode program BahasaKu yang menampilkan frame GUI:

Membuat GUI Dengan BahasaKu

Pada seri artikel ini, saya telah menunjukkan bahwa bahasa pemograman untuk platform Java (atau Java Virtual Machine) tidak harus selalu berupa bahasa pemograman Java (atau Java Programming Language). Seseorang bisa saja membuat bahasa pemograman baru yang berjalan di JVM. Hal ini semakin mudah dilakukan terutama bila dibantu oleh tools seperti ANTLR sebagai parser/lexer generator dan ASM sebagai bytecode generator. Pada sebuah situs, http://www.is-research.de/info/vmlanguages, terdaftar sekitar 300 bahasa untuk JVM (tentunya ini tidak semua bahasa tersebut akan menjadi mainstream).

Iklan

Menciptakan Bahasa Dinamis Di Java: Part 3 – Membuat Operasi Aritmatika

Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Kali ini saya akan membuat implementasi untuk rule assignment. Seperti biasa, saya akan men-override salah satu method visitor yaitu visitAssignment() seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private List namaVariabel;

    ...

    @Override
    public Object visitAssignment(@NotNull BahasaKuParser.AssignmentContext ctx) {
        String nama= ctx.IDENTIFIER().getText();
        int index = namaVariabel.indexOf(nama) + 1;
        if (index <= 0) {
            namaVariabel.add(nama);
            index = namaVariabel.size();
        }
        visit(ctx.expr());
        mv.visitVarInsn(ASTORE, index);
        return null;
    }   

    ...

    public void generateOutput() {
        mv.visitInsn(RETURN);

    // PENAMBAHAN MULAI DISINI

        Label label = new Label();
        mv.visitLabel(label);
        for (int i=0; i<namaVariabel.size(); i++) {
            mv.visitLocalVariable(namaVariabel.get(i), "Ljava/lang/Object;",
                null, label0, label, i);
        }

    // PENAMBAHAN BERAKHIR DISINI

        mv.visitMaxs(0, 0);
        mv.visitEnd();
        cv.visitEnd();

        Path targetFile = Paths.get(sourceFile.getAbsoluteFile().getParent().toString(), namaSourceFile + ".class");
        System.out.println("Menghasilkan target file di lokasi: " + targetFile.toString() + "n");
        try {
            Files.write(targetFile, cw.toByteArray(),
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (Exception ex) {
            System.out.println("Terjadi kesalahan saat membuat file target!");
            ex.printStackTrace();
        }
    }


}

Pada bahasa pemograman Java, seluruh variabel harus dideklarasikan sebelum dipakai. BahasaKu mengambil pendekatan yang berbeda dimana ia tidak mensyaratkan deklarasi variabel. Setiap variabel di BahasaKu dapat langsung dipakai. Tentu saja ini membuat compiler harus bekerja lebih keras. Pada kode program di atas, saya melakukan pemeriksaan pada setiap assignment. Bila terdapat assignment dengan nama variabel yang belum terdaftar sebelumnya, maka saya akan menyimpan nama variabel baru tersebut. Pada akhirnya, saya tetap memiliki daftar nama seluruh variabel di kode program BahasaKu.

Walaupun kode program terasa lebih sederhana bila seluruh variabel tidak perlu dideklarasikan, ini menimbulkan kekurangan tersendiri. Sebagai contoh, pada kode program BahasaKu berikut ini, programmer melakukan salah ketik nama varibel:

nilai1 <- 10
total <- total + nilai1
nilai2 <- 20
totel <- total + nilai2
tampilkan total

Hasil dari kode program di atas tidak akan sesuai harapan karena compiler BahasaKu akan mendeklarasikan 2 variabel berbeda, yaitu total dan totel. Padahal, variabel totel adalah salah ketik dan yang dimaksud adalah total.

Lalu bagaimana dengan tipe data? Bahasa pemograman Java adalah bahasa pemograman yang statically typed dimana setiap variabel wajib dideklarasikan dengan tipe data tertentu. Sebaliknya, BahasaKu mengikuti perilaku pada bahasa pemograman yang dynamically typed dimana sebuah variabel tidak dibatasi pada tipe data yang pasti. Sebagai contoh, pada kode program berikut ini, variabel a boleh di-isi berulang kali dengan tipe data yang berbeda:

a <- 10
tampilkan a
a <- 'Solid Snake'
tampilkan a
a <- iya
tampilkan a

Untuk mendukung penggunaan tipe data berbeda pada sebuah variabel yang sama, maka compiler BahasaKu akan mendeklarasikan seluruh variabel dengan tipe java.lang.Object. Karena seluruh object (termasuk angka, String, dan boolean) diturunkan dari java.lang.Object, maka variabel dengan tipe java.lang.Object dapat menampung seluruh tipe data yang ada. Btw, mengapa di bahasa pemograman Java tidak mendeklarasikan seluruh variabel sebagai java.lang.Object untuk mencapai efek dinamis? Hal tersebut tidak akan begitu berguna karena bahasa pemograman Java bersifat statically typed sehingga untuk memakai variabel tersebut harus dilakukan casting dimana-mana.

Berikutnya, saya akan lanjut membuat kode program untuk menerjemahkan ekspresi yang terdiri atas nama variabel, seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitIdentifier(@NotNull BahasaKuParser.IdentifierContext ctx) {
        String nama = ctx.IDENTIFIER().getText();
        int index = namaVariabel.indexOf(nama) + 1;
        if (index <= 0) {
            throw new RuntimeException("Ada variabel yang tak dikenal, yaitu: [" + nama + "]");
        }
        mv.visitVarInsn(ALOAD, index);
        return nama;
    }

    ...

}

Pada kode program di atas, bila nama identifier tidak ditemukan pada daftar variabel yang sudah diidentifikasi, maka compiler akan memunculkan pesan kesalahan. Sebagai contoh, berikut ini adalah kode program yang salah (karena variabel nama belum di-isi saat akan di-cetak):

tampilkan nama
nama <- 'Solid Snake

Berikut ini adalah contoh pesan kesalahan yang dijumpai:

C:BahasakubuildinstallBahasakubin>bahasaku C:contohLatihan.baku
Terjadi kesalahan [Coba cek program-mu! Ada variabel yang tak dikenal, yaitu: [n
ama]]
java.lang.RuntimeException: Coba cek program-mu! Ada variabel yang tak dikenal,
yaitu: [nama]
        at com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor.visit
Identifier(BahasaKuVisitor.java:115)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Identifie
rContext.accept(BahasaKuParser.java:474)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractPars
eTreeVisitor.java:44)
        at com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor.visit
Tampilkan(BahasaKuVisitor.java:92)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Tampilkan
Context.accept(BahasaKuParser.java:189)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visitChildren(Abst
ractParseTreeVisitor.java:68)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor.visi
tStatement(BahasaKuBaseVisitor.java:85)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$Statement
Context.accept(BahasaKuParser.java:134)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visitChildren(Abst
ractParseTreeVisitor.java:68)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor.visi
tFile(BahasaKuBaseVisitor.java:117)
        at com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser$FileConte
xt.accept(BahasaKuParser.java:67)
        at org.antlr.v4.runtime.tree.AbstractParseTreeVisitor.visit(AbstractPars
eTreeVisitor.java:44)
        at com.wordpress.thesolidsnake.bahasaku.Main.main(Main.java:60)

Langkah berikutnya adalah melakukan implementasi untuk visitor yang menghasilkan operasi aritmatika. Oleh sebab itu, saya membuat kode program seperti berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitPembagian(@NotNull BahasaKuParser.PembagianContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IDIV);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPerkalian(@NotNull BahasaKuParser.PerkalianContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IMUL);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPenjumlahan(@NotNull BahasaKuParser.PenjumlahanContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(IADD);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    @Override
    public Object visitPengurangan(@NotNull BahasaKuParser.PenguranganContext ctx) {
        visit(ctx.expr(0));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        visit(ctx.expr(1));
        mv.visitTypeInsn(CHECKCAST, "java/lang/Integer");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I");
        mv.visitInsn(ISUB);
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;");
        return null;
    }

    ...

}

Pada kode program di atas, saya tidak melakukan optimalisasi. Implementasi yang lebih optimal adalah memperhitungkan konstanta sehingga tidak harus selalu memanggil instruksi IDIV, IMUL, IADD, dan ISUB.

Saya akan mencoba operasi aritmatika dengan kode program berikut ini:

a <- 10
b <- 20
tampilkan a + b
c <- a * a - b * b
tampilkan c
c <- c / 100
tampilkan c
c <- c + 100
tampilkan c

Hasilnya akan terlihat seperti berikut ini:

C:contoh> java Latihan
30
-300
-3
97

Operasi aritmatika pada BahasaKu memprioritaskan perkalian dan pembagian (sama seperti pada matematika) berkat urutan yang didefinisikan di grammar ANTLR.

Walaupun bahasa yang dynamically typed seperti BahasaKu lebih mudah dan lebih nyaman dipakai, compiler akan sulit mencegah terjadinya kesalahan. Sebagai contoh, ekspresi a + b saat ini hanya valid jika baik nilai a dan nilai b adalah angka. Tapi karena tipe variabel pada BahasaKu bersifat dinamis, maka compiler tidak dapat menebak apakah a dan b adalah angka atau bukan bila program belum dijalankan. Dengan demikian, saya tidak dapat mencegah terjadinya kesalahan pada saat melakukan kompilasi program. Sebagai contoh, kode program berikut ini dapat di-compile dengan sukses:

a <- 10
b <- 20
tampilkan a + b
a <- 'Solid'
b <- 'Snake'
tampilkan a + b

Akan tetapi kesalahan akan terjadi pada saat program dijalankan:

C:contoh> java Latihan
30
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot
 be cast to java.lang.Integer
        at Latihan.main(Unknown Source)

Bahasa pemograman statically typed seperti C/C++ dan Java dapat mendeteksi kesalahan di atas pada saat kompilasi karena tipe sebuah variabel dapat ditentukan dengan melihat kode program. Anehnya, saya melihat kecenderungan pada mahasiswa pemula dimana mereka lebih tertarik pada bahasa dinamis dengan alasan lebih sedikit pesan kesalahan yang muncul. Bahkan saya pernah menemukan mahasiswa yang menyembunyikan (atau mengabaikan) puluhan pesan warning & error di PHP, berpura-pura bahwa semuanya baik-baik saja. Padahal, compiler selalu berusaha sebisa mungkin untuk mendeteksi kesalahan secara dini dan menginformasikannya pada programmer sebelum terjadi ‘kasus’.

Pada contoh di atas, terlihat bahwa saya tidak bisa menggabungkan String dengan menggunakan operator +. Tapi bukankah sebaiknya fitur ini ada? Oleh sebab itu, saya perlu mengimplementasikannya. Saya akan menggunakan fasilitas invokedynamic yang diperkenalkan sejak Java 7. Instruksi invokedynamic cukup membuat heboh karena instruksi ini sama sekali tidak berguna untuk bahasa pemograman Java (compiler javac tidak akan pernah menghasilkan instruksi invokedynamic!). Hanya pembuat bahasa untuk JVM yang memanfaatkan fitur ini. Penjelasan mengenai invokedynamic dapat dijumpai di http://docs.oracle.com/javase/7/docs/technotes/guides/vm/multiple-language-support.html#invokedynamic.

Untuk memakai invokedynamic, saya perlu membuat sebuah bootstrap method yang akan dipanggil saat JVM menjumpai instruksi invokedynamic tersebut. Sebagai contoh, saya membuat file OperatorBootstrap.java di package com.wordpress.thesolidsnake.bahasaku.target.jvm yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku.target.jvm;

import java.lang.invoke.*;

public class OperatorBootstrap {

    private static final MethodHandle ARITMATIKA;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            ARITMATIKA = lookup.findStatic(OperatorBootstrap.class, "aritmatika",
                MethodType.methodType(Object.class, String.class, Object.class, Object.class));
        } catch (Exception ex) {
            throw new Error("Terjadi kesalahan tak terduga [" + ex.getMessage() + "]");
        }
    }

    public static Object aritmatika(String jenisOperator, Object arg1, Object arg2) throws Throwable {
        MethodHandle target;
        try {
            target = MethodHandles.lookup().findStatic(OperatorBootstrap.class, jenisOperator,
                MethodType.methodType(Object.class, arg1.getClass(), arg2.getClass()));
        } catch (Throwable e) {
            target = MethodHandles.lookup().findStatic(OperatorBootstrap.class, jenisOperator,
                MethodType.genericMethodType(2));
        }
        target.asType(MethodType.genericMethodType(2));
        return target.invokeWithArguments(arg1, arg2);
    }

    public static CallSite bootstrap(MethodHandles.Lookup lookup, String name, MethodType type) throws NoSuchMethodException, IllegalArgumentException {
        return new ConstantCallSite(ARITMATIKA.bindTo(name).asType(type));
    }

    public static Object tambah(Integer arg1, Integer arg2) {
        return arg1 + arg2;
    }

    public static Object tambah(String arg1, String arg2) {
        return arg1 + arg2;
    }

    public static Object tambah(Object arg1, Object arg2) {
        if (arg1.getClass()== String.class || arg2.getClass()==String.class) {
            return new StringBuilder().append(arg1).append(arg2);
        } else {
            throw new IllegalArgumentException("Operator tambah (+) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
        }
    }

}

Kode program di atas akan memakai class di package java.lang.invoke seperti MethodHandles, MethodHandle dan MethodType untuk menentukan pemanggilan method. Penggunaan java.lang.invoke seharusnya lebih cepat bila dibandingkan dengan memakai reflection (sebelum Java 7, implementasi di atas hanya bisa dilakukan dengan melalui reflection!). Untuk meningkatkan kinerja, implementasi yang lebih baik dari di atas adalah dengan menggunakan inline caching.

Berikutnya, saya akan mengubah isi BahasakuVisitor.java dimana saya akan menggantikan implementasi aritmatika dengan pemanggilan instruksi invokedynamic seperti yang terlihat pada kode program berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    private static final Handle OPERATOR_HANDLE;

    static {
        OPERATOR_HANDLE = new Handle(H_INVOKESTATIC,
            "com/wordpress/thesolidsnake/bahasaku/target/jvm/OperatorBootstrap",
            "bootstrap",
            "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;");
    }

    ...

    @Override
    public Object visitPembagian(@NotNull BahasaKuParser.PembagianContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("bagi", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPerkalian(@NotNull BahasaKuParser.PerkalianContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("kali", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPenjumlahan(@NotNull BahasaKuParser.PenjumlahanContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("tambah", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    @Override
    public Object visitPengurangan(@NotNull BahasaKuParser.PenguranganContext ctx) {
        visit(ctx.expr(0));
        visit(ctx.expr(1));
        mv.visitInvokeDynamicInsn("kurang", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", OPERATOR_HANDLE);
        return null;
    }

    ...

}

Dampak penggunaan dari invokedynamic adalah kini pada saat menjalankan program, harus ada referensi ke library BahasaKu. Sebagai contoh, kini saya menjalankan sebuah program BahasaKu dengan cara seperti yang terlihat pada gambar berikut ini:

Menguji Operator Penjumlahan

Menguji Operator Penjumlahan

Berikutnya, saya akan melakukan implementasi untuk operator -. Khusus untuk ekspresi String, operator - akan menghilangkan bagian String lainnya. Saya juga membuat beberapa kombinasi lainnya, seperti yang terlihat pada kode program berikut ini (di OperatorBootStrap.java):

public class OperatorBootstrap {

    ...

    public static Object kurang(String arg1, String arg2) {
        return arg1.replace(arg2, "");
    }

    public static Object kurang(String arg1, Integer arg2) {
        return arg1.substring(0, arg1.length() - arg2);
    }

    public static Object kurang (Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator kurang (-) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Saya akan mencoba operator tersebut, seperti yang terlihat pada gambar berikut ini:

Menguji Operator Pengurangan

Menguji Operator Pengurangan

Pada gambar di atas, operator - pada BahasaKu memiliki arti yang spesial untuk String. Ekspresi seperti 'Latihan.txt' - '.txt' memiliki arti berupa hilangkan '.txt' dari 'Latihan.txt'. Bila ingin menghilangkan 5 huruf terakhir dari sebuah String, saya dapat menggunakan ekspresi seperti 'Latihan' - 5. Sebagai alternatif lain, saya juga bisa membuat implementasi seperti pada bahasa dinamis lain dimana String akan dikonversi menjadi bilangan sehingga operator - tetap melakukan operasi matematika.

Berikutnya, saya akan melakukan implementasi untuk operator * (perkalian) yang terlihat seperti berikut ini:

public class OperatorBootstrap {

    ...

    public static Object kali(Integer arg1, Integer arg2) {
        return arg1 * arg2;
    }

    public static Object kali(String arg1, Integer arg2) {
        StringBuilder builder = new StringBuilder(arg1);
        for (int i=1; i<arg2; i++) {
            builder.append(arg1);
        }
        return builder.toString();
    }

    public static Object kali (Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator kali (*) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Saya dapat mencoba operator tersebut, seperti yang terlihat pada gambar berikut ini:

Menguji Operator Perkalian

Pada gambar di atas, terlihat bahwa penggunaan operator * untuk sebuah String dan angka akan menyebabkan pengulangan String tersebut sebanyak angka. Sebagai contoh, ekspresi '='*20 akan menghasilkan '=' sebanyak 20 karakter.

Terakhir, saya akan melakukan implementasi untuk operator / (pembagian) yang terlihat seperti berikut ini:

public class OperatorBootstrap {

    ...

    public static Object bagi(Integer arg1, Integer arg2) {
        return arg1 / arg2;
    }

    public static Object bagi(Object arg1, Object arg2) {
        throw new IllegalArgumentException("Operator bagi (/) tidak mendukung [" + arg1 + "] dan [" + arg2 + "]");
    }

}

Tidak ada yang spesial pada operator / karena ia hanya melakukan operasi pada angka dan akan menolak operasi pada String, seperti yang diperlihatkan pada gambar berikut ini:

Menguji Operator Pembagian

Menguji Operator Pembagian

Akhirnya, BahasaKu sudah memiliki dukungan operasi aritmatika sederhana pada tipe data terbatas yang dimilikinya.

Menciptakan Bahasa Dinamis Di Java: Part 2 – Membuat Back End Dengan ASM

Artikel ini merupakan bagian dari seri artikel tentang pembuatan bahasa dinamis BahasaKu yang berjalan di Java yang terdiri dari:

  1. Membuat Front End Dengan ANTLR
  2. Membuat Back End Dengan ASM
  3. Membuat Operasi Aritmatika
  4. Memanggil Java Libraries

Kode program untuk seri artikel ini dapat ditemukan di https://github.com/JockiHendry/BahasaKu

Back-end di compiler bertugas menerjemahkan teks yang sudah dibaca oleh front-end menjadi target yang diinginkan. Biasanya, target adalah instruksi mesin yang dapat langsung dimengerti oleh CPU. Instruksi mesin tersebut harus dikemas dalam sebuah format file yang dimengerti oleh sistem operasi. Sebagai contoh, biarpun sama-sama memakai prosesor x86 yang sama dengan instruksi mesin yang sama, hasil kompilasi untuk platform Windows dan Linux akan berbeda. Pada sistem operasi Windows, instruksi mesin harus dikemas dalam format Portable Executable dimana pengguna lebih mengenalnya sebagai file EXE. Pada sistem operasi Unix (dan turunannya seperti Linux), instruksi mesin harus dikemas dalam format Executable and Linkable Format. Dengan adanya pemisahan compiler menjadi front-end dan back-end, sebuah front-end yang sama dapat dipakai pada beberapa back-end berbeda sehingga dapat mendukung beberapa platform yang berbeda.

BahasaKu tidak men-target instruksi CPU secara langsung, melainkan ke bytecode Java. Sebagai informasi, setiap hasil kompilasi program Java selalu berada dalam bentuk bytecode bukan instruksi mesin yang dimengeri CPU. Bytecode Java berperan sebagai bahasa perantara sehingga file yang sama dapat berjalan di beberapa platform berbeda. Referensi struktur file class Java dan daftar instruksi bytecode dapat ditemukan di Java Virtual Machine Specification. Secara garis besar, instruksi bytecode lebih sederhana dan lebih terbatas bila dibandingkan dengan instruksi CPU x86. Walaupun demikian, menulis kode program byte per byte (setiap instruksi diwakili oleh sebuah byte) tetap merupakan hal yang melelahkan dan rentan terhadap kesalahan.

Yup! Pada zaman sekarang, dimana sudah bermunculan banyak bahasa pemograman tingkat tinggi, menulis program byte per byte akan terlihat sulit dan aneh. Mungkin itu sebabnya pada zaman dahulu kala, tidak semua orang bisa menjadi programmer 🙂 Beruntungnya, di Java terdapat library ASM. Library ini menyediakan API untuk men-build bytecode secara user friendly dan juga dapat melakukan perhitungan ukuran stack frame secara otomatis. Saya segera mengubah file build.gradle saya menjadi seperti berikut ini (agar Gradle akan men-download ASM bagi saya):

...
dependencies {
    compile group: 'org.antlr', name: 'antlr4', version: '4.1'
    compile group: 'org.ow2.asm', name: 'asm-all', version: '4.2'
}
...

Satu hal yang sering dijumpai dalam bytecode Java adalah type descriptor yang berupa:

  • Z untuk tipe boolean
  • C untuk tipe char
  • B untuk tipe byte
  • S untuk tipe short
  • I untuk tipe int
  • F untuk tipe float
  • J untuk tipe long
  • D untuk tipe double
  • Lnama/package/namaClass; untuk sebuah object. Contoh: Ljava/lang/Object; atau Ljavax/swing/JFrame;
  • Awali dengan tanda [ untuk array. Contoh: int[] diwakili [I dan Object[] diwakili dengan [Ljava/lang/Object;
  • V untuk void

Method descriptor mewakili signature dari sebuah method. Sebagai contoh, signature untuk void main(String[] args) adalah ([Ljava/lang/String;)V. Contoh lainnya, signature untuk int tambah(int angka1, int angka2) adalah (II)I.

Agar code completion di IntelliJ IDEA dapat mengenali class dari ASM, jangan lupa men-klik tombol Refresh Gradle Project di window JetGradle seperti yang terlihat pada gambar berikut ini:

Memperbaharui dependency Gradle

Memperbaharui dependency Gradle

Saya akan mulai dengan membuat sebuah implementasi visitor yang diberi nama BahasaKuVisitor.java di package com.wordpress.thesolidsnake.bahasaku.target.jvm. Dengan menggunakan visitor design pattern, saya akan menelusuri parse tree yang telah dihasilkan oleh front-end. Karena JVM menerima unit kompilasi dalam bentuk class, maka langkah pertama yang saya lakukan adalah membuat sebuah class melalui bytecode, seperti yang terlihat pada kode program berikut ini:

package com.wordpress.thesolidsnake.bahasaku.target.jvm;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuBaseVisitor;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import org.antlr.v4.runtime.misc.NotNull;
import org.objectweb.asm.*;
import org.objectweb.asm.util.TraceClassVisitor;
import java.io.*;
import java.nio.file.*;
import static org.objectweb.asm.Opcodes.*;

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    private ClassWriter cw;
    private ClassVisitor cv;
    private MethodVisitor mv;
    private Label label0;
    private boolean enableTrace;
    private File sourceFile;
    private String namaSourceFile;

    public BahasaKuVisitor(File sourceFile) {
        this(sourceFile, false);
    }
    public BahasaKuVisitor(File sourceFile, boolean enableTrace) {
        this.sourceFile = sourceFile;
        if (!sourceFile.getName().endsWith(".baku")) {
            throw new RuntimeException("Nama source file [" + sourceFile.getName() + "] tidak diakhiri dengan .baku");
        }
        this.namaSourceFile = sourceFile.getName().substring(0, sourceFile.getName().lastIndexOf('.'));
        this.enableTrace = enableTrace;

        cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        if (enableTrace) {
            cv = new TraceClassVisitor(cw, new PrintWriter(System.out));
        } else {
            cv = cw;
        }

        // Membuat sebuah class public dengan nama (tanpa package) sesuai dengan namaFile
        cv.visit(V1_7, ACC_PUBLIC + ACC_SUPER, namaSourceFile, null, "java/lang/Object", null);

        // Membuat method static void main(String[] args).
        // Seluruh implementasi kode program BahasaKu akan ada di method ini.
        mv = cv.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
        mv.visitCode();
        label0 = new Label();
        mv.visitLabel(label0);
    }

}

Pada kode program di atas, saya menggunakan visitor dari ASM untuk membuat bytecode yang mewakili sebuah class. Lagi-lagi visitor design pattern seperti pada ANTLR. Karena proses menghasilkan bytecode sangat rentan terhadap kesalahan, saya menyediakan pilihan untuk memakai TraceClassVisitor yang akan akan mencetak instruksi yang dihasilkan ke layar. Fitur ini akan mempermudah saya untuk mencari kesalahan, tapi umumnya tidak akan begitu berguna bagi programmer yang memakainya.

Hasil kompilasi kode program BahasaKu adalah dalam bentuk sebuah file class (bytecode Java) dengan nama yang sama seperti nama source. Class ini hanya memiliki sebuah method, yaitu public static void main(String[] args) yang didalamnya berisi implementasi kode program BahasaKu. Sangat sederhana sekali, bukan?

Berikutnya, saya akan mulai menerjemahkan setiap grammar satu per satu. Saya akan mulai dari yang paling sederhana, yaitu ekspresi yang mewakili data angka, String dan boolean. Untuk itu saya perlu men-override method visitAngka(), visitString(), visitBooleanTrue() dan visitBooleanFalse() yang dihasilkan ANTLR, seperti pada contoh berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitAngka(@NotNull BahasaKuParser.AngkaContext ctx) {
        Integer angka = Integer.valueOf(ctx.getText());
        mv.visitTypeInsn(NEW, "java/lang/Integer");
        mv.visitInsn(DUP);
        mv.visitIntInsn(BIPUSH, angka);
        mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Integer", "", "(I)V");
        return angka;
    }

    @Override
    public Object visitString(@NotNull BahasaKuParser.StringContext ctx) {
        String str = ctx.getText().substring(1, ctx.getText().length()-1);
        mv.visitLdcInsn(str);
        return str;
    }

    @Override
    public Object visitBooleanTrue(@NotNull BahasaKuParser.BooleanTrueContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "TRUE", "Ljava/lang/Boolean;");
        return Boolean.TRUE;
    }

    @Override
    public Object visitBooleanFalse(@NotNull BahasaKuParser.BooleanFalseContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/Boolean", "FALSE", "Ljava/lang/Boolean;");
        return Boolean.FALSE;
    }

    ...

}

Seluruh tipe data pada BahasaKu adalah objek. Pada kode program di atas, saya menerjemahkan angka menjadi objek Integer. Demikian juga, literal iya akan diterjemahkan menjadi Boolean.TRUE dan literal tidak akan diterjemahkan menjadi Boolean.FALSE. Nilai mereka akan di-push ke dalam stack dan siap dipakai oleh perintah yang berada di level di atasnya pada syntax tree.

Berikutnya, saya akan membuat implementasi yang akan menerjemahkan keyword tampilkan menjadi pemanggilan System.out.println() seperti pada berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    @Override
    public Object visitTampilkan(@NotNull BahasaKuParser.TampilkanContext ctx) {
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
        visit(ctx.expr());
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/Object;)V");
        return null;
    }
    ...

}

Walaupun belum membuat implementasi semua rule, saya sudah tidak sabar untuk mencoba memakai BahasaKu. Oleh sebab itu, saya akan membuat sebuah method yang berguna untuk menulis file class (atau output dari compiler BahasaKu) yang terlihat seperti berikut ini:

public class BahasaKuVisitor extends BahasaKuBaseVisitor {

    ...

    public void generateOutput() {
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
        cv.visitEnd();

        Path targetFile = Paths.get(sourceFile.getAbsoluteFile().getParent().toString(), namaSourceFile + ".class");;
        System.out.println("Menghasilkan target file di lokasi: " + targetFile.toString() + "\n");
        try {
            Files.write(targetFile, cw.toByteArray(),
                StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
        } catch (Exception ex) {
            System.out.println("Terjadi kesalahan saat membuat file target!");
            ex.printStackTrace();
        }
    }

    ...

}

Berikutnya, saya membuat sebuah file Main.java di package com.wordpress.thesolidsnake.bahasaku yang isinya seperti berikut ini:

package com.wordpress.thesolidsnake.bahasaku;

import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuLexer;
import com.wordpress.thesolidsnake.bahasaku.grammar.BahasaKuParser;
import com.wordpress.thesolidsnake.bahasaku.target.jvm.BahasaKuVisitor;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.tree.ParseTree;

import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        if (args.length==0) {
            System.out.println("Compiler BahasaKu\n\n" +
                "Contoh proses kompilasi:\n" +
                "bahasaku namafilesource.baku\n" +
                "bahasaku source1.baku source2.baku\n" +
                "bahasaku -trace namafilesource.baku\n");
            return;
        }

        boolean enableTrace = false;
        List sourceFiles = new ArrayList();

        // Melakukan parsing argumen
        // Dapat melakukan kompilasi lebih dari satu file
        for (String arg: args) {
            if ("-trace".equals(arg)) {
                enableTrace = true;
            } else {
                sourceFiles.add(arg);
            }
        }

        // Memulai proses kompilasi
        for (String sourceFile: sourceFiles) {
            Path file = Paths.get(sourceFile);
            if (!file.toFile().exists()) {
                System.out.println("Tidak dapat menemukan file: [" + file + "]");
                continue;
            }

            try {

                // Front-end
                ANTLRInputStream antlrInputStream = new ANTLRInputStream(Files.newInputStream(file));
                BahasaKuLexer lexer = new BahasaKuLexer(antlrInputStream);
                CommonTokenStream commonTokenStream = new CommonTokenStream(lexer);
                BahasaKuParser parser = new BahasaKuParser(commonTokenStream);
                ParseTree parseTree = parser.file();

                // Back-end
                BahasaKuVisitor jvmVisitor = new BahasaKuVisitor(file.toFile(), enableTrace);
                jvmVisitor.visit(parseTree);
                jvmVisitor.generateOutput();

            } catch (Exception ex) {
                System.out.println("Terjadi kesalahan [" + ex.getMessage() + "]");
                ex.printStackTrace();
            }
        }

    }

}

Class di atas akan mengerjakan lexer dan parser dari ANTLR (front-end) serta menghasilkan file class (back-end). Saya perlu memberi tahu Gradle bahwa class ini adalah class yang harus dijalankan pertama kali dengan menambahkan baris berikut ini pada build.gradle:

...
apply plugin: 'application'
mainClassName = 'com.wordpress.thesolidsnake.bahasaku.Main'
...

Setelah itu, klik tombol Refresh Gradle Project seperti yang ditunjukkan pada gambar 1. Kemudian, saya akan men-double click pada task installApp seperti yang diperlihatkan pada gambar berikut ini:

Menjalankan task installApp

Menjalankan task installApp

Gradle akan membuat folder install dimana di dalamnya terdapat folder bin dan lib, seperti yang terlihat pada gambar berikut ini:

Struktur Folder Yang Dihasilkan

Struktur Folder Yang Dihasilkan

Pada folder lib akan terdapat seluruh jar yang dibutuhkan. Sementara itu, pada folder lib tersedia launcher yang dapat dipakai untuk menjalankan proyek. Launcher Bahasaku.bat dipakai untuk platform Windows, sementara itu platform lainnya dapat memakai launcher Bahasaku.

Sekarang, saya akan mencoba sebuah membuat sebuah program BahasaKu. Sebagai contoh, saya akan membuat sebuah file dengan nama C:\contoh\Latihan.baku yang isinya seperti ini:

tampilkan 'Solid Snake'
tampilkan 10
tampilkan 20
tampilkan 'Percobaan'

Saya dapat men-compile file di atas dengan perintah seperti berikut ini:

Menjalankan Program Yang Memakai BahasaKu

Menjalankan Program Yang Memakai BahasaKu

Saya menjalankan compiler BahasaKu dengan menyertakan -trace supaya saya dapat melihat instruksi yang dihasilkan oleh ASM. Terlihat bahwa dari file Latihan.baku akan tercipta sebuah file Latihan.class. Output dalam bentuk bytecode Java ini dapat dikerjakan oleh JVM dengan menggunakan perintah java sama seperti program Java lainnya. Jadi, ini menunjukkan sebuah fakta bahwa sebuah program untuk Java tidak harus selalu dibuat dalam bahasa pemograman Java. Fakta menarik lainnya adalah karena output BahasaKu adalah sebuah bytecode dan class Java biasa, maka ia dapat dipanggil oleh program Java lainnya (yang memakai bahasa pemograman Java)! Ini memungkinkan terjadinya polyglot programming dimana programmer menggabungkan lebih dari satu bahasa pemograman di proyek yang sama.

Saat ini, compiler BahasaKu masih belum mendukung variabel dan operasi aritmatika. Oleh sebab itu, pada tulisan berikutnya, saya akan men-implementasi-kan rule yang berhubungan dengan variabel dan operator aritmatika.

ASM #5: Memakai Agen

Pada tulisan sebelumnya, class hasil transformasi tidak dapat dipakai secara langsung, melainkan harus memakai interface atau superclass-nya. Bagaimana jika aku tetap ingin memakai seperti biasa tanpa membuat interface atau superclass? Salah satu solusi yang mungkin adalah dengan memakai agent. Class tetap dapat dipakai seperti biasa, bahkan tanpa membuat classloader baru. Hal ini karena agent akan dikerjakan saat JVM dijalankan.

JVM akan mencari dan mengerjakan method milik agent class yang definisinya seperti berikut:

public static void premain(String agentArgs, Instrumentation inst);
atau
public static void premain(String agentArgs);

Berikut ini adalah contoh agent class yang aku pakai:

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new ClassFileTransformer(){

      @Override
      public byte[] transform(ClassLoader loader, String className,
          Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
          byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className.equals("latihan/Music")) {
          try {

            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, 0);
            ToStringAdapter adapter = new ToStringAdapter(writer);

            reader.accept(adapter,0);
            return writer.toByteArray();

          } catch (Exception ex) {
            ex.printStackTrace();
          }
        }
        return classfileBuffer;
      }

    });
  }

}

Agent di atas akan melakukan transformasi melalui ToStringAdapter (turunan dari ClassAdapter) yang sudah aku buat di tulisan sebelumnya. Untuk dapat memakai agent ini, aku masih harus membuat jar dari class ini, serta menambahkan manifest yang isinya sebagai berikut:

Manifest-Version: 1.0
Main-Class: latihan.Main
Premain-Class: latihan.Agent
Boot-Class-Path: lib/asm-3.1.jar lib/asm-commons-3.1.jar lib/asm-util-3.1.jar

Entry yang dibutuhkan untuk agent adalah Premain-Class dan Boot-Class-Path. Fungsinya dapat ditebak dari namanya: Premain-Class sama seperti Main-Class, hanya saja ia menyatakan class mana yang merupakan agent. Boot-Class-Path sama seperti Class-Path, hanya saja ia menyatakan classpath yang khusus dipakai oleh agent.

Sekarang, class Music dapat dipakai layaknya class normal lainnya (tentu saja dengan pengecualian sudah punya definisi method toString() secara otomatis):

public class Main {

  public Main() {
    Music m = new Music();
    m.setMusicID("MID-123");
    m.setTitle("TITLE");
    System.out.println("Music = " + m);
  }

  public static void main(String[] args) throws Exception {
    new Main();
  }

}

Btw, untuk menjalankan program di atas, aku tidak lupa menambahkan argument -javaagent agar JVM mengerjakan agent class, seperti:

java -javaagent:latihanASM.jar -jar latihanASM.jar

ASM #4: Ready For Action

Setelah mempelajari cara kerja JVM dan bytecode Java, aku siap untuk melakukan modifikasi class Java secara dinamis. Sebagai contoh kasus, aku akan mengubah method toString() untuk setiap bean agar menampilkan nilai dari setiap field secara otomatis.

Seperti yang telah aku catat sebelumnya, sebuah class dikenali oleh JVM melalui kombinasi dari classloader dan nama class-nya. Untuk melakukan instrumentasi bytecode, aku pasti membuat classloader sendiri. Sebagai efek sampingnya, class hasil instrumentasi tidak akan bisa dipakai oleh class yang di-load oleh JVM sebelumnya. Yup, tidak dapat dipakai secara langsung, tetapi dapat dipakai melalui superclass atau interface-nya. Aku akan mencoba memakai metode interface di tulisan kali ini. Next time, aku akan mencoba memakai agent untuk melakukan instrumentasi class yang di-load JVM. Dengan agent, aku tidak perlu memakai superclass atau interface, tetapi proses menjalankan program menjadi lebih panjang.

Byte code instrumentation dengan interface memang tidak begitu cocok untuk contoh kasus kali ini, tapi setidaknya ini cara yang paling gampang. Aku akan membuat interface Music dan implementasi-nya MusicImpl.

public interface Music {

  public String getMusicID();
  public void setMusicID(String musicId);
  public String getTitle();
  public void setTitle(String title);

}

Lalu, aku akan membuat classloader yang melakukan instrumentasi untuk MusicImpl seperti berikut:

import java.util.*;
import org.objectweb.asm.*;
import static org.objectweb.asm.Opcodes.*;

public class MyClassLoader extends ClassLoader {

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    if (name.endsWith("Impl")) {
      try {
        ClassReader reader = new ClassReader(name);
        ClassWriter writer = new ClassWriter(reader,0);
        ToStringAdapter adapter = new ToStringAdapter(writer);
        reader.accept(adapter,0);
        byte b[] = writer.toByteArray();
        return defineClass(name,b,0,b.length);

      } catch (Exception e) {
        e.printStackTrace();
      }

    }
    return super.findClass(name);
  }

  protected class ToStringAdapter extends ClassAdapter {

    private Map<String,String> mapFields;
    private String className;

    public ToStringAdapter(ClassVisitor cv) {
      super(cv);
      mapFields = new HashMap<String,String>();
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {
      if (name.equals("toString")) {
        return null;
      }
      return super.visitMethod(access, name, desc, signature, exceptions);
    }

    @Override
    public void visitEnd() {
      MethodVisitor mv = cv.visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", null, null);
      mv.visitCode();
      mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
      mv.visitInsn(DUP);
      mv.visitLdcInsn(className + ": ");
      mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "(Ljava/lang/String;)V");

      for (String field : mapFields.keySet()) {
        mv.visitLdcInsn(field + "=[");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
          "(Ljava/lang/String;)Ljava/lang/StringBuilder;");

        mv.visitVarInsn(ALOAD, 0);
        mv.visitFieldInsn(GETFIELD, className, field, mapFields.get(field));
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
          "(Ljava/lang/Object;)Ljava/lang/StringBuilder;");

        mv.visitLdcInsn("]; ");
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append",
          "(Ljava/lang/String;)Ljava/lang/StringBuilder;");

      }

      mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString",
        "()Ljava/lang/String;");
      mv.visitInsn(ARETURN);
      mv.visitMaxs(3,1);
      mv.visitEnd();
      super.visitEnd();
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc,
        String signature, Object value) {
      if (access==ACC_PRIVATE) {
        mapFields.put(name,desc);
      }
      return super.visitField(access, name, desc, signature, value);
    }

    @Override
    public void visit(int version, int access, String name,
        String signature, String superName, String[] interfaces) {
      className = name;
      super.visit(version, access, name, signature, superName, interfaces);
    }

  }

}

Pada classloader tersebut, aku akan menambahkan method toString() pada semua class yang namanya diakhiri dengan “Impl”. Jika class tersebut sudah memiliki method toString(), aku akan menghapus method toString() yang sudah ada sebelum menambahkan yang baru. Untuk mempersingkat kode program, aku menganggap bahwa class yang di-transformasi memiliki field turunan dari Object saja, bukan tipe native seperti int, char, dsb. Hal ini karena aku men-hard code method append() yang aku panggil dari class StringBuilder dengan descriptor (Ljava/lang/Object;)Ljava/lang/StringBuilder;

Sekarang aku dapat mencoba memakai classloader di atas, misalnya dengan kode sederhana berikut:

public class Main {

  public Main() throws Exception {
    MyClassLoader myCL = new MyClassLoader();
    Music m = (Music) myCL.findClass("latihan.MusicImpl").newInstance();
    m.setMusicID("MID-123");
    m.setTitle("TITLE");
    System.out.println("Music = " + m);
  }

  public static void main(String[] args) throws Exception {
    new Main();
  }

}

Dan outputnya adalah:

Music = latihan/MusicImpl: title=[TITLE]; musicID=[MID-123];

Hal ini juga berlaku untuk seluruh class lain yang namanya diakhir dengan “Impl”.. Aku tidak perlu lagi membuat method toString() secara manual..

ASM #3: Kode-Kode Bahasa Java

Hari ini aku akan mempelajari bagaimana byte code Java (atau bahasa assembler-nya Java) bekerja. Yang akan menjadi “bahan penelitian” adalah method berikut:

public String toString() {
  return "[MUSIC]: MusicID=[" + musicID + "]; title=[" + title + "]";
}

Setelah di-compile, method di atas akan memiliki atribut code seperti berikut:

00 03 00 01 00 00 00 25 
bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00 
2d b6 00 35 b0 00 00 00 
02 00 18 00 00 00 06 00 
01 00 00 00 1f 00 19 00 
00 00 0c 00 01 00 00 00 
25 00 1a 00 1b 00 00

Wew, bagaimana menerjemahkan byte code yang tampak asing ini? Dua byte pertama, 00 03, adalah jumlah maksimum untuk operand stack. Nilai ini harus dihitung oleh compiler secara manual. Dua byte berikutnya, 00 01, adalah jumlah variabel lokal yang dipakai dalam method (termasuk parameter). Empat byte berikutnya, 00 00 00 25, menunjukkan kalau ada 0x25 (desimal 37) byte berikutnya yang berisi byte code. Dengan demikian, bagian yang benar-benar berisi byte code adalah 25 byte berikutnya, yaitu:

bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00
2d b6 00 35 b0

Byte pertama, 0xbb menunjukkan bahwa instruksi tersebut adalah instruksi new. Dua byte berikutnya, 00 27, adalah referensi ke constant pool index ke-0x27 atau ke-39, dimana berupa class java/lang/StringBuilder. Ini artinya, JVM akan membuat instance baru dari class java.lang.StringBuilder dan men-push referensi instance tersebut ke operand stack.

Instruksi berikutnya adalah, 0x59, adalah instruksi dup. Instruksi ini tidak membutuhkan operand. Pada saat menemukan instruksi ini, JVM akan men-push nilai yang sama dengan nilai yang berada paling atas di operand stack saat ini.

Operand Stack (Sebelum dup):

| Instance StringBuilder | <-- TOP

Operand Stack (Setelah dup):

| Instance StringBuilder | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya 0x12, adalah instruksi ldc. Instruksi ini akan men-load operand-nya (byte berikut-nya, yaitu 0x29 atau 41) yang merupakan referensi index di constant pool ke operand stack. Index ke-41 di operand stack adalah sebuah String “[MUSIC]: MusicID=[“. Dengan demikian, isi operand stack akan menjadi:

Operand Stack (Setelah ldc):
| "[Music: MusicID=["      |  <-- TOP
| Instance StringBuilder   |
| Instance StringBuilder   |

P.S: Jumlah operand stack tidak boleh melebihi maksimum yang telah ditentukan sebelumnya, yaitu 3. Jika ada perintah yang men-push sekali lagi, maka JVM akan memunculkan pesan kesalahan.

Instruksi berikutnya, 0xb7, adalah instruksi invokespecial. Instruksi ini membutuhkan dua byte operand, 00 2b (desimal 43), yang merupakan referensi ke method <init> di constant pool. Selain itu, instruksi ini akan mengambil informasi instance class mana yang akan dikerjakan method-nya melalui informasi di operand stack. Karena method yang didefinisikan di index 43 adalah constructor StringBuffer yang mengambil sebuah parameter String, maka isi operand stack menjadi:

Operand Stack (Setelah invokespecial):
| Instance StringBuilder   | <-- TOP

Berikutnya, terdapat instruksi 0x2a atau aload_0. Instruksi ini akan me-load nilai yang terdapat di local variable array yang berada di index 0 ke operand stack. Seperti yang kita tahu, nilai yang paling awal di local variable array adalah referensi ke class yang sedang aktif (this). Akibatnya, isi operand stack akan menjadi:

Operand Stack (Setelah aload_0):
| this (instance class Music)      | <-- TOP
| Instance StringBuilder           |

Instruksi berikutnya, 0xb4, adalah instruksi getfield. Instruksi ini akan men-push nilai field yang ditunjukkan oleh dua byte berikutnya, 00 14 (desimal 20), ke operand stack. Sebelumnya, ia akan men-pop terlebih dahulu dari operand stack untuk mengetahui class mana yang akan diambil nilai field-nya. Karena index ke-20 adalah referensi ke field musicID, maka isi operand stack akan menjadi:

Operand Stack (Setelah getfield):
| "nilai musicID"        | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya, 0xb6, adalah instruksi invokevirtual. Instruksi ini akan mengerjakan method yang direferensikan oleh dua byte berikutnya, 00 2d (atau desimal 45), yaitu method append. Setelah pengerjaan method append, isi operand stack akan menjadi:

Operand Stack (Setelah invokevirtual):
| Instance StringBuilder | <-- TOP

Instruksi berikutnya, 0x12, kembali lagi merupakan instruksi ldc, untuk men-push String “]; title=[” ke operand stack. Setelah itu, instruksi 0xb6 (invokevirtual) akan mengerjakan method append milik StringBuffer. Selanjutnya, instruksi 0x2a (aload_0) akan me-load nilai this ke operand stack. Dan instruksi 0xb4 (getfield) akan mengambil nilai dari field title dan men-push nilai tersebut ke operand stack. Instruksi 0xb6 (invokevirtual) kembali mengerjakan method append. Instruksi 0x12 (ldc) yang berikutnya akan men-push String “]” ke operand stack. Instruksi berikutnya 0xb6 kembali mengerjakan method append. Instruksi 0xb6 berikutnya akan mengerjakan method toString milik StringBuffer. Dan instruksi terakhir, 0xb0 adalah instruksi areturn, yang akan keluar dari method serta mengembalikan nilai berupa referensi class yang akan di-pop dari operand stack.

Wew, perjalanan yang cukup panjang hanya untuk sebuah method yang sangat sederhana, bahkan hanya satu baris saja. Untungnya, aku tidak perlu selalu menerjemahkan bytecode dengan cara manual seperti ini. Di situs ObjectWeb, dimana aku mendownload ASM, aku juga dapat men-download plugin Eclipse untuk melihat isi byte code dari sebuah source code Java. Setelah meng-install plugin tersebut, aku dapat memilih Window, Show View, Byte Code. Akan muncul sebuah window Byte Code di sebelah kanan workbench yang akan berisi dissasembler dari source code Java dimana kursor editor sedang aktif. Ini adalah output untuk method yang aku pakai di tulisan ini:

  public toString()Ljava/lang/String;
   L0
    LINENUMBER 31 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "[MUSIC]: MusicID=["
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    ALOAD 0
    GETFIELD latihan/Music.musicID : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]; title=["
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD latihan/Music.title : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]"
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Llatihan/Music; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 1

ASM #2: Membedah File Class

Hari ini aku akan mempelajari bagaimana format sebuah file class. Untuk itu, aku membuat sebuah file class dari source code sederhana berikut:

package latihan;

public class Music {

  private String musicID;
  private String title;
  private final String musicPrefix = "01";

  public Music(String musicId, String title) {
    super();
    musicID = musicId;
    this.title = title;
  }
  public String getMusicID() {
    return musicID;
  }
  public void setMusicID(String musicId) {
    musicID = musicId;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public static Music getMusic(String id) {
    return null;
  }

}

Setelah selesai men-compile Music.java menjadi Music.class, aku membuka file Music.class melalui hex editor untuk melihat isinya. Setiap file class Java selalu diawali dengan empat byte 0xca, 0xfe, 0xba, 0xbe. Cafe Babe, huh? Setelah itu terdapat dua byte yang berisi minor version (nilainya 0) dan dua byte yang berisi major version (nilainya 50 pada contoh class-ku).

Dua byte berikutnya, 00 35, menunjukkan bahwa di dalam constant pool terdapat 34 item. Nilai ini merupakan jumlah item di constant pool ditambah dengan 1. Setelah itu, berikutnya adalah kumpulan byte untuk constant pool. Ukuran dan isinya dapat berbeda-beda tergantung kode program. Secara umum, constant pool terdiri atas satu atau lebih item, dimana setiap item selalu diawali dengan sebuah byte yang berisi jenis item (byte ini disebut tag).

Sebagai contoh, berikut ini adalah sebagian isi constant pool untuk kode program di atas:

Item #1
Tag: Method References
Class Index: 7
Name & Type Index: 27

...

Item #7
Tag: Class
Name Index: 33

...

Item #13
Tag: UTF8
String Value: <init>

...

Item #25
Tag: UTF8
String Value: SourceFile

Item #26
Tag: UTF8
String Value: Music.java

Item #27
Tag: Name & Type
Name Index: 13
Descriptor Index: 34

...

Item #33
Tag: UTF8
String Value: java/lang/Object

Item #34
Tag: UTF8
String Value: ()V

Item pertama menunjukkan informasi method, untuk class yang ada di item ke-7 (yaitu java/lang/Object). Informasi mengenai method yang ditunjukkan oleh item pertama dapat dilihat lebih lanjut di item ke-27, yang selanjutnya memberikan informasi mengenai method bernama “<init>” (item ke-13) yang descriptor-nya adalah “()V” (item ke-34). Btw, aku tidak membuat method dengan nama “<init>” di source code, darimana munculnya method “<init>”? Ini adalah nama yang khusus diberikan untuk constructor.

Setelah isi constant pool, terdapat dua byte yang berisi access flag. Nilai flag ini menunjukkan apakah class ini termasuk class final, abstract, atau merupakan interface. Nilai untuk class percobaan hari ini adalah 0x0021 yang merupakan kombinasi dari flag 0x0001 (class public) dan flag 0x0020 (selalu harus di-set untuk compiler baru).

Berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi class ini (class yang dirujuk oleh keyword this). Lalu, berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi super-class (parent-class). Dalam contoh class percobaan, super class-nya adalah java/lang/Object (item ke-7 di constant pool). Nilai dua byte ini dapat berupa 0x0000, jika class ini adalah java/lang/Object (satu-satunya class di Java yang tidak punya super class).

Setelah itu terdapat dua byte yang berisi jumlah inteface yang di-implement oleh class ini, diikuti dengan rangkaian index di constant pool untuk menjelaskan interface tersebut. Karena class percobaanku tidak men-implementasi-kan interface, nilai interface count adalah 0.

Berikutnya adalah dua byte yang berisi jumlah field/variabel yang didefinisikan dalam class ini (nilainya adalah 3, karena aku hanya mendefinisikan 3 variabel di source code). Berikutnya adalah struktur yang menjelaskan informasi field tersebut. Ukurannya bisa berbeda tergantung pada jumlah field/variabel yang didefinisikan di dalam class ini. Berikut ini adalah isi informasi field pada class percobaan hari ini:

Field #0
Access Flag:  PRIVATE
Name Index: 8
Descriptor Index: 9
Attribute Count: 0

Field #1
Access Flag:  PRIVATE
Name Index: 10
Descriptor Index: 9
Attribute Count: 0

Field #2
Access Flag:  PRIVATE  FINAL
Name Index: 11
Descriptor Index: 9
Attribute Count: 1
  Attribute Name Index: 12
  Attribute Length: 2 [00 02 ]

Setelah informasi field, terdapat informasi mengenai method. Dua byte pertama, seperti biasa, menunjukkan jumlah method yang didefinisikan dalam class ini (nilainya adalah 6, karena aku mendefinisikan 6 method di source code). Berikutnya, terdapat informasi mengenai method. Pada bagian ini terdapat informasi byte code Java untuk masing-masing method yang terletak di atribut dengan nama “Code”. Aku akan mempelajari lebih lanjut tentang atribut ini di kemudian hari. Ini adalah contoh informasi method pada class percobaan:

Method #0
Access Flag:  PUBLIC
Name Index: 13
Descriptor Index: 14
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 61
00 02 00 03 00 00 00 15
2a b7 00 01 2a 12 02 b5
00 03 2a 2b b5 00 04 2a
2c b5 00 05 b1 00 00 00
01 00 10 00 00 00 16 00
05 00 00 00 0a 00 04 00
07 00 0a 00 0b 00 0f 00
0c 00 14 00 0d 

Method #1
Access Flag:  PUBLIC
Name Index: 17
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 04 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 0f 

Method #2
Access Flag:  PUBLIC
Name Index: 19
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 04 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 12 00 05
00 13 

Method #3
Access Flag:  PUBLIC
Name Index: 21
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 05 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 15 

Method #4
Access Flag:  PUBLIC
Name Index: 22
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 05 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 18 00 05
00 19 

Method #5
Access Flag:  PUBLIC  STATIC
Name Index: 23
Descriptor Index: 24
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 26
00 01 00 01 00 00 00 02
01 b0 00 00 00 01 00 10
00 00 00 06 00 01 00 00
00 1b

Dan bagian yang paling terakhir dari sebuah file class Java adalah atribut untuk class tersebut. Dua byte pertama berisi informasi jumlah atribut, di-ikuti dengan definisi atribut. Ini adalah contoh informasi atribut untuk class percobaanku:

Attribute #0
Attribute Name Index: 25
Attribute Length: 2
00 1a

Atribut tersebut adalah atribut “Source File” yang nilainya adalah referensi ke item 0x1a (atau desimal 26) di constant pool, yang nilainya tidak lain adalah “Music.java”.

ASM #1: JVM, Mesin Yang Tidak Nyata

Untuk melakukan bytecode instrumentation melalui library seperti ASM, aku setidaknya harus mengerti bagaimana cara kerja JVM.  Setiap program Java, atau tepatnya class Java, akan dijalankan oleh Java Virtual Machine (JVM).  Mirip seperti mesin asli, JVM juga memiliki struktur-struktur seperti register dan stack.

Masing-masing thread yang dijalankan oleh JVM memiliki sebuah register Program Counter (pc).  Register pc berisi lokasi alamat instruksi yang sedang dikerjakan.  Untuk method native,  nilai register pc tidak didefinisikan.

Setiap thread di JVM juga memiliki Java Virtual Machine Stack yang kegunaannya mirip seperti di program biasa seperti C, seperti menampung nilai variabel lokal dan hasil perhitungan sementara.

Seluruh thread di JVM memiliki memory yang di-share yang disebut heap. Heap adalah lokasi memori yang berisi informasi instance dari sebuah class dan array.  Garbage collector akan bekerja secara otomatis untuk menentukan instance yang tidak dibutuhkan lagi dan membebaskan lokasi memori di heap sehingga memori dapat dipakai ulang.

Setiap kali sebuah method dipanggil, JVM akan membuat sebuah frame di Java Virtual Machine stack untuk thread bersangkutan.  Setelah method selesai dikerjakan, JVM akan memusnahkan frame tersebut.   Di dalam frame terdapat informasi local variables.  Sebuah “slot” local variable dapat mengandung nilai boolean, byte, char, short, int, float, reference, atau returnAddress.  Nilai long atau double memerlukan dua “slot”  local variable.

Setiap “slot” local variable memiliki index berurut mulai dari 0, 1, 2, dst.   Pada saat method mulai dijalankan, local variable 0 akan berisi referensi ke object yang mengandung method tersebut (nilai this dalam program Java).  Local variable 1, 2, dst berisi nilai parameter secara berurut.

Frame juga mengandung apa yang disebut operand stack yang banyak dipakai oleh instruksi JVM untuk menulis dan membaca nilai.  Pada saat frame pertama kali dibuat, operand stack tidak memiliki isi.

Instruksi JVM terdiri atas sebuah byte yang berisi opcode, diikuti oleh operands (jika ada).  Kebanyakan instruksi JVM bekerja pada tipe data tertentu.  Sebagai contoh, instruksi yang diawali dengan huruf i bekerja pada data int, huruf l bekerja pada data long, huruf s bekerja pada data short, huruf b bekerja pada data byte, huruf c bekerja pada data char, huruf f bekerja pada data float, huruf d bekerja pada data double, dan huruf a bekerja pada data reference.  O ya, di bahasa pemograman Java ada tipe data boolean, tapi  JVM tidak mengenal istilah boolean.   JVM akan menganggap boolean sebagai integer.

Instruksi load dan store dipakai untuk mengambil nilai dan menulis nilai ke dalam local variable. Sebagai contoh, iload 1 akan menulis local variable 1 yang bertipe int ke dalam operand stack.  Sebaliknya, istore 1 akan menulis nilai int yang ada di operand stack ke local variable 1.

Contoh instruksi aritmatika yang tersedia seperti iadd (penjumlahan), isub (pengurangan), imul (perkalian), idiv (pembagian),  irem (modulus), ineg (negation), ishl (shift left), ishr (shift right), ior (bitwise OR), iand (bitwise AND), ixor (bitwise XOR), iinc (local variable increment), lcmp (perbandingan).  Sebagai contoh, instruksi iadd akan menjumlahkan dua nilai int yang ada di operand stack (men-pop dua nilai terakhir), kemudian menyimpan hasil penjumlahan ke operand stack (men-push hasil penjumlahan).

JVM juga menyediakan beberapa instruksi untuk konversi, seperti i2l (int to long), i2f (int to float), i2d (int to double), l2f (long to float), l2d (long to double) dan f2d (float to double).

Untuk membuat instance class baru, terdapat instruksi new.  Untuk membuat array baru, tersedia instruksi newarray, anewarray, dan multianewarray.  Untuk meng-akses field dari sebuah object, instruksi berikut dapat dipergunakan: getfield, putfield, getstatic, putstatic.  Beberapa instruksi lain yang berkaitan dengan class: arraylength, instanceof, dan checkcast.

Instruksi berikut dipakai untuk memanipulasi operand stack: pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, dan swap.

Contoh instruksi yang dipakai untuk control transfer (percabangan), misalnya: ifeq (jika sama dengan), iflt (jika lebih kecil), tableswitch, goto, dan ret.

Untuk memanggil method dari sebuah class, instruksi berikut dapat dipergunakan: invokevirtual, invokeinterface, invokespecial, dan invokestatic.  Untuk keluar dari method, instruksi berikut dapat dipergunakan sesuai dengan tipe kembalian dari method: return, ireturn, lreturn, freturn, dreturn, dan areturn.  Untuk membuat exception, instruksi athrow dapat dipergunakan.

Untuk mendukung sinkronisasi (keyword synchronize di bahasa pemograman Java), JVM menggunakan monitor, dan menyediakan instruksi berikut: monitorenter dan monitorexit.

ASM: Assemblernya Java

Just kidding… ASM yang sedang aku pelajarin bukanlah bahasa mesin Java, tetapi suatu library untuk mengotak-atik class Java yang sudah tercompile. Konon nama library tersebut diambil dari keyword __asm__ di C, yang kepanjangannya mungkin adalah assembler.

Seandainya aku memiliki sebuah class dari source berikut:

package latihan;

public class Music {

  private String musicID;
  private String title;

  public String getMusicID() {
    return musicID;
  }
  public void setMusicID(String musicId) {
    musicID = musicId;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }

}

Class tersebut aku simpan dengan nama di folder c:\ dengan nama music.class. Dengan bantuan ASM, aku akan membuat program untuk membaca file music.class dan menampilkan setiap variabel dan method milik class tersebut:

public class Main {

  public Main() {
    try {
      BufferedInputStream bis = new BufferedInputStream(new FileInputStream("c:\\Music.class"));
      ClassReader classReader = new ClassReader(bis);
      classReader.accept(new ClassEventHandler(), 0);
      bis.close();
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  public static void main(String[] args) {
    new Main();
  }

  protected class ClassEventHandler implements ClassVisitor {

    @Override
    public void visit(int version, int access, String name,
        String signature, String superName, String[] interfaces) {

      System.out.println("Classname: " + name);
      System.out.println("Superclass Name: " + superName);
      System.out.println("Implements Interface: " + interfaces.toString() + "\n");

    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
      return null;
    }

    @Override
    public void visitAttribute(Attribute attr) {
    }

    @Override
    public void visitEnd() {
      System.out.println("\nVisit End");
    }

    @Override
    public FieldVisitor visitField(int access, String name, String desc,
        String signature, Object value) {

      System.out.println("Field Descriptor: " + desc);
      System.out.println("Field Name: " + name);
      System.out.println("\n");
      return null;

    }

    @Override
    public void visitInnerClass(String name, String outerName,
        String innerName, int access) {
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {

      System.out.println("Method Descriptor: " + desc);
      System.out.println("Method Name: " + name);
      System.out.println("\n");
      return null;
    }

    @Override
    public void visitOuterClass(String owner, String name, String desc) {
    }

    @Override
    public void visitSource(String source, String debug) {
    }

  }

}

Hasil dari program di atas adalah:

Classname: latihan/Music
Superclass Name: java/lang/Object
Implements Interface: [Ljava.lang.String;@190d11

Field Descriptor: Ljava/lang/String;
Field Name: musicID

Field Descriptor: Ljava/lang/String;
Field Name: title

Method Descriptor: ()V
Method Name: <init>

Method Descriptor: ()Ljava/lang/String;
Method Name: getMusicID

Method Descriptor: (Ljava/lang/String;)V
Method Name: setMusicID

Method Descriptor: ()Ljava/lang/String;
Method Name: getTitle

Method Descriptor: (Ljava/lang/String;)V
Method Name: setTitle

Visit End

Output di atas memakai penamaan internal setelah class di-compile. Wajar saja, karena yang dibaca bukanlah source code program Java, melainkan class Java yang sudah ter-compile. Sebagai contoh, descriptor Ljava/lang/String menunjukkan bahwa itu adalah tipe data java.lang.String. Nilai lain yang mungkin seperti Z untuk boolean, C untuk char, B untuk byte, dan sebagainya. Descriptor ()V pada method menunjukkan bahwa method tersebut tidak menerima argumen dan tidak mengembalikan nilai (void). Descriptor (Ljava/lang/String;)V menunjukkan bahwa method tersebut tidak mengembalikan nilai (void), dan menerima parameter berupa sebuah object java.lang.String.

ASM tidak hanya bisa membaca class, tapi juga bisa membuat class ke dalam bentuk byte array. Byte array ini nantinya dapat ditulis ke dalam file, atau langsung di-load oleh JVM untuk dipakai. Sebagai contoh, aku akan membuat program Java yang membuat class Music di memory (tanpa membuat source code Music.java):

package latihan;

import java.lang.reflect.Field;
import org.objectweb.asm.ClassWriter;
import static org.objectweb.asm.Opcodes.*;

public class Main {

  public Main() {

    MusicClassLoader cl = new MusicClassLoader();
    try {
      Class<?> c = cl.findClass("latihan.Music");
      System.out.println("Class name = " + c.getName());
      for (Field f : c.getFields()) {
        System.out.println("Field name: " + f.getName() + "; Field type: " + f.getType());
      }
    } catch (Exception e) {
      e.printStackTrace();
    }

  }

  public static void main(String args[]) {
    new Main();
  }

  protected class MusicClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
      if (name.equals("latihan.Music")) {
        ClassWriter cw = new ClassWriter(0);
        cw.visit(V1_6, ACC_PUBLIC, "latihan/Music", null, "java/lang/Object", null);
        cw.visitField(ACC_PUBLIC, "musicID", "Ljava/lang/String;", null, null);
        cw.visitField(ACC_PUBLIC, "title", "Ljava/lang/String;", null, null);
        cw.visitEnd();
        byte[] b = cw.toByteArray();
        return defineClass("latihan.Music", b, 0, b.length);
      }
      return findClass(name);
    }
  }
}

Pada contoh di atas, class latihan.Music yang aku buat on-the-fly terlihat tidak begitu berguna karena ia bukan tidak memiliki ‘kontrak’ yang jelas (misalnya tidak melakukan implementasi interface tertentu). Pada tulisan berikutnya, aku akan mencoba mempelajari fungsi ASM yang lain, yaitu melakukan transformasi class.