QBasicからMS-C/C++7を呼ぶ。

QBasicとMS-C/C++7をリンクしたいのに、まっとうな方法ではできないっぽい。

関連項目:
QuickBasic4.5とCで遊びたいのに。

かつて「BASICは遅い」と言われていて、I/OとかOh!なんたらとかのマイコン雑誌に掲載された投稿プログラムだとBASICソースの中にDATA文でマシン語のコードが埋め込んで初期化中にPOKE命令でそのマシン語コードを適当なメモリに書き込んで必要なときに呼び出す、という手法が使われていた。この方法だとBASIC処理系以外のアセンブラやリンカが必要無いから手軽だけど、マシン語コード修正の度にDATA文に埋め込みなおすのも面倒だし、8ビットマイコンの絶対アドレスならとにかく、x86だとセグメントアドレスのことも考慮しないといけなくて、できればBASIC以外の部分は単体のマクロアセンブラやCコンパイラを使って書きたい。さらにQBasicの場合インタプリタしかなくてCALL ABSOLUTEで動的にアドレスを指定する必要があって、QuickBasicのようにコンパイラで静的リンクが使えない分、マシン語リンクのハードルが高い。

ここではQBasicより先にCで書いたルーチンをメモリに常駐させてからQBasicを子プロセスとして起動し、呼び出されるルーチンのアドレスは環境変数でQBasicに渡すことにした。

呼び出されるCのコード。FARCALLを想定して/ALでラージモデルを使った。/Gsでスタックチェックは無効にする。
INSTFN.CPP

/*
instfn.cpp
cl /AL /Gs instfn.cpp
*/
#include <process.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <string.h>

extern "C" int instfn(int* ptr) {

        // __asm int 3
        int data = *ptr;

        static char msg[]="Called instfn()\r\n$";
        __asm {
                push    ax
                push    dx
                push    ds
                mov     ah,9
                mov     dx,seg msg
                mov     ds,dx
                mov     dx,offset msg
                int     0x21
                pop     ds
                pop     dx
                pop     ax
        };

        return data*2;
}

int main(int argc, char* argv[], char** envp) {
        int ret = 0;
        int i, envnum, envsize, pos;    // カウンタ,現在の環境変数の数(末尾NULL分を含まない),現在の環境変数のサイズ(各末尾のNULを含む),dest用ポインタ

        // 新しくヒープで確保する環境変数のポインタ列と実体
        char** newenvp = NULL;
        char*  newenv  = NULL;

        // 追加する環境変数
        int addnum, addsize;
        char* addenv[] = {"INSTFN=xxxx:xxxx", NULL};
        void* instfnp = instfn;

        // INSTFN環境変数を整形する
        // 子プロセスからinstfn()を呼べるようにアドレスをセットする
        sprintf(addenv[0], "INSTFN=%04lX:%04lX", (((long)instfnp)>>16)&0xffff, ((long)instfnp)&0xffff );

        // 現在の環境変数の個数newenvpとサイズenvsizeを数える
        for(envsize=0, envnum = 0; envp[envnum] != NULL; envnum++) {
                envsize += strlen(envp[envnum])+1;      // +1は末尾のNUL分
        }
        // 追加の環境変数の個数addnumとサイズaddsizeを数える
        for(addsize=0, addnum=0; addenv[addnum] != NULL; addnum++) {
                addsize += strlen(addenv[addnum])+1;    // +1は末尾のNUL分
        }

        // spawn* に渡す環境変数のポインタ列と実体を確保
        newenvp = (char**)malloc((envnum+addnum+1) * sizeof(char*));    // +1は末尾のNULL分
        newenv  = (char*)malloc((envsize+addsize) * sizeof(char));

        // 現在の環境変数をヒープ内にコピー
        for(pos=0, i=0; i<envnum; i++) {
                newenvp[i] = strcpy(newenv+pos, envp[i]);
                pos += strlen(envp[i])+1;       // strcpyは末尾NULをコピーするが、strlenは末尾NULをカウントしない
        }
        // 追加の環境変数をヒープ内にコピー
        for(i=0; i<addnum; i++) {
                newenvp[i+envnum] = strcpy(newenv+pos, addenv[i]);
                pos += strlen(addenv[i])+1;     // strcpyは末尾NULをコピーするが、strlenは末尾NULをカウントしない
        }
        newenvp[envnum+i] = NULL;       // iは追加の環境変数の数+1を指している
        
        // 子プロセス実行
        ret = _spawnvpe(_P_WAIT, argv[1], (char const**)&argv[1], (char const **)newenvp);

        // ヒープを開放
        free(newenvp);
        free(newenv);

        printf("terminate child process (%d)\n", ret);
        return ret;
}

