2007/11/01

Janus - Java to C++ source code converter without GC,thread and ...

Janus

Java to C++ source code converter
without GC,thread and ...

飯田 陽彦
株式会社バックス

haruhiko_iida@vacs.co.jp
概要
Janus(ヤヌス)は、Javaソースコードから、C++ソースコードおよびヘッダファイルを生成する言語コンバータである。JavaとC++の大きな違いの一つであるガーベジコレクションについては、これをサポートしない。メモリ管理は、C++と同様、開発者の責任であり、不要なメモリの解放は、Javaソースコード内に、C++のdelete,delete[]に相当するコードを明示的に記述することで行う。マルチスレッドのサポート、例外処理におけるfinally節等、JavaにあってC++にない要素は、単純に無視され、演算子多重定義やテンプレートなども、元のソースコードがJavaである事から、使用する事は出来ない。

つまり、Janusコードは、JavaとC++の構文要素の共通サブセットを用いて記述する事になり、さらに悪く言えば、JavaとC++の短所を集めた言語である、と言うことが出来る。

にも関わらず、Janusコードを用いる事が適切であるような状況がいくつか考えられる。

まず、Java仮想マシンが実装可能な資源を持たない組み込み機器向けのソフトウェアを記述する場合である。このような機器は、資源のコスト低下により、将来的にはJava仮想マシンが実装可能になるであろうし、Java仮想マシン実装技術自体の進展や、Personal JavaやEmbedded Javaの登場で、現在の機器でもJava仮想マシンが実装可能になる事が期待できる。このような場合、Janusコードでソフトウェアを記述しておけば、その状況に応じて、Javaのまま使用するのか、Janusを用いてC++に変換してから使用するかを選択する事が出来る。

また、Java仮想マシンが実装されている通常のパーソナルコンピュータやワークステーションであっても、リアルタイム系、特にサーバー系のソフトウェアを記述する場合には、性能面でJavaが使用可能かどうかの判断を行うのが困難である。このような場合、Janusを使用する事で、開発したソフトウェアをJavaとC++の双方で試験し、その結果を元に最終的な判断を下すことが出来る。

背景
Javaとその関連技術の進展により、ソフトウェアプラットフォームとしてのJava仮想マシンの存在は、日々その重要度を増している。そのため、ソフトウェア開発者に、この新しいプラットフォームに自身のソフトウェアを移植する事が求められている。しかし、Java仮想マシンの稼働する環境が増えつつあるとはいえ、Java仮想マシンが実装出来ない環境、とりわけ組込機器等のプラットフォームは、依然存在しつづける事が予想される。そのため、開発者は、機能的には同一のソフトウェアを、Java言語と、C,C++等の既存言語との両方で開発・維持しなければならず、その負担は大きくなってしまう。

この問題を打破するために、J2C,JCC等のNative Compilerが開発されている例があり、Janus開発の前に、これらが使用出来ないかどうか検討した。しかし、これらはいずれも完全なJavaソフトウェアを移植するためのものであり、当然ながらガーベジコレクションを初めとするJava仮想マシンの持つ機能を内包しているような実行ファイルを生成する。このため、実行ファイルの規模が大きくなり、(少なくとも私自身が対象としている)小資源プラットフォーム向けに使用する事はできない事が判明した。

一方、より直接的な方法として、既存のC,C++で記述されたソフトウェアをJavaに変換する方法(C to Java Converterの開発等)も検討したが、こちらはC,C++の持つ低レベル処理(ポインタ操作等)をJavaに置換することが困難である事が判明した。また、仮にそれらの方法が確立したとしても、今後益々増加する事が予想されるJava仮想マシンプラットフォームへのソフトウェア開発を既存言語で行うというのは、あまり好ましい事ではないだろうという感覚があり、この方法も断念した。

以上のような試行錯誤の上、最後にJanusのコンセプトである、JavaからC++への変換という着想を得た。一般に知られている通り、Javaの構文はC++のそれに酷似しており、JavaのソースコードをC++に変換する事が可能ではないかという事は以前から考えてはいたが、私自身、lexやyacc等の言語操作系ソフトウェアの扱いに不慣れだった事もあり、自分自身に開発が可能であるとは思われなかった。しかし、Sun Microsystemsが開発し、一般公開している、Javaで記述され、Javaを使ってソフトウェア記述が可能なコンパイラ・コンパイラJavaCCの存在を知り、これを用いると比較的容易にJavaソースコードの構文解析が出来る事が判明した。そこで、試しに簡単なconverterを試作したところ、たった3日で、望むものにかなり近い結果を得ることが出来、これが、今回のJanus開発につながった。

