2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[なでしこv1] 動的DLLロード

Last updated at Posted at 2024-12-23

更新履歴

  • 2024/12/23 … 記事を作成
  • 2024/12/31 … なでしこでも sprintf が使いたいっ! を更新
  • 2025/01/01 … 誤字等の修正

まえがき

日本語プログラミング言語「なでしこ」が今年で20周年を迎えました。
筆者が当時他に触っていた言語はHot Soup Processor (HSP)と、ひまわり、そしてホームページ制作でデザインを飾るための簡単なJavaScriptくらいでした。
(QBASICというWin98のインストールCDに付属のBASIC言語も少し弄ったことがありますが、これはどちらかというと配布されているゲームを遊ぶ目的で導入していた記憶があります)
また、長年愛用している作曲ソフト テキスト音楽「サクラ」 には IfFor などが使えるスクリプト機能があります。これもプログラミングの一種にカウントすれば、
人生で最も続けているプログラミングはDelphi、なでしこ、サクラかもしれません。

はじめに

なでしこv1で外部DLLの関数を使うための言語の機能としては、次の構文があります。

user32.dll の MessageBoxA 関数を MsgBox として宣言
●MsgBox(hwnd,text,caption,type) = DLL("user32.dll", "int MessageBoxA(int,char*,char*,int)")

ただし、この方法はDLLがプログラムの起動時に読み込めることを前提に動作するものです。もし、そのDLLや関数が見つからなかったりして失敗するとエラーになり起動すらできなくなってしまいます。
呼び出す関数が必須の機能ならともかく、補助やプラグイン的な位置付けである場合、そのファイルがないと起動すらできない…というのも困るでしょう。
これを解決するのが、プログラムの実行中にDLLを動的ロードする方法です。

DLLの読み込みと関数アドレスの取得

まずはDLLの読み込みに必要な Windows API を宣言しておきます。

DLLの動的ロードを行うのに必要な Windows API
●LoadLibrary(dllFile) = DLL("kernel32.dll", "int LoadLibraryA(char*)")
●FreeLibrary(handle) = DLL("kernel32.dll", "int FreeLibrary(int)")
●GetProcAddress(handle,apiName) = DLL("kernel32.dll", "int GetProcAddress(int,char*)")

LoadLibrary でDLLを読み込んで得られたハンドルと GetProcAddress を使って関数のアドレスを取得します。
ここでは簡単なサンプルとしてビープ音を鳴らしてみます。

DLLの読み込みと関数アドレスの取得
kernel32 = LoadLibrary(`kernel32.dll`)
beep     = GetProcAddress(kernel32, `Beep`)

DLLのハンドルと関数ポインタが0以外であれば成功です。

もし(kernel32 <> 0)かつ(beep <> 0)なら『DLLのロードに成功しました』という

関数に渡す引数は、あらかじめ確保しておいたメモリに書き込んで準備します。
バイナリ設定 を使う場合は、データの書き込み位置の指定が 1起点 であることに注意が必要です。

引数の設定
引数スタックに8を確保 // Beep 関数は int, int を引数に取るため 8 バイト
引数スタックの1に440を『int』でバイナリ設定 // 第1引数は周波数(A=440)
引数スタックの5に250を『int』でバイナリ設定 // 第2引数は音の長さ(ミリ秒)

GetProcAddress で取得した関数を呼び出すには EXEC_PTR を使います。
呼出規約はドキュメントを参考にして指定します。
Win32APIは基本的に stdcall ですが、後述する sprintfcdecl 規約です。

呼出規約
# 呼出規約はstdcall、引数はint型が2つなので8バイト、戻り値はBOOL
EXEC_PTR(`stdcall`, beep, 8, 引数スタック, `BOOL`)

LoadLibrary で読み込んだDLLは使い終わったら FreeLibrary で解放しましょう。

FreeLibrary(kernel32) // 使わなくなったら解放

実行環境

Intel(R) Core(TM) i7-6700HQ 2.60GHz RAM 16GB
Windows 10 Pro 22H2 19045.5011
なでしこ ver.1.590

グループ化して使いやすくする

上記のコードをいちいちベタ打ちなんて面倒…と思ったことでしょう。
変数の管理も楽にしたいですね。そこで、グループ化です。

dll.nako
!型一覧は「int=4{~}long=4{~}word=4{~}byte=4{~}dword=4{~}qword=8{~}int64=8
char*=4{~}void*=4{~}char=4{~}float=4{~}double=8{~}bool=4」

