ABCLからJavaのメソッドを叩いてみた

  • 2
    Like
  • 8
    Comment

はじめに

本稿では,我らがCommonLispの処理系の一つであり,JVM上で動作する処理系であるABCL(Armed-Bear-CommonLisp)を用いて,Java言語で記述されたクラスやメソッドを操作するための基本的な手順を示します.

「Java用のAPIしか提供されていない,でもCommonLisp(以下,CLと表記)でそのアプリケーションを使いたい!」なんてことがあるでしょう.ありますよね?えぇ,きっとありますとも.

「でも日本語ドキュメントはほぼ無いし,Clojureとかはあんまり得意じゃないし,Lisp以外で書くのはキツイ!」なんて方がいらっしゃるかは不明ですが,本稿はそういったかた向けにABCL基礎中の基礎を書く記事です.

この記事では,CLからの簡単なJavaメソッドの呼び出しだけを行います.細かいところまでごちゃごちゃするのは別の記事で書きますので,それを求めている人は得るものがないかもしれませんのでブラウザバック推奨です.

参考になるドキュメント

  これが一番(というか唯一)参考になりました.

  はじめはコレを参考にしていましたが,あんまり意味がわからずハマりました.

  迷ったらソースコードにあたるのはいいアイデアかもしれません.

  ABCLに関する情報は基本的には当然ここにありますよね.

日本語しか読めない,読まないという人は,残念ながら現状ではドキュメントがありません.そんなに難しい英語は書かれていないので,頑張って読んでください.

この記事で基本がわかっていただければ幸いですね!

では,以降はCommonLispからJavaを呼び出すためのABCLの仕様を説明していきます.

実行環境

念のため,今回の実行環境をば.
恐らく他のプラットフォームでも問題なく動くはず.
検証はしてませんが...

  • Machine: Mac mini(Late 2014)
  • OS: Mac OS X 10.11.6
  • ABCL: 1.5.0
  • Java: 1.8.0_60(64bit)

Javaの静的メソッド呼び出し

まずは静的メソッドの呼び出しについて説明します.
今回呼び出しに用いる簡単なJavaコードはコレです.

Calc.java
public class Calc {
    public static int addTwoNumbers(int a, int b) {
        return a + b;
    }
}

Calcクラスの静的メソッドとして,整数型の引数を2つ取り,それらの合計を返り値として返すだけの簡単なお仕事をしてくれます.
このaddTwoNumbersメソッドをCommonLispから呼び出してみましょう.
コードはこんな感じになります(↓)

calc.lisp
(defun add-two-num (num1 num2)
  (let ((result (jstatic "addTwoNumbers" "Calc" num1 num2)))
    result))

jstaticがJavaの静的メソッドを呼び出すための組み込み関数です.
第1引数に呼び出すメソッド名(文字列),第2引数に対象のメソッドが属するクラス,第3引数以降がメソッドの引数となります.
評価するとメソッドの実行結果が返り,resultに束縛され,最後に出力されるというシンプルなものです.

サンプルプログラムを見てもコレだけしか書いていませんが,当然Javaコードはそのままでは呼び出せませんので,コンパイルしてクラスパスを通します.
あんまりその辺わからないよって人のために一応書いておくと,

$ javac Calc.java
$ ls
Calc.class Calc.java calc.lisp

となれば成功です.
コンパイルが通らない場合は何か書き間違いしているのでチェックし直してください.
と言いつつ,この長さのコードで間違いも何もないかもですね.

では,REPLに移って実際に呼び出してみましょう!

CL-USER> (load "/Path/to/calc.lisp")
T
CL-USER> (add-to-classpath "/Path/to/Calc.java/")
("/Path/to/java-class")
CL-USER> (add-two-num 2 3)
5
CL-UER> (add-two-num -1 9)
8

正しく呼び出せていますね!
add-to-classpathはJavaのクラスファイルへのパスを通すためのABCLの組み込み関数です.
Javaの方でクラスパスを通しても良いのですが,こちらの方が動的に読み込めてわかりやすいです(個人的見解).

Javaの動的メソッド呼び出し

流石にStaticメソッドだけ呼び出せてもそこまで嬉しくはないので,メソッドのDynamicディスパッチを確認しましょう.
今回試しに使うコードは以下です.

Hero.java
public class Hero {
    private int hp;
    private int mp;

    public Hero() {
       hp = 100;
       mp = 100;
    }

    public void getDamage(int damage){
       hp -= damage;
    }

    public void useMagic(int consumption){
       mp -= consumption;
    }

    public String showStatus(){
       return "HP: "+hp+", MP: "+mp;
    }

    public static void main(String[] argv){
       Hero hero = new Hero();
    }
}

あるヒーローがいてそいつは生まれたときに「HP:100」と「MP:100」を持つ.攻撃を受けると一定数のダメージを受け,魔法を使うとMPを消費します.ステータスを確認すると,残りHPとMPが表示されます.

こんなクラス設計あかんやろ,っていうのは置いておいていただいて,仮にこんな感じのクラスを定義したと思ってください,お願いします.

hero.lisp
(add-to-classpath "/Path/to/java-class-file")
(defun hero-status ()
  (let* ((hero-class (jclass "Hero"))
     (hero-instance (jnew (jconstructor "Hero")))
     (method (jmethod hero-class "showStatus"))
     (result (jcall method hero-instance)))
    result))

まずは,このHeroクラスをインスタンス化して,showStatusメソッドを呼び出します.初期状態の「HP: 100,MP: 100」が出力されるはずです.

先程はREPLのトップレベルでadd-to-classpathを評価しましたが,もちろんLispファイルの中にかけば,Load時に実行されます.

jclassは文字列を引数に取り,その文字列の名前を持つJavaのクラス参照を返します.ここで返されるのはインスタンスではなくクラスオブジェクトです.

jnewはコンストラクタオブジェクトを引数にとり,インスタンスを生成します.
(jconstructor "クラス名(文字列)")によってコンストラクタオブジェクトを取得しないとインスタンス化できないので注意してください1

jmethodは呼び出すメソッドをオブジェクトとして取得します.Javaのクラスオブジェクトとメソッド名,必要であれば引数の型クラスを渡します2

最後にjcallに先程取得したメソッドオブジェクトと,インスタンスオブジェクトを渡し,メソッドを実行します.

実際に実行してみると以下のようになるはずです.

CL-USER> (load "/Path/to/hero.lisp")
T
CL-USER> (hero-status)
"HP: 100, MP: 100"

次に,HP減少メソッドとMP減少メソッドを使った後で,ステータス確認メソッドを実行します.両メソッドは引数に減少する値を渡せばその分だけ値を引いてくれます.

呼び出し用のLisp式はこんな感じになります.

hero-extend.lisp

(defun extended-hero-status (hp mp)
  (let* ((hero-class (jclass "Hero"))
     (hero-instance (jnew (jconstructor "Hero")))
     (java-int-class (jclass "int"))
     (get-damage (jmethod hero-class "getDamage" java-int-class))
     (use-magic (jmethod hero-class "useMagic" java-int-class))
     (method (jmethod hero-class "showStatus")))
    (jcall get-damage hero-instance hp)
    (jcall use-magic hero-instance mp)
    (let (result (jcall method hero-instance))
      result)))

まさにJavaっぽい書き方になってしまいますが,この関数を実際に評価すると

CL-USER> (load "/Path/to/exted-hero.lisp")
T
CL-USER> (extended-hero-status 5 10)
"HP: 95, MP: 90"

となり,正しく動作していることがわかっていただけるのではないかと思います.

おわりに

いかがでしたでしょうか.雰囲気はつかめたでしょうか.
ABCLなら,ComonLispでJavaライブラリを比較的簡単に使え,これまでにJava言語にて書かれたいろいろなプログラムをCLプロジェクトに組み込むことができるかもしれませんね.

今回は紹介しませんでしたが,Java言語からCommonLispを呼び出すことももちろん可能です.詳しくは上で述べているユーザリファレンスを参照してください.

間違い等の指摘や感想などお待ちしております.


  1. 筆者はここでjnewの引数がわからずハマった 

  2. 引数自体をここで渡すのではなく,引数の型クラスオブジェクトを渡します.具体的には,(jclass "int")のようなものを渡すことで,ディスパッチするメソッドを決定します.