変換の実際
以下に、具体的な変換規則を順を追って説明する。

プリミティブ型の変換
Javaにおけるプリミティブ型は、C++と異なり、対象CPUや処理系に依存せず、言語仕様で厳密に規定されている。しかし、その事を無視すれば、意味的にはC++のプリミティブ型とほぼ1対1に対応するので、Janusでは、それらプリミティブ型の宣言を以下のように変換する。

変換前 変換後 備考
byte signed char -
short signed short -
int signed int -
long signed int64_t C++言語規定ではない
float float -
double double -
char unsigned short wchar_tではない
boolean signed int -

クラスとその参照
参照は、ガーベジコレクションという安全装置の存在、また参照値に対する演算が出来ない事を除けば、C ++のポインタと意味的に酷似している。そこで、参照型をポインタ型に置換する。具体的には、Javaで定義したクラス名に'_'(アンダースコア)を付加したものをC++のクラス名とし、そのポインタ型に対し、Javaのクラス名を使用する。 C++のクラス定義は、Javaソースコードと同名で、末尾が.Hであるヘッダファイルに出力される。また、Javaのクラスは、暗黙のうちにクラスObjectのサブクラスであるため、C++への変換でもそれを踏襲する。C++におけるクラス Object(厳密にはObject_)は、利用者が自分の用途に応じて用意する事ができる。(オブジェクト破壊の際の安全性を高めるための、デストラクタ(後述)に対するvirtual属性の付加等)


Point.java Point.h



class Point {
int x;
int y;
}




class Point_;
typedef Point_* Point;
class Point_ : public Object_ {
signed int x;
signed int y;
};


以上のようにクラス定義がされているので、参照型の宣言は、new 演算子適用時等の特殊な状況を除き、そのままC++ソースコードに反映される。

また、フィールドやメソッドの参照のための'.'は、クラス参照'::'、又はポインタ参照'->'演算子に置換される。(なお、現在の Janusの実装では、与えられた識別子がクラスを示しているのか、オブジェクトを示しているのかの判断を正しく行っていない。現在の実装では、識別子の最初の文字がUpper Caseであった場合、これをクラスと見なしている。従ってクラス名は、必ずUpper Caseでなければならず、反対にオブジェクト参照型の名称は、必ずLower Caseでなければならない)


Foo.java Foo.cpp


class Foo {
aMethod() {

Point pt;

pt = new Point();
pt.x = 10;
pt.y = Constant.maxY;
;
;
}
}





Foo_::aMethod(void) {

Point pt;

pt = new Point_();
pt->x = 10;
pt->y = Constant_::maxY;
;
;
}



配列
プリミティブ型や参照型の配列もJavaにおいては一種の参照型であり、上記のオブジェクト参照型と同様、これもポインタとみなして変換を行う。C++で、デフォルトコンストラクタの存在するクラスに対して、その実体の配列を生成する演算、例えば、new Point_[10]のような記述は、Java言語仕様に対応する記法が存在しないので、記述できない。


Foo.java Foo.cpp


class Foo {
aMethod() {
int[] ia;
Point[] pa;

ia = new int[10];
pa = new Point[20];
;
;
}
}





Foo_::aMethod(void) {

int* ia;
Point* pa;

ia = new int[10];
pa = new Point[10];
;
;
}



delete,delete[]演算子
C++ではガーベジコレクションがサポートされておらず、不要なメモリは、開発者が明示的にdelete,delete[]演算子を記述して解放する必要がある。Janusでも、その方針をそのまま踏襲しており、同様の記述をする必要がある。ただし、JanusコードがそのままJavaコードとして利用可能なようにするため、クラス'CC'を定義し、そのクラスメソッド 'dispose'、及び、'disposeArray'の呼び出しを、delete,delete[]演算子に置換するようになっている。Janus コードをJavaコードとして利用する場合には、上記のクラスメソッドを持つクラスCCを自身のパッケージ内に含める必要がある。

また、Javaでオブジェクトの持つ資源を解放するために利用されるメソッド'finalize'は、C++のデストラクタに置換される。


Foo.java Foo.cpp


class Foo {
aMethod() {

Point pt;
int[] ia;

pt = new Point();
ia = new int[10];
;
;
CC.dispose(pt);
CC.disposeArray(ia);
}
void finalize() {
CC.dispose(x);
CC.dispose(y);
}
}





Foo_::aMethod(void) {

Point pt;
int* ia;

pt = new Point_();
ia = new int[10];
;
;
delete pt;
delete[] ia;
}
Foo_::~Foo_(void) {
delete x;
delete y;
}