■DLLローダー
 ・{文字列}DLLファイル名
 ・{整数}ハンドル
 ・{配列}登録済み引数
 ・{配列}登録済み引数型
 ・{=`long`}戻り値型
 ・初期化処理~
 ・作る~初期化処理
 ・ロード(DLLから|DLLを)~
  もしハンドルが0でなければ『このインスタンスでは既にDLLが開かれています。』とエラー発生
  DLLを母艦パスで相対パス展開してDLLファイル名に代入
  LoadLibrary(DLLファイル名)をハンドルに代入
  もしハンドルが0なら「DLL "{DLLファイル名}" が読み込めません。」とエラー発生
 ・開く(DLLから|DLLを)~DLLをロード
 ・閉じる~FreeLibrary(ハンドル)して0をハンドルに代入
 ・引数初期化~登録済み引数は空。登録済み引数型は空。
 ・引数追加(VALUEをTYPEとして|TYPEで)~
  TYPEを空白除去して小文字変換してATYPEに代入
  もし型一覧@ATYPEが空なら「{ATYPE}型には対応していません。」でエラー発生
  VALUEを登録済み引数に配列追加
  ATYPEを登録済み引数型に配列追加
 ・呼び出す(FUNCを{=`stdcall`}CCで)~
  もしハンドルが0なら『DLLが読み込まれていません。』とエラー発生
  GetProcAddress(ハンドル, FUNC)をアドレスに代入
  もしアドレスが0なら「{DLLファイル名} の {FUNC} が見つかりません。」とエラー発生

  スタックサイズは0
  登録済み引数型を反復
   もし対象で『*』が何文字目 > 0なら対象は『int』
   型一覧@対象を整数変換して、それをスタックサイズに直接足す
  引数スタックにスタックサイズを確保

  データ書き込み位置は1
  登録済み引数型を反復
   もし対象で『*』が何文字目 > 0なら対象は『int』
   引数スタックのデータ書き込み位置に登録済み引数[回数 - 1]を対象でバイナリ設定
   型一覧@対象を整数変換して、それをデータ書き込み位置に直接足す
  EXEC_PTR(CC, アドレス, データ書き込み位置 - 1, 引数スタック, 戻り値型)

●LoadLibrary(dllFile) = DLL("kernel32.dll", "int LoadLibraryA(char*)")
●FreeLibrary(handle) = DLL("kernel32.dll", "int FreeLibrary(int)")
●GetProcAddress(handle,apiName) = DLL("kernel32.dll", "int GetProcAddress(int,char*)")

上記のコードを dll.nako として保存してください。
次の例は接続されているゲームパッドを取得して表示するものです。

enum-gamepad.nako
!『dll.nako』を取り込む

MMとはDLLローダー
MMについて
 「{SYSTEMパス}winmm.dll」を開く
 『joyGetNumDevs』を呼び出してゲームパッド最大認識可能数に代入

 !JOYCAPS_STRUCT_SIZE = 404
 JOYCAPSにJOYCAPS_STRUCT_SIZEを確保
 ゲームパッド数は0
 (ゲームパッド最大認識可能数)回
  引数初期化
  回数 - 1を『int』として引数追加
  POINTER(JOYCAPS)を『void*』として引数追加
  JOYCAPS_STRUCT_SIZEを『int』として引数追加
  『joyGetDevCapsA』を呼び出して結果に代入
  もし結果が0なら
   ゲームパッド接続数に1を直接足す
   JOYCAPSの61を『DWORD』でバイナリ取得してボタン数に代入
   「ID:{回数 - 1} … {ボタン数} ボタン」と表示

 もしゲームパッド接続数≧1なら
  「{ゲームパッド接続数}個のゲームパッドが接続されています。」と表示
 違えば
  「ゲームパッドが接続されていません。」と表示
 閉じる

ここでは、iBUFFALOのBSGP801とBSGP1204を接続した状態で実行してみます。

実行結果
ID:0 … 8 ボタン
ID:1 … 12 ボタン
2個のゲームパッドが接続されています。

なでしこv1は可変長引数に対応していないので、引数の登録は 引数追加 で値と型を指定して行う方法を取っています。引数の内容はグループ内の配列で保持しています。
新しく関数呼び出しを行う場合は 引数初期化 を行う必要があります。

なでしこでも sprintf が使いたいっ!

他のプログラミング言語を習得している方なら、書式指定で文字列を出力できる printf 系関数の便利さはご存知の通りです。
なでしこにも FORMAT または 形式指定 関数があることはあるんですが、

  • 引数が 1つ しか指定できない
  • 使用できる書式指定子が限られている(f,m,d,x,X のみ)

という物足りない仕様になっています。
そこで、Visual C++ ランタイム ライブラリ MSVCRT.DLLsprintf を使う方法を試してみましょう。

必要な文字列長を確保してから出力を実行する

sprintf を使う前に、まずは文字列を格納するためのメモリを確保する必要があります。
_scprintf 関数を使うと、 sprintf で書き込むのに必要な文字列長を求めることができます。

int _scprintf(const char *format [,argument] ...);
int sprintf(char *buffer, const char *format [,argument] ...);

_scprintfsprintf を呼ぶときの違いは、格納先指定(第1引数)の有無だけです。
また、どちらも呼出規約は cdecl を指定します。

sprintf.nako
!『dll.nako』を取り込む
!SPRINTF_FORMAT = "%sさんが 次のレベルになるまで{~}あと %I64u の経験値が必要です。"
!SPRINTF_MAXINT64 = -1

CRTとはVCランタイム
エラー監視
 CRTについて
  引数初期化
  POINTER(SPRINTF_FORMAT)を『char*』として引数追加
  POINTER(`テトン`)を『char*』として引数追加
  SPRINTF_MAXINT64を『int64』として引数追加
  形式出力して表示
エラーならば
 エラーメッセージを表示
CRTを閉じる

■VCランタイム +DLLローダー
 ・作る~初期化処理して「{SYSTEMパス}msvcrt.dll」をロード
 ・必要文字列長取得~『_scprintf』を『cdecl』で呼び出す
 ・形式出力~
  必要文字列長取得して文字列長に代入
  一時文字列に文字列長 + 1を確保
  登録済み引数の0にPOINTER(一時文字列)を配列挿入
  登録済み引数型の0に『char*』を配列挿入
  『sprintf』を『cdecl』で呼び出す
  登録済み引数の0を配列削除
  登録済み引数型の0を配列削除
  一時文字列を戻す
実行結果
テトンさんが 次のレベルになるまで
あと 18446744073709551615 の経験値が必要です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?