呼び出すQBasicのコード
CALLFN.BAS

DIM INSTFN AS STRING, segm AS LONG, offs AS LONG
DIM dat AS INTEGER

'Get INSTFN entry point from environment vars
INSTFN = ENVIRON$("INSTFN")
IF LEN(INSTFN) <> 9 THEN PRINT "No INSTFN= environment": END

segm = VAL("&h" + LEFT$(INSTFN, 4))
offs = VAL("&h" + RIGHT$(INSTFN, 4))
PRINT "fn="; RIGHT$("0000" + HEX$(segm), 4); ":"; RIGHT$("0000" + HEX$(offs), 4)

dat = &H7890

'Call INSTFN entry point
DEF SEG = segm
CALL absolute(BYVAL dat, offs)

DEF SEG

SYSTEM

コンパイルと実行。※スクリーンショットでは間違っているが日本語モードでは動かないのでQBasic実行前にCHEV USで英語モードにしておく。

:INSTFN.CPPコンパイル
cl /AL /Gs instfn.cpp

:INSTFNを実行して常駐
instfn command.com

:QBasicを実行してCルーチンを呼出すテスト
qbasic /run callfn.bas

Cのコードに埋め込んだDOSコールでメッセージを表示する。Cのコードとリンクすると言いながらCの標準ライブラリが安定して動かなかったから結局アセンブラになってしまった。こんなんだったら最初からアセンブラで組んだほうが良かったかもしれない。

Cソースの// __asm int 3 の部分はコメントアウトするとDEBUGでデバッグするためのブレークポイントになる。この場合はINSTFN.EXEの実行をDEBUG INSTFN.EXE COMMAND.COMとしてデバッガ上で実行すると呼び出されたときにデバッガで停止できる。

テスト用として、CからINSTFNを呼び出すコード。
CALLFN.CPP

/*
cl /AL /Gs callfn.cpp 
*/
#include <stdio.h>
#include <string.h>

#define FNENV   "INSTFN="

// INSTFN=seg:offの文字列から関数ポインタを取り出して呼び出すテスト
extern "C" typedef int(*FN)(int*);       // 呼び出す関数プロトタイプ

int callfn(char** envp) {
        char* pos;
        int i;
        long seg=0, off=0;

        FN fn;
        int dat;

        // envpからINSTFN=を探す
        for(; *envp!=NULL; envp++) {
                if(strncmp(*envp, FNENV, strlen(FNENV))==0) {
                    break;
                };
        }
        if(*envp==NULL) {       // 見つからなかった
                printf("%s environment not found.\n", FNENV);
                return 0;
        }
        pos = *envp+strlen(FNENV);      // 先頭からsscanfするとおかしい。
        sscanf(pos, "%04X:%04X", &seg, &off);
        fn = (FN)((seg << 16) | off);   // 関数ポインタのアドレスを組み立てる
        printf("pos=%s, seg=%04lX, off=%04lX, fn=%lx\n", pos, seg, off, (long)fn);

        // 呼び出し
        dat = 1122;
        dat = (*fn)(&dat);
        printf("ret=%d\n", dat);

        return dat;
}

int main(int argc, char* argv[], char** envp) {

        // 呼び出すテスト
        callfn(envp);

        return 0;
}

Cのコードを呼び出してインラインアセンブラのコードに渡ればあとは何とかなるだろうってことで、今回はこれでおしまい。1980年代ならネタにもなったかもしれないが、時すでに2020年代。

QBasicからMS-C/C++7を呼ぶ。」への1件のフィードバック

  1. ピンバック: パソコンサンデーしてみる。 | の回想録

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中