CC.java


public final class CC {
public static void dispose(Object o) {}
public static void disposeArray(Object o) {}
}



disposeを減らすための特別な場合
Javaではほとんどの場合、オブジェクト生成をnew演算子で行う。そのため、Janusコードを記述する場合、それと対になるCC.disposeが必要になり、メモリ管理に関する危険性が高くなる。一方、純粋にC++で記述する場合、new演算子の他に、スタティック領域やスタック領域に直接オブジェクトを生成するためのいくつかの記法が用意されている。そこでJanusでは、final属性を持つ型宣言があり、その初期演算子としてnewが記述されている場合に限り、new演算を取り去り、そこに直接オブジェクトを生成するようなC++コードに置換する。


Foo.java Foo.h


class Foo {
final Point x = new Point();
final Point[] y = new Point[10];






class Foo_ : public Object_ {
Point_ x;
Point y[10];
};


Foo.cpp



aMethod() {

final Point pt = new Point();
final Point[] apt = new Point[10];
final int[] ai = new int[20];

pt.x = 10;
pt.y = 20;
}
}





Foo_::aMethod(void) {

Point_ pt;
Point apt[10];
int ai[20];

pt->x = 10;
pt->y = 20;
}


この特別な記法を用いた場合の問題点とJansuの対処方法を以下に示す。

メンバー選択演算子の変換について
C++に変換後のPointオブジェクトのメンバーにアクセスするには、'->'ではなく、'.'を用いなければならないが、Janusでは、上記に見られるように、他のオブジェクトと同様、一律に'->'に変換している。その代わり、Janusでは、Pointクラスを変換する際、各々のクラスに以下のような演算子多重定義宣言を付加し、'->'演算やキャスト演算が、結果として正しく動作するようにしている。

Foo.h

class Point_ : public Object_ {
public: inline Point operator ->(void) {return this;}
public: inline operator Point(void) {return this;}
}


配列の添え字
この方法で配列を宣言する場合、C++では、[]内に記述する値は定数式でなければならないが、この検査はJanus内では行っていない。従って、記述の際には注意が必要である。

ローカル変数として使用する場合
この方法で生成されたオブジェクトは、メソッド終了時に破壊される。従って、生成したオブジェクトを保存したり、戻り値として使用してはならない。


定数宣言
Janusでは、'static final [プリミティブ型] = 値'の形式を持つフィールド宣言に限り、以下のような特殊な置換を行う。これは、このフィールドが定数式である事をC++に明示するために必要な措置である。

Foo.java Foo.h


public class Foo {
public static final int LIMIT = 10;
public final int[] ia = new int[LIMIT];
;
;
}





class Foo_ : public Object_ {
public: enum {LIMIT = 10};
public: int ia[LIMIT];
;
;
}



throw式に現れるnewのdispose
throw文で一般に行われるnew演算に限り、以下のような形式に変換される。Janusで例外をthrowする場合、この形式に沿わなければならない。また、この形式に沿う限り、生成された例外オブジェクトのdisposeに関して意識する必要はない。


Foo.java Foo.cpp


throw new Exception();





throw &Exception_();



マルチスレッドに関する制限
マルチスレッドに関する構文、synchronized属性およびsynchronized ブロックは、単純に無視される。マルチスレッド操作を含む(OS・処理系依存の)純粋なC++コードを記述し、これと変換済のJanusコードとを組み合わせてマルチスレッドソフトウェアを開発する事は可能ではあるが、上記の同期命令は使用できないので、注意が必要である。


Foo.java Foo.cpp


public synhronized void foo() {
;
synchronized(x) {
a = 1;
b = 2;
c = 3;
}
}





void Foo_::foo(void) {
;
{
a = 1;
b = 2;
c = 3;
}
}



finally節に関する制限
Janusではfinally節をサポートしていない。C++に対応する言語要素が存在せず、goto命令を用いたエミュレーションも、複雑なfinally節の動作順序を正しく真似るのはほぼ不可能である事からである。従って、Janus コードを記述する際には、finally節を用いてはならない。もし、記述してあった場合、そのfinally節は、後続のブロックを含めて、除去される。なお、finally節を含まない、try-catchは、C++でもサポートされているので、そのまま利用する事が出来る。
パッケージの使用
Janusには、Javaの優れた特徴の1つである、コアパッケージがない。ただし、これは、単にコアパッケージに相当するライブラリを作成する手間がなかったという問題であり、Janus自身の制約ではない。コアパッケージを使用したい場合、利用者自身が、必要とするコアパッケージの機能を持つライブラリを別途作成する必要がある。このような場合のため、packege宣言及びimport宣言を以下のように変換する。

