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).

Perihal Solid Snake
I'm nothing...

3 Responses to Menciptakan Bahasa Dinamis Di Java: Part 4 – Memanggil Java Libraries

  1. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 3 – Membuat Operasi Aritmatika | The Solid Snake

  2. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 2 – Membuat Back End Dengan ASM | The Solid Snake

  3. Ping-balik: Menciptakan Bahasa Dinamis Di Java: Part 1 – Membuat Front End Dengan ANTLR | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: