GTK+ による Python コンソールの作成

2010年2月23日

はじめに

GTK+ を使って Python コンソールを作る。

環境

GTK+ 2.16.6, Python 2.6.4, MinGW 5.1.4

ファイル

gpython (simple)

別途 GTK+ により作っておいたコンソールモジュール console を使って、Python コンソールを作る。コンソールモジュールは GtkTextView をベースにしたもので、コマンドライン部分以外は編集できないようになっている。コマンド履歴機能を持つ。操作方法はだいたい bash に従っている。

Python インタプリタの実装方法としては、code モジュールの InteractiveConsole を使用する。そのままでは Python の出力は標準出力、標準エラー出力に出されるので、sys.stdout, sys.stderr を上書きしてコンソールに出力されるようにする。入力をコンソールからもらい、InteractiveConsole の push() で Python のコードを実行する。

プログラムを見ていこう。

gpython.c

int main(int argc, char *argv[])
{
	...
	window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
	gtk_window_set_title(GTK_WINDOW(window), TITLE);
	gtk_window_resize(GTK_WINDOW(window), WIDTH, HEIGHT);
	gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
	g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);

	console = console_new();
	gtk_container_add(GTK_CONTAINER(window), CONSOLE_WIDGET(console));

ウインドウを作り、コンソールのオブジェクトを作成し、そのウィジットをウインドウに関連付けている。

	console_set_prompt(console, ">>> ");
	console_set_callback(console, command);

Python の初期のプロンプトとコールバック関数をコンソールに設定する。このコールバック関数は、ユーザーがコマンドを入力したときに呼び出され、入力された文字列が渡される。

	sprintf(str, "Python %s on %s\n", Py_GetVersion(), Py_GetPlatform());
	console_write(console, str);

	sprintf(str, "Type \"help\", \"copyright\", \"credits\" or \"license\" "
	  "for more information.\n");
	console_write(console, str);

Python ぽい初期メッセージをコンソールに表示する。

	init_python();
	console_start(console);

Python 関連の初期化処理を行い、コンソールをスタートさせる。

Python 関連の初期化処理は以下のようになる。