まず、暗黙のうちにimportされているjava.lang.*のための#includeプリプロセッサ命令を付加する。

import宣言は、必ず、最後が*で終わる形式でなければならない。宣言は、区切りのピリオドをスラッシュに置換し、さらに、最後の*を、直前の名称に置き換え、末尾に.Hを付加したものを挿入する#includeプリプロセッサ命令に置換される。最後に、package宣言は、ピリオドで区切られた名称最後の部分を名称とし、末尾に.Hを付加したものを挿入する#includeプリプロセッサ命令に置換される。順番を入れ替えるのは、importされたクラス群をパッケージ内で参照する場合のためである。

他のパッケージをimportせずに使用する構文は、Janusではサポートしていない。他のパッケージを使用する場合には、必ず(末尾が *であるような)import宣言を行う必要がある。このため名前の衝突が起きる可能性があるが、これについては良い回避策がないため、何も行っていない。複数のパッケージを用いる場合、名前の衝突が起きないよう注意する必要がある。


Foo.java Foo.cpp


package JP.co.vacs.Test;
import lang.util.*;





#include
#include
#include "Test.h"



予約語の問題
Javaの予約語ではなく、C++では予約語である語がいくつか存在する。それらの語は、Javaでは識別子として使用できるが、そのままではC++に変換する事ができない。以下に、そのような語の一覧を示す。

asm signed auto
operator sizeof typedef
delete friend union
struct unsigned virtual
inline register template
enum extern

そこで、Janusではこれらの語に限り、語の後ろに'_'(アンダースコア)を付加する。しかし、この変換を行った結果、名前の衝突が発生する可能性があるため、上記の語は識別子としての使用をなるべく避ける方が望ましい。

Janusの使用法
単一のソースコードをコンバートする。

java JP.co.vacs.janus.Janus Foo.java

Foo.javaを元に、Foo.h,Foo.cppを生成する。現実には、単一クラスの生成はあまり意味がなく、Janusの生成試験以外にはあまり使えない。
同一パッケージの全ソースコードをコンバートする。

java JP.co.vacs.janus.Janus @files.txt

packages.txtに記述されたクラス群を全て変換する。同時に、パッケージ全体を他所から参照するためのヘッダファイル<パッケージ名>.hファイルも生成する。 files.txtには、変換対象となるJavaファイル名を列挙する。また、先頭に#がある場合は、その行をコメントとみなす。また、クラスが継承関係にある場合、スーパークラスのファイルはサブクラスのファイルの前に記述しなければならない。

files.txtの内容

#
# コメント(インデントは単に見やすくするためのものである)
#
Foo.java
Animal.java
Tiger.java
Dog.java
Tosa.java
Kisyu.java
#
# continue
#

ソースコード及び実行モジュール
実際のソフトウェアは以下から入手することが出来る。Janus自身はJavaで記述されているので、Java仮想マシンが実装されたプラットフォームならば、どこでも稼働するものと思われる。(ちなみに私自身はWindows95上で開発・実行している)

J970812.zip

あとがき
Janusは、Java及びJavaCCを用いて開発した。JavaCCは、とても優れたJava言語のためのコンパイラコンパイラであり、また、jjtreeという、構文木を自動生成するような定義ファイルを生成するプリプロセッサも同時に提供されている。更に、Java言語仕様のための構文定義ファイルも提供されており、Java言語解析のためには至れり尽くせりの環境である。無論、これらのツールの源となっているJavaが優れた言語/環境である事は今更言うまでもない。
Sun Microsystemsがもたらしたこれらのソフトウェア群を利用する事により、Janusは驚くほど短期間に開発する事が出来た。この場を借りて、深く感謝したい。

余談であるが、このソフトウェアの名称となったJanusは、古代ローマの双面の軍神の名前からとった。 JavaとC++の2面性を持ち、ソフトウェア開発者の戦場とも言える、現実のソフトウェア開発の現場で直ちに使用出来る事を狙いとして開発したソフトウェアの名称として相応しいものであると思う。

参考文献
書籍名 著者 出版社
プログラミング言語C++ B.Stroustrup
(訳:斉藤信男、三好博之、追川修一、宇佐美徹) アジソン・ウェスレイ・トッパン
Java言語入門 L.Lemay,C.Perkins
(訳:武舎広幸、久野禎子、久野靖) プレンティスホール出版

No comments: