das Programmierungstagebuch (1999/08-2000/02)

ご注意

このページは、西暦 2000 年問題不対応(非対応にあらず)です。 2000 年の記事は 1999 年の記事の後ろにありますのでご注意下さい。


99.11.01 Java 事始め

大学の教養部(1、2 年生)の授業「計算機プログラミングI」で、 今学期から Java プログラミング(注)が始まった。

(注)昨年までは教官によって Pascal を使ったり C++ を使ったりしていたのだが、今年からは Java で統一された。

そのため、学内のニュースグループ(学内の人は、「lectures.g99.cp1-教官名-W-曜日-時限」 というニュースグループがそれですので、覗いてみてください) に提出課題が山のように投稿されるようになった。

私は傍観者としてほぼ全部のグループに目を通しているのであるが、 たまに自分でフォローを入れることがある。そのためには自前で Java 処理系を用意しなければならない。

実はすでに JDK(Java Development Kit)1.2.1 をダウンロードしてある (家で friends2 を動かすために JRE(Java Runtime Environment)が必要だったので、 大学の太い線を使って、その superset たる JDK を落としたのである) のだが、実は今までは自分でコードを書いたことがなかったので、 この機会に色々やってみることにする。

まずはいちばん簡単なコードから。

class HelloWorld
{
   public static void main( 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」である! さきのエラーは、 .\HelloWorld\class.class を探しにいっていたせいなのである。

99.10.15 BDF フォントエディタと Perl/Tk の話

99.10.14 libX11 はそもそも Windows 対応だった

99.08.24 で紹介した libX11 は、実は最初から Win32 対応にできている、 ということがわかった。XFree86 のサイトに落ちているものを拾ってきて、 そのまま NT+VC でコンパイルできるのである。 手元の Windows 98 では cmd.exe がないのでうまく動かないのだが…

99.10.13 ひとつで 100MB のファイル

08.21 で紹介した Platform SDK を、 大学の太い回線を使って丸ごとダウンロードする。 さすがにこれだけの数のファイルを一つ一つダウンロードするのは面倒なので、ファイル一覧の入っている HTML ファイルを(08.21 参照) を切り出して、それを WebAuto で丸ごとオートパイロットさせてやったのである。

さて、そうすると、全部で 400 MB 弱のファイルが出てきた。 大半は数 MB 以内という常識的なサイズだが、いくつか、 10MB 以上のものが混じっている。まあそれはいいとしても、中にひとつだけ、100MB というとんでもないサイズのものがある。 inet_d_common.1.cab というファイル名で、 中身は単なるヘルプファイルであるが……おそるべしマイクロソフト。

99.09.13 (2) MS C もフリー?

99.08.18 にふれた OS/2 の DDK には 16 ビット環境用の Microsoft C が入っている、という情報を入手した。 あとで調べておこう。

99.08.24 (2) libX11 for Win32

某所で、Win32 ネイティブ(Cygwin ではなく)用の X 関係ツールのバイナリが入っている x11r6bin.tar.Z をしばらく前にダウンロード。 これを覗いてみたところ、なんとヘッダやライブラリ (UNIX の libX11.so よろしく X11.dll や、 それを読み込むための libX11.a よろしく X11.lib など)も入っていることがわかった。 これで、X アプリのコンパイルが Cygwin なしでもできることがわかったわけである。

まあ、今すぐ何かしたいというわけではないのだけど。

99.08.24 (1) MASM の [addr] の件

MASM で試していて疑問に思ったことがあった。それは、たとえば 「mov AX, word ptr [1234H]」と書いたら、 そのコンパイル結果が「mov AX, 1234H」 となってしまうのである。これについて新しいことがわかったので報告する。

実は、これは「DS:[1234H]」 と書くべきなのである。あるいは、セグメントを定義しておいて、 「_DATA:[1234H]」としてもよい。ただし、 「_TEXT:[1234H]」は、「assume DS:_TEXT」 がない限りは「CS:[1234H]」となるので注意。

ちなみに、この場合の DS: はコンパイル時に無駄なバイトを生成しない。

99.08.22 Cabezon のバグ、LSI C-86 の cpp の問題

Cabezon を動かしていたら、困ったバグを見つけた。 このコンパイラはコンパイル時に 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) のシスオペをしていることを知ってビックリ。

99.08.21 Win32 SDK もフリー

以前、Windows 98 Driver Development Kit (98DDK) がフリーであると書いた。さらに、Platform SDK もフリーであることがわかった。これは Win32 API を使うためのヘッダ・ライブラリ・資料などが入っているもので、かつて Win32 SDK と言われていた内容はすべて含まれている。 Visual Basic Magazine の 99 年 3 月号の連載記事「MSDN の羅針盤 第 9 回」がこれについては詳しいので一読を勧める。 これは VB から Win32 API を使う話であるが。

ちなみに私は、

という人間なので、今のところダウンロードしていない。 結構大きそうなので、必要になったときは、VC6(というより VS6) を持っている友人からコピーさせてもらおう。

さらに、専用のインストーラーがネットワーク的にけっこう重いので、 そういう場合はファイルを直接たたくとよい。ネットニュースの microsoft.public.platformsdk.sdk_install に流れたファイル一覧を参照して、 ほしいファイルだけを直接ダウンロードしてもよい。

99.08.18 (1) MASM 入手(続報)と Cabezon

前回の続報。MASM 6.14 はきちんと動いているようである。 LINK 5.63 も正常に動いているようである。

symdeb 互換のデバッガも Vector (Windows::プログラミングの他に、 DOS::プログラミングもさがしてみるとよい)でさがしてみる。

前評判の高いのは exdeb だが、バグがあるようである。わけのわからない (どういうメカニズムで起こるか見当がつかない) バグなので、やめておこう。

私が調べた限りでは、movsx, movzx のアセンブルにバグがある。なぜか movsx ECX, CLmovsx 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 についてくる r86lld で代用できるので、 それを使うという方法がある。初心者に勧めるにはこれが簡単だろう。 マニュアルによると 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 ももう作ったので、 こちらへどうぞ。

99.08.15 MASM 入手

最近、古いマシン(FMTOWNS II HR100) のバックアップをあさっていたら、 しばらく前にアセンブラで書いたソースが出てきた。

というわけで、再コンパイルするためには、 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 のリンカがあるし。


00.02.07 パイプの作り方

UNIX のお家芸のひとつに「リダイレクト・パイプ」がある。 子プロセスを立ち上げ、その標準入出力を親が横取りするというアレである。

たいていのプログラムでは、 標準入力と標準出力のどちらかだけを横取りすればよく、 そのための関数として、C 言語では popen() が用意されており、 Perl では open FILEHANDLE, "command |" といった具合に実現できる。

しかし、 ときには標準入出力とエラー出力をすべて横取りしたい場合がある。 あるいは、C の popen() や Perl の open() ではコマンド名をシェルが解釈するので、 それをやめてほしいというケースもある。

たとえば、私が最近書いた例だと、 ある PostScript データを印刷すると何枚になるかを数えるため、 すでにメモリ上に持っているデータを 「psnup -1」コマンドに標準入力として与え、 その標準エラー出力を読むということをやった。 この場合、Perl5 なら簡単、つぎのように書けばよい。

use IPC::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", ">&DEV_NULL",CHILDS_STDERR, 'psnup', '-1', 'xxx.ps');
とするのは誤りである。こうしてしまうと、これ以降、STDIN からの入力はすべて子プロセスがとってしまい、親プロセス上でいくら <STDIN> と書いても、end of file を示す undef が返ってくるだけである。

では、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() を使って、子の STDINSTDOUT などをつなぎかえてやり、 最後に 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 しておかなければならないのである。

わかったところで次は STDINSTDOUT (または STDERR)の両方を取り出す場合。つまり、上記の psnup -1 を使うような場合。

この場合、2 本のパイプを

pipe PARENT2CHILD_R, PARENT2CHILD_W;
pipe CHILD2PARENT_R, CHILD2PARENT_W;
として用意し、fork() してから、子の側では
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>)
{
  ...
}
をすればよろしい。

Copyright © IIJIMA Hiromitsu aka Delmonta, 2016/03/10 15:09 JST
ここは、椅子人の私文書を保管しているものです。
企画倒れの文書、古いデータ、リンク切れ、 本人にしか通じないネタなども野ざらしにしてありますので、 ご了承ください。

椅子人文庫トップ | 電脳外道学会