このページは、西暦 2000 年問題不対応(非対応にあらず)です。 2000 年の記事は 1999 年の記事の後ろにありますのでご注意下さい。
大学の教養部(1、2 年生)の授業「計算機プログラミングI」で、 今学期から Java プログラミング(注)が始まった。
(注)昨年までは教官によって Pascal を使ったり C++ を使ったりしていたのだが、今年からは Java で統一された。
そのため、学内のニュースグループ(学内の人は、「
」
というニュースグループがそれですので、覗いてみてください)
に提出課題が山のように投稿されるようになった。
私は傍観者としてほぼ全部のグループに目を通しているのであるが、 たまに自分でフォローを入れることがある。そのためには自前で Java 処理系を用意しなければならない。
実はすでに JDK(Java Development Kit)1.2.1 をダウンロードしてある (家で friends2 を動かすために JRE(Java Runtime Environment)が必要だったので、 大学の太い線を使って、その superset たる JDK を落としたのである) のだが、実は今までは自分でコードを書いたことがなかったので、 この機会に色々やってみることにする。
まずはいちばん簡単なコードから。
classHelloWorld
{
public static voidmain
( String argv [])
{
System.out.println ("hello, world."
);
}
}
これを「javac HelloWorld.java
」でコンパイルする。
元のファイル名に関係なく、ファイル中の「class
HelloWorld
」にもとづいて HelloWorld.class
というファイルができる。
さて、いよいよ実行しようと思って「java
HelloWorld.class
」としてみると、
Exception in threadとなってしまう。(ここでオチがわかってしまった人も多いと思いますが、 お付き合いくださいませ。)"main"
java.lang.NoClassDefFoundError:HelloWorld/class
まず、そういえば学内の某ニュースグループ(学外からは見えません)で以前に話題になっていたなと思い、問題のスレッドを探し出して読んでみる。
「CLASSPATH
の設定がおかしいのでは?
autoexec.bat
に記述があったら消してみては?
(そうするとデフォルトに戻るから)」というフォローがあるが、
私のマシンではそもそもそんな設定はしていないので、
デフォルトのままのはずである。
次に、「PATH
の設定がおかしいのか?」
「コンパイル時に何かオプションが必要なのか」
と疑って、色々やってみるが、これも効果なし。
それではということで、Java House メーリングリストの過去記事を調べてみる。
「スペシャルトピック(必読)」の中に「CLASSPATH
はこう設定せよ」というのがあって、NoClassDefFoundError
が出たらそれを読め、とあるのだが、難しい話をしていてよくわからない。
延々悩んだ挙げ句、ふと思いついた。正解は「java
HelloWorld
」である! さきのエラーは、
を探しにいっていたせいなのである。
08.21 で紹介した Platform SDK を、 大学の太い回線を使って丸ごとダウンロードする。 さすがにこれだけの数のファイルを一つ一つダウンロードするのは面倒なので、ファイル一覧の入っている HTML ファイルを(08.21 参照) を切り出して、それを WebAuto で丸ごとオートパイロットさせてやったのである。
さて、そうすると、全部で 400 MB 弱のファイルが出てきた。
大半は数 MB 以内という常識的なサイズだが、いくつか、
10MB 以上のものが混じっている。まあそれはいいとしても、中にひとつだけ、100MB というとんでもないサイズのものがある。
inet_d_common.1.cab
というファイル名で、
中身は単なるヘルプファイルであるが……おそるべしマイクロソフト。
x11r6bin.tar.Z
をしばらく前にダウンロード。
これを覗いてみたところ、なんとヘッダやライブラリ
(UNIX の libX11.so
よろしく X11.dll
や、
それを読み込むための libX11.a
よろしく
X11.lib
など)も入っていることがわかった。
これで、X アプリのコンパイルが
Cygwin なしでもできることがわかったわけである。
まあ、今すぐ何かしたいというわけではないのだけど。
mov AX, word ptr [1234H]
」と書いたら、
そのコンパイル結果が「mov AX, 1234H
」
となってしまうのである。これについて新しいことがわかったので報告する。
実は、これは「DS:[1234H]
」
と書くべきなのである。あるいは、セグメントを定義しておいて、
「_DATA:[1234H]
」としてもよい。ただし、
「_TEXT:[1234H]
」は、「assume DS:_TEXT
」
がない限りは「CS:[1234H]
」となるので注意。
ちなみに、この場合の DS:
はコンパイル時に無駄なバイトを生成しない。
cabezon.ini
というファイルを読み込むのであるが、
このフルパスが 31 文字以内になっていなければならないというバグである。
ソースを読んでみると、プログラムでは usesOne
(executePath + 'CABEZON.INI'
, false)
という呼び出しをしているのだが、この usesOne
() および、
その下請けの makeFileName
() が、それを、alpha
型(変数名などを保持するための型で、実体は string[31])
で受け取っていることが判明。これを fileNameType
(実体は
string[64])に変えてコンしパイルしなおしたところうまくいった。
☆
もうひとつ気になった点がある。LSI C-86 のプリプロセッサには、 ANSI 準拠度を高めた改造版が存在するのである。現在は C Magazine に LSI C-86 と一緒に収録されている。ちなみに、WWW 上にはないようである。
(余談)この改造版プリプロセッサを作った人と、Cabezon を作った人がふたりで Nifty のプログラム言語フォーラム(FPL) のシスオペをしていることを知ってビックリ。
以前、Windows 98 Driver Development Kit (98DDK) がフリーであると書いた。さらに、Platform SDK もフリーであることがわかった。これは Win32 API を使うためのヘッダ・ライブラリ・資料などが入っているもので、かつて Win32 SDK と言われていた内容はすべて含まれている。 Visual Basic Magazine の 99 年 3 月号の連載記事「MSDN の羅針盤 第 9 回」がこれについては詳しいので一読を勧める。 これは VB から Win32 API を使う話であるが。
ちなみに私は、
さらに、専用のインストーラーがネットワーク的にけっこう重いので、
そういう場合はファイルを直接たたくとよい。ネットニュースの
に流れたファイル一覧を参照して、
ほしいファイルだけを直接ダウンロードしてもよい。
http://download.microsoft.com
となるべきところ、
http://download/microsoft.com
となっている)
があるので、.eml 形式で保存して
HTML を切り出す必要あり。
前回の続報。MASM 6.14 はきちんと動いているようである。 LINK 5.63 も正常に動いているようである。
symdeb
互換のデバッガも Vector
(Windows::プログラミングの他に、
DOS::プログラミングもさがしてみるとよい)でさがしてみる。
前評判の高いのは exdeb
だが、バグがあるようである。わけのわからない
(どういうメカニズムで起こるか見当がつかない)
バグなので、やめておこう。
私が調べた限りでは、movsx
,movzx
のアセンブルにバグがある。なぜかmovsx ECX, CL
がmovsx EAX, CL
に化けるし、movsx EAX, AX
というコードは 0x66 prefix がつかないはずなのになぜかついている。そのほか、mov AX,[1234]
と書くとmov AX, ES:[1234]
に化ける。 (こちらは 1.85 では直っている)
ということで、ddeb
を入れることにする。
☆
さて、Vector で色々なツールをあさっていたら、Cabezon という Pascal コンパイラを見つけた。 8086 の機械語が生成できる本格派である。
必要なものは Cabezon コンパイラの他に MASM と 16 ビット版
LINK
である。MS から落としたものには masm.exe
がないので、別にOS/2 の
DDK にも masm.exe
が入っているという情報を元に取り出してみたらダメである。どうやら、masm.exe
が
DOS ファンクションを使って ml.exe
を起動しているが、
ml.exe
は Win32 コンソールアプリのため起動できない、
ということのようである。
ひとつの方策としては、Cabezon は LSI C-86 についてくる
r86
と lld
で代用できるので、
それを使うという方法がある。初心者に勧めるにはこれが簡単だろう。
マニュアルによると r86
では一部機能が使えないとのことらしいが、ソース(これが Cabezon
自身で書いてあるのだ!)を読んだ限りでは何も制限はなさそうだ。
あるいは、たぶん、DOS7 には専用のファンクションコールがあって、それを経由すれば大丈夫なはずであるし、
それがダメでも、command.com
経由で、つまり C 言語からなら
system("ml ...")
とすれば大丈夫なはずである。
というわけで、私家版 masm.exe
をあとで作っておこう。
(後日注)実は Cabezon は作者本人の WWW ページでも公開していることを発見。 つまり、今でもまだ「過去の作品」にはなっていないようである。
(後日注)Win32 アプリは int 21H AX=4B00H
で起動できることがわかった。
OS/2 の masm.exe
がうまくいかないのは
AX=4B01H
を使っているからかも知れない。
(後日注)リンカについては、lld
を使う場合には少々の制限があるから、Microsoft から
link.exe
をダウンロードしてくるのがよいと思われる。
また、私家版 masm.exe
ももう作ったので、
こちらへどうぞ。
というわけで、再コンパイルするためには、 Microsoft のアセンブラ「MASM」が必要である。 それに、Win32 アプリを作るためにも MASM はあったほうがいい。
以前は他人のところでコンパイルさせてもらっていて、 なぜか手元にある(^^;)のだが、できれば新しい版を仕入れておきたい。 486 のコードとか、MMX コードとかの問題があるので。
ということで例によって goo のお世話になる。まずは、MASM 互換という話の NASM を見てみるが、 どうも疑似命令やセグメント設定の点で問題がありそうだ。つづいて、 LASM というのも見つけたが、こいつは残念ながら定価 16800 円、 体験版は 100 行まで、ということで、買うのはちょっと…なぁ。
で、よくよく調べたら、MASM はありました。
comp.lang.asm.x86
で流れている FAQ を参考にすると、
見つかりました。詳細はこちらにまとめたのでどうぞ。
リンカもあります。でも、実は僕は某所で景品に
QuickBasic をもらったので DOS 用のリンカはあるし、LSI C-86 試食版についてくるリンカが MS 互換で、その上に
.com
ファイルの直接生成もできるのであんまり問題ないのですが。
ついでに、32 ビット版はこれまた同じところの景品でもらった
Visual C++ 4.0 のリンカがあるし。
たいていのプログラムでは、
標準入力と標準出力のどちらかだけを横取りすればよく、
そのための関数として、C 言語では popen() が用意されており、
Perl では open FILEHANDLE, "command |"
といった具合に実現できる。
しかし、 ときには標準入出力とエラー出力をすべて横取りしたい場合がある。 あるいは、C の popen() や Perl の open() ではコマンド名をシェルが解釈するので、 それをやめてほしいというケースもある。
たとえば、私が最近書いた例だと、
ある PostScript データを印刷すると何枚になるかを数えるため、
すでにメモリ上に持っているデータを
「psnup -1
」コマンドに標準入力として与え、
その標準エラー出力を読むということをやった。
この場合、Perl5 なら簡単、つぎのように書けばよい。
useIPC::Open3
;
open3(CHILDS_STDIN, CHILDS_STDOUT, CHILDS_STDERR,'psnup','-1'
);
print CHILDS_STDIN @postscript_file_contents;
close CHILDS_STDIN;
$pages = undef;
while (<CHILDS_STDERR>)
{
if (/Wrote (\d+) pages/
)
{
$pages = $1;
last;
}
}
wait; # 子プロセスが終了するのを持つ
close CHILDS_STDOUT;
close CHILDS_STDERR;
しかし、ここでは CHILDS_STDOUT は使っていないので、
わざわざ開くのはもったいない。そこであらかじめ、たとえば
DEV_NULL という名前で /dev/null
(Windows なら
/dev/nul
)を開いておいて、open3() の第二引数を
CHILDS_STDOUT とせずに ">&DEV_NULL"
とするとよい。
なお、子の STDOUT は親の STDOUT への垂れ流しでよい、
という場合は、第二引数を ">&STDOUT"
とできる
(STDERR についても同様)が、子の STDIN
には何もいらないというときに
open3(とするのは誤りである。こうしてしまうと、これ以降、STDIN からの入力はすべて子プロセスがとってしまい、親プロセス上でいくら <STDIN> と書いても、end of file を示す undef が返ってくるだけである。"<&STDIN"
,">&DEV_NULL"
,CHILDS_STDERR,'psnup'
,'-1'
,'xxx.ps'
);
☆
では、C 言語ならどうするか。あるいは、Perl でも
IPC::Open3
がないような古いバージョン(たとえば
Perl 4.0036)を使う羽目になったときはどうするか。
これは OS によって異なるので、とりあえず UNIX の場合。
C でも Perl でも基本的には同じなので、以下 Perl で書く。
まず使うのは、pipe() という関数。
pipe READHANDLE, WRITEHANDLE
これは、ふたつのファイルハンドルを新しく作るのであるが、 そのふたつのハンドルがつながっている。つまり、WRITEHANDLE に書き込んだ内容は何かのファイルに行くのではなく、 READHANDLE から読み出せるのである。
これだけでは能がないので、一緒に fork() を使う。 これはいわずと知れた、自分のコピーを作るコマンドである。
fork() で分身を作ってから、親が WRITEHANDLE にデータを書き込み、子がそれを READHANDLE から読みだすという形で使うのである。逆に、子が WRITEHANDLE に書き込むと、 親は READHANDLE から読み出すこともできる。
あとは、子の側で C なら dup() を、Perl なら open() を使って、子の STDIN や STDOUT などをつなぎかえてやり、 最後に exec() すればいいのである。
ということでまず、簡単な、「子の標準入力だけをパイプする」 というケースから取りあげる。ここでは、 子の標準出力・標準エラーは親と同じコンソールに流すことにする。 プログラムは次のようになる…が、実はこのままではきちんと動かない。
pipe READHANDLE, WRITEHANDLE;
select READHANDLE; $|=1;
select WRITEHANDLE; $|=1; # バッファリング関係で面倒を起こさないため
select STDOUT;
$childs_pid = fork();
if ($childs_pid==0) # 0 は子供の側で返ってくる
{
# ↓C なら close(0)→dup(READHANDLE) か dup2(READHANDLE, 0)
# ここで 0 は STDIN の意である。 fileno(stdin)==0
open STDIN,"<&READHANDLE"
;
exec"cat -n"
;
}
else # 親では子供のプロセス ID を受け取る
{
print WRITEHANDLE"hello, world.\n"
;
print WRITEHANDLE"this is the test of piping.\n"
;
close WRITEHANDLE;
wait;
}
どこがおかしいかお分かりであろうか。じつは、WRITEHANDLE
に書き込みができるのは親だけではないのである。子のほうでも
WRITEHANDLE に書き込むことはできるので、このプログラムでは、
cat
は「自分が書き込むのをひたすら待っている」
という状態になっているのである。したがって、exec() する前に
close WRITEHANDLE しておかなければならないのである。
わかったところで次は STDIN と STDOUT
(または STDERR)の両方を取り出す場合。つまり、上記の
psnup -1
を使うような場合。
この場合、2 本のパイプを
pipe PARENT2CHILD_R, PARENT2CHILD_W;として用意し、fork() してから、子の側では
pipe CHILD2PARENT_R, CHILD2PARENT_W;
close PARENT2CHILD_W;を、親の側では
open STDIN,"<&PARENT2CHILD_R"
;
open STDOUT,"> /dev/null"
;
open STDERR,">&CHILD2PARENT_W"
;
exec"psnup -1"
;
close CHILD2PARENT_W;をすればよろしい。
print PARENT2CHILD_W @postscript_file_contents;
close PARENT2CHILD_W;
while (<CHILD2PARENT_R>)
{
...
}