static void init_python(void)
{
	...
	Py_Initialize();

	m = Py_InitModule("output", output_methods);
	PySys_SetObject("stdout", m);
	PySys_SetObject("stderr", m);

まず、Python を初期化する。続いて write() メソッドをもつ output モジュールを作成し、sys.stdout, sys.stderr にセットしている。これで Python の標準出力、標準エラー出力への出力をつかまえて、コンソールに送る。write() (実装は output_write()) は出力文字列をそのままコンソールへと出力させる。

	m = Py_InitModule("input", input_methods);
	PyRun_SimpleString("import input as __input");
	PyRun_SimpleString("__builtins__.raw_input = __input.raw_input");
	PyRun_SimpleString("del __input");

raw_input() メソッドをもつ input モジュールを作成し、ビルトインの raw_input() を自前のものと置き換えている。これは raw_input() を無効化するためのものである。こうしないと、raw_input() が呼ばれたときに標準入力からの入力待ちに入り、コンソールが固まってしまう。たとえば、help() や license() などを実行したときにそうなる。raw_input() (実装は input_raw_input()) は、プロンプト文字は表示するが、ただちに空文字を返して入力待ちを終わらせる。

	m = Py_InitModule("exit", exit_methods);
	PyRun_SimpleString("import exit as __exit");
	PyRun_SimpleString("__builtins__.exit = __exit.exit");
	PyRun_SimpleString("__builtins__.quit = __exit.exit");
	PyRun_SimpleString("del __exit");

exit() メソッドをもつ exit モジュールを作成し、ビルトインの exit() と quit() を自前のものと置き換えている。というのは、ここでは InteractiveConsole の push() を使って Python コードを実行させるのだが、これが exit() や quit() にちゃんと対応してくれないからである。

	PyRun_SimpleString("import code");
	PyRun_SimpleString("__interpreter = code.InteractiveConsole(locals())");
	PyRun_SimpleString("del code");

	mainModule = PyImport_AddModule("__main__");
	globalDict = PyModule_GetDict(mainModule);
	interpreter = PyDict_GetItemString(globalDict, "__interpreter");
	Py_INCREF(interpreter);

code モジュールをインポートし、InteractiveConsole を取得している。

Python コードの実行は、コンソールに設定したコールバック関数 command() で行う。

static void command(Console *console, const char *command)
{
	...
	result = PyObject_CallMethod(interpreter, "push", "s", command);

	if(result){
		if(PyArg_Parse(result, "i", &status)){
			multiline = status > 0;
		}
		Py_DECREF(result);
	}

	if(multiline){
		console_set_prompt(console, "... ");
	}else{
		console_set_prompt(console, ">>> ");
	}
}

引数 command にユーザーが入力した文字列が入ってくるので、それを PyObject_CallMethod() を使って InteractiveConsole の push() に渡している。これでユーザーの入力が Python コードとして実行される。返り値の result を調べることで、一行で終わるコードか、if 文のような複数行にわたるコードかがわかる。それに応じてコンソールのプロンプトを設定しなおしている。

以上で、ほぼ Python コンソールの完成である。これでもまあ使い物にはなるのだが、1 つ、raw_input() が使えない。それを使用している関数 (help(), license() など) は正しく機能しない。

gpython (using threads)

上の例では、Python の出力 (sys.stdio, sys.stderr) を置き換えてコンソールへと出力させた。だったら同じように、Python の入力 (sys.stdin) を置き換えて、コンソールから入力できるようにすればいいじゃない、そうすれば raw_input() の問題もなくなる、と考えたくなるが、sys.stdin はファイルなので、そういうわけにはいかない。だったら、raw_input() をコンソールからの入力を受け取るように書き換えてしまおう。Python はデータの特別な入力のためだけではなく、ふつうのコードの入力でも raw_input() を使っているようで、stdin を置き換えなくても、raw_input() を置き換えてしまえばすべての入力を置き換えられると考えてよさそうである。

問題は、単純に入力待ちに入ってしまうと、やっぱりコンソールが固まってしまうことである。これに対処するために、スレッドを使う。

入力を置き換えてしまえば、Python コードの実行には InteractiveConsole の push() の代わりに interact() が使える。interact() は raw_input() で入力を受け取るので、interact() を実行してコンソールに入力すれば、勝手に Python のコードが実行されることになる。

スレッドには gthread を使う。Makefile の変数を以下のようにする。

CFLAGS = -Wall -O2 `pkg-config --cflags gtk+-2.0 gthread-2.0`
LIBS = -L/c/Python26/libs -lpython26 `pkg-config --libs gtk+-2.0 gthread-2.0`

プログラムを見ていこう。

gpython.c

int main(int argc, char *argv[])
{
	...
	g_thread_init(NULL);
	gdk_threads_init();
	
	...
	
	gdk_threads_enter();
	gtk_main();
	gdk_threads_leave();
	...

main() にはスレッド用に上記のコードが挿入されている。

static void init_python(void)
{
	...
	g_thread_create(python_proc, NULL, FALSE, &error);
}

Python の初期化処理の最後で、Python 用のスレッドを作成している。python_proc() は、InteractiveConsole の interact() を実行する。

static gpointer python_proc(gpointer data)
{
	PyRun_SimpleString("__interpreter.interact(\"\")");

	return NULL;
}

これで、Python とコンソールが別スレッドとして動く。

raw_input() の実装がちょっとややこしい。

static void command(Console *console, const char *command)
{
	strncpy(inputString, command, MAX_COMMAND_SIZE);
	waiting = 0;
}

...

static PyObject* input_raw_input(PyObject *self, PyObject *args)
{
	const char *str;
	char command[MAX_COMMAND_SIZE];

	if(PyArg_ParseTuple(args, "s", &str)){
		gdk_threads_enter();
		console_write(console, str);
		gdk_threads_leave();
	}

	waiting = 1;
	while(waiting){
		g_usleep(SLEEP_TIME);
	}

	strncpy(command, inputString, MAX_COMMAND_SIZE);
	inputString[0] = '\0';

	return PyString_FromString(command);
}

raw_input() (input_raw_input()) は、プロンプトが指定されていればそれを表示したあと、ループにより待ちに入る。ただループするとあれなので、スリープを入れている。寝て起きてやっている間、別行動中のコンソールからコールバック関数 command() が呼び出され、inputString に文字列がセットされたあと、waiting が 0 に設定されると、ループから抜け出して入力文字列を取得し、それを Python の文字列型として返す。

これで Python コンソールの完成である。