第90回 mirror classについて1(冴子先生をナンパする男) 99/12/16
会社にOffice2000が導入されたんですよ。いらんことしーで有名な私の先輩は、さっそくOfficeアシスタントをイルカのカイル君から冴子先生に変更。後ろからこっそり眺めていると、「わからないことがあったらお手伝いします」という冴子先生に早速、何か質問しているではないですか。何入れてるのかなーと思ったら「服脱げ」と入力しているではないですか。ちょ、ちょっと〜。さらに「質問の意味がわかりません」と答える冴子先生に「とぼけんな!」とか入力しているのを見て、私は呆然。それはなんか違うと思うんですけど…(笑)
さて。本題。
たいていのCPUには、たいていnバイト境界(n=1,2,4,8,16,32,...)ちゅー問題があって、その境界をまたぐアクセスをすると非常に遅くなったりします。これは昔のCPUアーキテクチャにも存在していたので、C言語の仕様でも
class X {
int a;
char b;
};
というような構造体があったとき、
sizeof(X) ≠ sizeof(int) + sizeof(char)
であるかも知れないとされています。
ところで、参照されていない関数は、いまどきのコンパイラならば最適化によってその関数実体が生成されるコードから消されますが(おそらくリンク段階の最適化)、メンバ変数についてこの限りではありません。C言語の規約上、どのようになっているのか詳しくは知らないのですが、メンバ変数は参照していないからと言って、そのメンバ変数が最適化によって消され、構造体のサイズが小さくなるわけではなかったように思います。
さて。このことを踏まえて、あるclass(のメンバ変数とメンバ関数)を、LoadLibraryで動的に読み込まれたDLL側から、アクセスすることについて考えてみましょう。(ちゅーか、yaneGameScript2000のほうで、そういう必要が出てきたのですが…)
解決の一手段は、すべてActiveXでインターフェースを公開しておくことです。それはまあ、悪くはないように思います。個人的にはレジストリに登録するってのが少し気持ち悪いのと、インターフェースを記述する部分が少し面倒だと感じますが(勉強するのが面倒というのもある…)決して悪くはありません。
今回は、もっと原始的な方法でアクセスすることを考えてみましょう。
まず、メンバ変数の格納位置(オフセット)は、(同一のコンパイラならば)同じと仮定してよさそうです。つまりは、
class A {
int n;
int m;
};
class B { // This is a mirror class of A
int n;
int m;
};
A*を渡して、B*にキャストしても、メンバ変数n,mにはうまくアクセスできるだろうと。
次にstaticなメンバ変数についてはどうでしょうか?
class A {
static int n;
};
staticなメンバ変数(やメンバ関数)はいくらあっても、構造体自体のサイズsizeof(A)は変化しません。(ここからstaticメンバ変数とメンバ関数との類似性を見てとれるのですが、これについては後述します)
ということは、A::nにアクセスするためには、別途A::nのアドレスを渡してやる必要があります。
class B { // This is a mirror class of A
static int *n;
};
として、
B::n = &(A::n);
とするのが、一手段ですが、これではintではなくint*になってしまい、同一のインターフェースとは言えません。(それはそれで構わない気もしますが)
前回説明したように参照変数が、初期化子を要求してこないならば
class B { // This is a mirror class of A
static int &n;
};
として、あとから
&(B::n) = &A::n;
とアドレスを渡してやることも出来るのですが…これが出来ないので、現実的な解決策としては、propertyキーワード(→第84回参照)を使うぐらいでしょうか…。
そうそう。忘れていましたが、前回の記事に関して、さ〜さん御自身から訂正メールが来ていました。同様の内容について何人かの方からご指摘いただきました。一番、詳しく説明してある、吉田さんのメールを紹介して終わります。(吉田さん、ありがとう!)
こんにちは、やねうらおさん。吉田といいます。 いつも楽しく「スーパープログラマーへの道」を読ませて頂いています。 for の変数スコープの話ですが、他の方からもメールがあると思いますが、 C++ の規格では、CodeWarrior の処理が正しいです。 で、VC++ が悪いかといえば、一概にそうではなく、この規格自体が新しい もので、すでに VC++ は世に出ていたわけです。そういうわけで、VC++ はコンパイルオプションで ANSI 準拠の for の変数スコープ と設定する ことができますが、MFCなんてどでかいソースを書き直すのも、ユーザ のアプリのソースを書き直すのも、たいへんなので誰もこのオプションを 設定しないし、デフォルトではオフになっています。私も、どのオプション だったか忘れました。 互換性を気にするのであれば、for で使用する変数を for の前で宣言すれ ばいいわけです。 せっかくメール出すので、ついでに... 私が、VC++ で困ったのが template< class T1, class T2> void hoge( T1 n1 ) { T2 n2; } みたいな、テンプレート関数が作れないことでした。 どうも必ず関数の引数に T1, T2 を渡してあげなければいけないらしい。 C++ の規格では OK のはずなのに... |
第91回 冴子先生に惚れた男(mirror classについて2) 99/12/18
会社の先輩は、あいかわらず、冴子先生に「デートしようよ?」とか質問しているのですが(笑)、先輩の愛が冴子先生のハートに届く日は来るのでしょうか…。
冴子先生に限らず、WordやExcelを立ち上げて、数分間、放置しておくと何パターンかのアニメーションを見ることが出来るのですが、たまたまWordを立ち上げたまま席を外していた先輩。スクリーンセーバーがいつものように起動していたのですが、さっそく使おうとマウスを触ると、な、なんと冴子先生はお化粧を直しているではないですか!驚いた先輩は、さっそく「そんなことをしなくても君は綺麗だよ」と入力。「質問の意味がわかりません」と答える冴子先生に「今晩、どう?」と返す。果たして先輩の妄想はどこまで突き進むんでしょうか…(笑)
さて。前回の続きで、今回は、mirror classにおける関数の処理について考えてみましょう。つまり
class A {
void test(void);
};
に対してAと同じインターフェースを持つBの
class B {
void test(void);
};
B::testを実行時に用意したいのですが、あいにくC言語では関数はconst的(?)であって、実行時に
B::test = A::test;
というように変更することは出来ません。実行時に変更するものは、変数に入れなければならないわけです。要は、関数ポインタを用意すれば良いのですが、メンバ関数のポインタは、通常の形とは少し赴きが異なります。
class B {
static void (B::*_test)(void); // This is a pointer of
function"void test(void)".
};
void (B::*B::_test)(void); // (file scopeで、このように宣言する必要あり)
これで、メンバ関数ポインタは確保できました。staticにしているのは、メンバ関数B::testはclass Bに対して一意に定まる(?)ものだからです。(前回指摘した、static変数とメンバ関数の類似性は、これです。メンバ関数≒staticメンバ関数ポインタなのです) そして、
B::_test = (void (B::*)(void))A::test;
とキャストして、メンバ関数ポインタにジャンプ先を入れることが出来ます。では、この呼び出しは、どうすれば良いのでしょうか?
B b;
// B型の変数bに対して
(b.*_test)(); // test()を呼び出す
のように.*演算子(メンバポインタ演算子)を用いれば呼び出せます。これで呼び出すと、呼び出し時の第一引数にthisポインタが自動的に付加されるのだと。また、メンバ変数は、thisポインタから相対的に参照されるので、構造体メンバのオフセットさえ変わらなければ(前回行なった仮定)うまく動作するわけです。(詳しくは次回)
実際のところ、class Bには、
void test(void) { (this->*_test)(); }
このような関数を用意して、インターフェースをclass Aと統一したほうが良いでしょう。これで、mirror classを用意する準備が出来ました。あとは、DLL側へ関数ポインタさえ受け渡ししてやることが出来れば、これでDLL側からyaneSDKの関数にアクセスするようなことが出来るようになりそうですが…。(つづく)
第92回 ATCでそうなんです!(mirror classについて3) 99/12/25
この時期、忘年会ラッシュで困ってます。いまも忘年会から帰ってきたばかりなので、べろんべろんに酔っ払っています。よく酔っ払っているときにやったらと面白いギャグを思いつくことがあるのですが、酔いが覚めたあとはすべて忘れています。一度、こういう形で書き残して、あとで見たらどうなっているのかたのしいいいいいいいいいいいいはしとちはしとち(しまった。いま、一瞬、寝ちまったよ。いかんいかん)
おまけに、やねうらおは、電車・バス・車のような動きものが大の苦手なのです。ATCってどこよ?南港だよと言われても、その南港っちゅーのがわからんのよ!今日も、速攻、迷子なったっちゅーに!!
「ATC(あたし)はどこ?ここは誰?」 そんなわけで置いてきぼりを食らったやねうらおは、既にパニック状態ですよ。道ゆく人に「あたしはどこ?」なんて言おうものなら、すぐに黄色い救急車で運ばれてちまいますですよ、はい。忘年会、もういかんといたろか?と天使ちゃんの囁きが脳裏くんをよぎった。まあ、もうすぐやめる会社やしな…。
それでも、この会社のしとたちとも、もうすぐお別れかと思うとなんだか寂しいのよー。今年入ってきた後輩の木下君なんかやね、Office2000でアシスタントをミミー(猫)にして、「こいつ、可愛いっす」とか言ってたかと思うと次の日に「こいつ、時々、難しいこと言うてきよるんですよ。セルをドラッグって何ですの?セルゆうたら細胞やないですか。ドラッグゆうたら…麻薬!?僕に麻薬しろゆうことですか?マジっすか。こいつ、そういう自分だけしかわからん難しい言葉使こてきよるとことか、猫のクセにめっちゃ失礼やと思うんですけど」とか何とか。何が失礼なんじゃこら!おもろいやないか、木下君。あー、しかし、このクレージードラッグ野郎ともお別れかと思うと、おじさん寂しいあるよ。(誰がクレージードラッグ野郎じゃ…)
あー。それにしても、道わからん。どこ行ったらええんやろ。そもそも、完璧主義のやねうらおは、自分の知らないことがひとつでもあると、そこから一歩も先には進めない性格なのですよ。ひとつでもわからないことがあったら、どこでもすぐに遭難できるの!「そうなんです。がはははは」と黄色い看板プロミスもびっくりして逃げ出すようなギャグを連発してやったわ。(してやったって、誰にやねん>俺)
そういや、コートのポッケに地図のコピーが入っていたような気がした。コートに手を突っ込むと、去年のアメちゃんが出てきた。アメちゃん!?アメちゃんも一年たったら、アメくんになってるよなぁ。ちゅーか、このまま食べずに放置しておけば、アメくんもどんどん歳とって、知らん間に、アメさんになっているのではないだろうか。アメさんが若いころに出入りしていた雀荘にふと顔を出すと昔、よく一緒に打ってたおっさんと卓を囲むことになって「アメくんも立派になったもんだ…いや、もうアメくんじゃなくて、アメさんか…」とか言って四枚目の西を捨てたときに、アメさんが「それロンです」と国士無双。「いけね。おじさん、しゃべりすぎちまったようだ」
お客さん終点です
あっ。もう着いたの?電車んなかで眠ってました。ちゅーか、ここどこよ?なんか終点まで行っちゃったみたいなんですけど。おまけに終電で。(笑) これがカップルだと「あたしたちもう、引き返せない!」ってなギャグのひとつも言えておいしかったんですけど。(おいしいとか、おいしないとかどうでもええんちゃうの>俺) そんなこんなで、家に帰れない日々が続いているので、ホームページの更新どころではないのでありまするよ。
さて。本題。
やっと、class Aと同じインターフェースを持つclassBは用意できました。できたんだよ。酔っ払ってよく覚えてないけど、確か、そんな気が。(笑)
非staticなメンバ変数の構成は同じだから、sizeof(A)とsizeof(B)は同じで、これでめでたしめでたしなのだけど、今後の拡張のことを考えるとメンバ変数には直接アクセスしないほうが良いような気がする。ついでに言えば、今後の拡張次第ではメンバ変数の構成は変わりうると。そうなったとき、sizeof(A)≠sizeof(B)となって、何が困るかというと
B b[10];
ってな感じで配列としてアクセスしようとするとき、b[0]からb[n]までのオフセット値は、n * sizeof (B)≠n * sizeof(A)=a[0]からa[n]までのオフセット値なわけで、まいっちんぐなわけですよ。
仕方ないので、
B* lb[10];
for (int i=0;i<10;i++) lb[i] = (B*)(&a[i]);
ってな感じで必ずそれぞれのインスタンスへのポインタをもらって、それ間接でアクセスすることにしようと。これで困るのは、すべてlb[10]->func();というように、->でアクセスする必要が出てくること。アクセス手段が変わってしまっては、何のためのmirror classかわからない。
#define b[n] (*lb[n])
ってな感じでごまかす手はありそうだが…。
第93回 忙しいときに忙しくする奴(mirror class完結編) 99/12/30
プロのゲーム制作には、ゲーム制作特有の修羅場があります。たとえば、原画マン。納期まぎわで、みんなが忙しくしているのに、既に自分の役割は終わって鼻歌交じりにゲームなんぞして遊んでいたりします。それでもやはり周囲の視線が気になるので(まわりには、ここ2、3日一睡もしていない奴とかがごろごろいる)、心優しい原画マンは、何かお手伝いをしようと考えました。
そうは言っても、プログラムを手伝えるはずもないし、シナリオを手伝えるわけでもない。着色も、いまから参加したのでは、全体的な統一感が無くなってしまいます。
そこで、原画マンは、自分も次のゲームに向けて、新しいキャラを描いてみようとか思ったりするのですよ。それでも、ここ数ヶ月、ずっとそのゲームのキャラばかり描いてきたので、さらっと描いたキャラが、そのゲームのキャラにどことなく似ていても無理はありません。というか、そのゲームのキャラそのものだったんですよ。
原画マンは上機嫌で、「新しい原画描いたから使ってよ」と言ったのですが、まわりを見渡すと、そこには殺意がみなぎっていました。「お前、この忙しいときに仕事増やしてどないすんねーん!」「そんなもんシナリオもプログラムも着色も作業増えるやんけー!」「何かんがえてんねん、このどあほー」 周囲から、袋叩きにされた原画マン。果たして彼は立ち直れるのでしょうか…。(笑)
さて本題。
MirrorClassで最後に問題となってくるのは、インスタンスの生成部分です。たとえばnewでclass Xのインスタンスを生成する過程で重要なのは、次のステップです。
1.sizeof(X)だけヒープから確保する。
2.class Xのコンストラクタを呼び出す。
このうち、1.で、sizeof(X)を参照しているわけですが、MirrorClassにとってはX(元のclass)のサイズが変わったときのために、これを隠匿する必要がありそうです。具体的には、
X* X::MakeInstance(void) { return new X; }
というような、自分自身をnewする関数を元のclassに追加して、こいつをMirrorClassから明示的に呼び出します。
本当は、MirrorClassのコンストラクタで、これをうまく呼び出すことが出来れば良いのですが、コンストラクタが呼び出されたときには、既に手遅れです。MirrorClassのnew operatorを定義してやり、そこからX::MakeInstanceを間接的に呼び出す手はあります。
しかし、MirrorClass::new operatorを定義したからと言って、
MirrorClass m;
としたときに、MirrorClass::new operatorが呼び出されるわけではありません。本質的にインスタンスの生成部分をカスタマイズすることは不可能です。(だと思う)
第94回 プログラミングの道は一日にしてならずぢゃ(COMインターフェース) '00/01/10
いま、やねうらおは、引越しの準備をしています。マンガ本も一掃する予定なんで、捨てる前にもう一度読み返そうと思い、読んでいたんですよ。
YAWARAの1〜27巻(28,29は持っていない)を読み、もう感動して涙が止まらなくなって、浦沢直樹あんた天才だよと言うやいなや、持っていなかった28,29巻を買いに本屋に走りましたよ。すると、そこの本屋には、氏のHAPPYも全23巻そろっているではないですか…。気づいたときには、2冊+23冊の25冊買ってましたよ。あっ…。本、減らさんとあかんちゅーに、増やしてどないすんねん!
しかも25冊も一気に!!
この調子だと、今年も、やねうらおは馬鹿街道まっしぐらなのでありましょうか…。
さて、yaneuraoGameScriptSDKのほう、公開しました。ygs2KのDLL側から直接、描画関数等にアクセスするための手段を提供します。本来ならばインターフェースは、COMインターフェースで公開すべきところですが、COMインターフェースも、少し面倒なところがあるので、今回はMirrorClassを応用してこの手段を提供してみました。興味のある方は、DownloadコーナーからDownloadしてご覧になってください。
今回は、これに関連して、構造体のサイズについて考えていきましょう。特にことわらない限り実装系はVC++/Win32環境とします。
class X {
int n;
};
sizeof(X)は4。Win32環境においてintは32ビット長だからです。
class X {
int *n;
};
sizeof(X)は 4。Win32環境においてポインタは32ビット長だからです。
では、
class X {
// empty
};
sizeof(X)は ?
どうも、1のようです。メンバが何もなくても0にはならないようで…。
class X {
void func(void);
};
sizeof(X)は 1のようです。メンバ関数はいくら増えようともsizeof(X)に影響を及ぼさない。
class X {
virtual void func(void);
};
では、この場合、sizeof(X)は?
C++に詳しい人ならばご存知でしょう。これは4です。仮想関数テーブルへのポインタをメンバとして保有するからですね。
class X {
virtual void func1(void);
virtual void func2(void);
};
それでは、この場合、sizeof(X)は?
これも、もちろん4です。仮想関数テーブルへのポインタを保有するが、そこにfunc1,func2が表のように登録されていくだけだからで、、virtual関数がこのあといくら増えようともsizeof(X)は増えないのです。
class dummy {
};
class X {
void (dummy::*pFunc)(void); // dummyクラスのメンバ関数へのポインタ
};
このとき、sizeof(X)は4。Win32環境では関数ポインタも32ビット長です。
では、ここで問題。このclass dummyを
class dummybase {
virtual void test(void) = 0;
};
class dummy : public dummybase {
virtual void test(void);
};
とやっている場合sizeof(X)はいくらになるのでしょうか?不思議なことにsizeof(X)は4ではありません。8です。
というか、このときsizeof(void(dummy::*)(void)) (dummy
classのメンバ関数ポインタの大きさ)が8なのです。
ついでに、単一継承・多重継承・仮想継承それぞれについてメンバ関数ポインタの構造は異なったはずです。
さらに言えば、クラス定義を後方参照する(宣言だけされて定義されていないクラスを参照するの意味。このようなクラスをC++の用語では前方参照クラスまたは不完全クラスと言う )場合も、クラスが上のどのタイプであるかその段階では確定しないので、メンバ関数ポインタは8バイトだったと思います。
MirrorClassを作っていて、この後方参照を行なっている部分があって、そこだけ8バイトでコンパイルされてしまい、受け渡し側は4バイトで行なっているため、構造体レイアウトが狂うことになりました。COM
interface的にvirtual関数で関数テーブルを作成し、それを受け渡すのもひとつの手段ですが、今度はそれを呼び出すためのcastが非常に大変です…。
悩んだ結果、
#pragma pointers_to_members(full_generality,multiple_inheritance)
で、強制的にメンバ関数ポインタを8バイトにすることによって、レイアウトを調整しました。(VC++限定)
うーん。こんなんでいいんでしょうか。なんか苦し紛れな印象を受けなくもないです。ご意見、ご感想をお待ちしております。
第95回 大学の課題(真由子現る!) '00/01/13
今日も、ご機嫌さんでメールチェックをしていると、こんなメールがまぎれているではありませんか!
はじめまして 真由子です。 大学の課題でプログラムがでてるんですけど、 ぜんぜんわからなくて困ってます。 助けてください。 明日の3時までなんです。 C言語使ってます。 余りを求める関数remainder(int X,int Y)と、文字パリティを調べる関数 問題2 商と余りを同時に求める関数divを書きなさい divの仕様は次のようにしなさい。 div(int x, int y, int *r) |
なんで、こんな大学の課題を、見ず知らずの私にメールしてくるんでしょうか。ひょっとして、私のページは「役に立つ、実用ホームページ888」とかに掲載されているんでしょうか。(それは絶対ないって>俺)
私、こういう問題に突っ込みを入れるのはめっちゃ好きなんですけど、それが災いしてか、大学のときのプログラムの試験はほとんど顔パスで、ろくに試験を受けさせてもらったことがないんですよ。
そんなわけで、こういう問題見るの初めてなんですけど…。なんや、これ、わからんがな!ちゅーか、C言語使ってんのに、アセンブラで書きなさいとは、どういうことやねん!いきなりトンチ問答か(笑) アセンブラやけど、構造化マクロ使って関数らしく書けっちゅーことか?それともインラインアセンブラで書けっちゅーことか?しかし、この真由子ちゃんに淫乱汗んブラもとい、インラインアセンブラと言っても、なんなんよーってなるに決まっとるし…。
おまけに、文字パリティって何やねん。通信のときに送るパリティビットのことなんかな?intで返すところを見ると、ビットが1になっている数えるような関数(あるいは、その合計の偶奇を0か1で返す)なんか?それをアセンブラで?それって、なんだか泥くさいよー。見るからに、通信専用の端末(ワンボードマイコン)か何かのハードウェア実習の傍ら、ソフトウェアの講義で、それに付随するプログラムをやらされているような気がするんだけど、そのアセンブラってターゲットとなるCPUはなんやねん!きっと、真由子ちゃんは、C言語みたくアセンブラもひとつしか無いと思ってるんだろうけど…。
そんなわけで、とりあえずCで書くならば、
int remainder(int X,int Y)
{ return X%Y; }
int parity(char c) { char x=1; int p=0; for(int i=0;i<8;i++,x<<=1)
if(x&c)p++; return p & 1; }
でいいんでないかなー。assembleは自分でやってちょ。
そして、問題2は、商は、*rに返すとして、余りをどこに返せばいいんでしょうか?関数divの返し値が余りなんでしょうか?だとしたら、もの凄いコーディングセンスですよ!こんな人の下では決して働きたくありません。というか、こんな奴が部下だとしても、そんな会社にはいたかーないです。
int div(int x,int y,int *r) { *r = x/y; return x%y; }
ひょっとして、こんな関数なんでしょうか?しかし、「商と余りを同時に」って書いてあるし...!あっ。わかった!
同時やな。これがミソや。これは、DualCPUを使って、商と余りを同一クロックパルスで求めるんや。あるいは並列コンピュータの話なんやな。だから値は返さんでええんや。なるほど。大学の問題って、難しいなぁ。(そんなわけあるかいや…)
第96回 C言語の勉強会に行く(自称素人大作戦) '00/01/16
C言語の勉強会を徹夜でやってました。
というのも、ゲーセンで知り合った友達と話しているうちに、パソコンの話になって「僕、いま、ゲーム会社でプログラム書いてる人にC言語教えてもらってるんですよ」とか言うんで、「あっ、おもしろそう。C言語ってよう知らんけど、興味あんねん。よかったら僕もまぜて」とか言って、一緒にその人の講義を受けることにしたんですよ。名づけて、自称素人大作戦!(笑) この光景を見る人が見たら、目玉と鼻水が飛び出てたかも知れないです。
とは言っても、やねうらおの自称素人病は、いまに始まったことではなく、JavaもPerlも使えるくせに、ホームページはFrontPageで作成し、「あんた、素人とちゃうねんからHomePageBuilderかなんか使いなはれ」という周囲の猛攻を「ぼくちん、ぱちょこんよくわかんないんで」とかわし続けています。(かわせてないって…)
まあそんなこんなで、素人のふりして、C言語の講義を聴いていたわけです。とは言っても、私もまともなC言語の入門書なんて読んだこともないし、そこらへんは謙虚になろうと思ったんですよ。それに、その講師の人、なかなかいいこと言うんですよ。
ときに、そのとき話題にあがったのがローカル関数で、関数のなかになぜ関数が書けないのかということでした。
確かPascalには、それに近い構文がありますし、関数のなかに関数実体が宣言できないがゆえに、class内で、ある関数からしか呼び出していないような関数でもprivateなメンバ関数として宣言する必要があって、その関数は、classの他の関数から“見える”わけで、よーするにカプセル化、出来ないではないか…ということです。(大規模なclassを設計していると、それで困ることがあります)
そう言われてみれば、そのclass内部でしか使わない構造体をclass内部で宣言する場合がありますし、ある関数内でしか使わないような構造体を、その関数内で宣言してしまうこともあります。カプセル化するためには、スコープは可能な限り短くあるべきです。
もう、ひとつ。スクリプトでマシンコードにコンパイルして実行するような環境を作ろうかーとかいう話があがっていました。つまりはJavaみたいな感じなのですが、仮想マシンコードなんてうざったい命令セットを考えるぐらいならば、直接ネイティブコードを吐いて、それをDLLとしてファイルに書き出し、それを読み込むことによって実行すればどうかということです。スクリプト言語仕様のほうは、Javaのサブセットでも良いでしょう。なかなか面白いアイデアです。
そんな話をあーでもない、こーでもないと延々朝まで…。うーん。学生の勉強会はなかなかハードです。おっちゃんには肉体的にも精神的にもかなり辛いものがあります。おっちゃんとしては、2時間ぐらいの講義で紅茶とお菓子が出てきて、かわいこちゃん(死語)が講師のプログラミングの勉強会がいいのですが。(笑)
第97回 ネイティブコンパイラ作成への短い道のり(短いんか?) '00/01/17
送別会ラッシュです。会社の人、友人、それから先輩と、いろんな人がお酒を飲みに連れてってくれます。それは良しとして、あまりお酒は飲めないんですよ。いや、飲むには飲むんですが、飲むとわけのわからんギャグを言って、笑いがとまらなくなったりして、そのままのたうちまわるんです。
「やねさんは、なんで携帯を持たへんのですか。携帯便利ですよ」とか言う話になって、「そら、携帯持ってたら、電話かかってくるからやん」と平然と答え、さらに「携帯は相手が持ってたら便利やけど、自分が持ってたら、かかってくるだけやん」と言って、周囲から猛烈に反感を買い、「それやったら、隠し持ってたらええやないですか」という後輩に、「そうはゆうても、だんれぼしてて落としたらどないすんねん。落とした瞬間、あっ。お前、携帯もっとんたんかーってなるやろ」と切り返す。しかし後輩も負けてはいない「それなら、かばんのなかに隠しといたらいいやないですか」
「ちょっと待て。自分、ニュース見てへんな。昨年度の警察への通報の4割は移動端末からの通報なんやで。事故起こったときほど、携帯が便利なことはないんや。実際、俺は、5,6年ぐらい前に、携帯を持ってたんや。あんときは携帯電話が高かったから、ビジネスマンと、イチビリの学生しか持ってへんかったころや。イチビリや思われるんいややったから、俺は携帯なんてひたかくしに隠しとったし、かばんのなかに入れて持ち歩いとった。しかし、友達と歩いてたら、目の前にいた子供が車と接触事故したんや。そしたら、子供、にわかに泣き叫んだんよ!痛いよーおかーちゃーん。でも、母親としても気が動転してもうてどうしようもできへんのや。嗚呼。マサヒロが。マサヒロー。誰か救急車を。救急車をー!ってなったんや。そしたら、俺かて鬼やない。赤い血が流れとる。その必死の姿を見て思わず、ひた隠しに隠していた携帯を、ついにかばんから出して救急車を呼んであげたわけや。ほんまやったら、俺、めちゃめちゃええ奴やないの。表彰されてもええぐらいやないの。ところが、このときだけは鬼の首でもとったかのように、そこにいた俺の友達全員が、あっ。お前、携帯もっとんたんかーって言うて、なんか俺、悪者扱いなわけや。救急車来るまで、なんで隠しとってんとか、いつから持っとってんとか、まるで犯罪者扱いや。なんでやねんと言いたい!子供の命を救うために、自分が不利な立場に追い込まれることも省みず、救急車呼んだったんやないのー。こんな馬鹿らしいこと知らんわ!そう思って、その日以来、携帯は捨てたんや。そやから、子供が車にひかれ、その母親がいかに泣け叫んでいようとも、もう俺には救急車を呼んでやるための携帯が無いっちゅーわけや」 「その話、マジっすかー」 「うそよね〜ん。くくくくくくくくくくくくくく」(このあと、笑いが止まらなくなって、テーブルの下でもだえる、やねうらお)
さて本題。
前回、スクリプトをコンパイルして、それをDLLとしてファイルに書き出して、それを読み込み実行するという風に書きましたが、実際には、そんな馬鹿なことはする必要ありません。Win32APIにはVirtualProtectというのがあるからです。
たとえば、
BYTE x[] = {
0xB8,0x7B,0,0,0, // mov eax,123
0xC3 // ret
};
int y;
y = (*((int (*)(void))&x))(); // yには123が入るかな?
というようなコードはメモリ保護違反になります。それというのも、&xのメモリが実行属性を持っていないからです。
そこで、メモリの属性をページ単位で強制的に書き換えてしまうのが、このVirtualProtectです。上のプログラムのint y;の上の行に
VirtualProtect((LPVOID)&x,sizeof(x),PAGE_EXECUTE,NULL);
を追加すれば、これは一応、動作するというわけ。簡単ですね。('00/06/19 訂正記事あり→第B4回参照のこと)
ygs2Kは、内部的に仮想CPUコードを生成して実行していますが、そのあと仮想CPUコードをi386のネイティブコードに変換して実行すれば相当のスピードupが計れます。これは、その昔に、この連載で書いていた、8x86のコードをCのソースに変換するプログラムと同じなので簡単です。(あのとき、実際に解答を例示していませんが、今回のプログラムをもって、その解答とします)
余談ですが、8x86系はretが0xc3ですね。0xc3は、Z80だとjmpなんです。CP/Mのコードとか逆アセしてみるとプログラムの先頭には、必ずダミーのjmpが存在するのですが、これは間違って実行ファイルを8x86系のパソコンで実行してしまったときの防御策だそうです。(いまどきCP/Mなんて誰も知らんか…)