空飛ぶ気まぐれ雑記帳

主に趣味とかプログラミングについて扱います。

スクリプトとDLLの間に生きる時のデバッグ

はじめに

4月になって環境が変わって色々と忙しい今日このごろ。
気づけば4月も末日、そろそろGW前だというのに未だに慣れず。

そう、4月末なのに今月はまだ1記事も書いてないのです。
マズイ…

というわけで、今回はちょっとしたデバッグのテクニックをご紹介。
他の人がCでDLL作ったけど、Pythonから関数呼び出して使うとなぜか動かない…でも、他人のソースを全部読むのはダルい…という場合にオススメです。

DLLを作る

とりあえず、今回は最小限の構成でDLLを作るのでヘッダーファイルなしのcppファイルのみです。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(DLL)

add_library(${PROJECT_NAME} SHARED DLLFunc.cpp)

DLLFunc.cpp

#include <iostream>
#include <random>

#define DLL_EXPORT __declspec(dllexport)

extern "C" DLL_EXPORT float add(int x, int y)
{
  float value = std::random_device()();
  return x + y + value;
}

extern "C" DLL_EXPORT void print(const char* s)
{
  std::cout << "DLL:" << s << std::endl;
}


これをcmakeしてからビルドすればとりあえず、DLLが出来上がります。
この時、DLLのデバッグをしたいので、DLLのビルド時のConfigはReleaseではなく、DebugかRelWithDebInfoを選択してください。
このRelWithDebInfoというのは最適化は掛けるけど、デバッグシンボルを埋め込むため、ブレークポイントを挿入したときにちゃんと止まってくれるようになります。(実際は一部最適化の関係でブレークポイントを挿入できない場所もあります)

DLLのテスト

最初はとりあえず、C++からテスト。というわけで、下記のコードを使います。コマンドライン引数にDLLのパス(Debugビルドのもの推奨)を与えてください
なお、もちろんですが下記のファイルは別のディレクトリに保存してくださいね。

CMakeLists.txt

cmake_minimum_required(VERSION 3.3)

project(Main)

add_executable(${PROJECT_NAME} main.cpp)

main.cpp

#include <iostream>
#include <string>
#include <Windows.h>

int main(int argc, char** argv)
{
  if (argc < 2)
  {
    std::cerr << "Wrong arguments!\nMain.exe <Input dll path>" << std::endl;
    return -1;
  }

  auto dll = LoadLibraryA(argv[1]);

  auto print_fn = GetProcAddress(dll, "print");
  auto add_fn = GetProcAddress(dll, "add");

  auto value = reinterpret_cast<float(*)(int x, int y)>(add_fn)(10, 20);
  reinterpret_cast<void(*)(const char*)>(print_fn)(std::to_string(value).c_str());

  FreeLibrary(dll);

  return 0;
}


これを実行すると何か標準出力に出るはずです。
このとき、Mainソリューションを開いた状態でDLLFunc.cppをVisualStudioで開き、ブレークポイントを設定すればちゃんとブレークしてくれるはずです。

Pythonで使う

ここまでだとぶっちゃけあまり意味がないのでPythonスクリプトから呼び出します。
実際に使ったのは下記コード。

import ctypes
import sys

print('Type input dll')
input_dll = input()
print('--------------------------------------')
dll = ctypes.cdll.LoadLibrary(input_dll)

dll.print.argtypes = [ctypes.c_char_p]
dll.add.restype = ctypes.c_float
dll.add.argtypes = [ctypes.c_int, ctypes.c_int]

value = dll.add(10, 20)
dll.print(ctypes.c_char_p(str(value).encode('utf-8')))


基本的には先程のテストと同様ですが今回は実行時にちょっとした処理を必要とするため、標準入力からdllファイルのパスを受け取るようにしています。
それから実際に実行して、DLLのパスを入力する前にブレークポイントを入れるためにちょっとした処理を行います。

まず、Visual Studioを起動します。次に「デバッグ>プロセスにアタッチ」もしくはctrl+alt+pで表示されるダイアログの選択可能なプロセスから「python.exe」を探します。pとか押せばpのとこまで飛べるんでそれを活用しつつ選んでから「アタッチ」ボタンを押せばアタッチ完了です。
この状態でDLLFunc.cppを開いてブレークポイントを入れれば、pythonから該当する関数を呼び出すとちゃんとブレークポイントで止まります。

まとめ

DLLデバッグしたいと思ったらとりあえずctrl+alt+pでプロセスにアタッチと覚えとけばいいと思います。
今回は簡単な例でしたが、DLL内で例外が発生して落ちてる場合もVisual Studioがキャッチしてくれて、その時のコールスタックが見れるので、それを活用してデバッグしましょう。
それとPython以外でもmatlabやRなんかでも使えるので(matlabの場合javaで動いている影響かガンガン例外が飛び交うので少々知識が必要ですが)是非お試しあれ