11月2008

C++クラスをLuaから使う

Luaスクリプト言語はC言語と親和性が高く、C言語のプログラムにLuaスクリプトを組み込むことによってLuaの関数が呼べたり、逆にLuaからC関数を呼び出すことも可能となっています。前回は、Luaだけでオブジェクト指向を実現するコードを書いてみましたが、今回は、LuaにC++のクラスを公開するコードを書いてみました。

C++のコード:

#include <stdio.h>
#include "lua.hpp"
#include <iostream>

#include <string>
using namespace std;

class CObject {
	string m_Name;
public:
	CObject(string name);
	~CObject();

	string description();
	string getName();
};

CObject::CObject(string name)
{
	m_Name = name;
}

CObject::~CObject()
{
	cout < < m_Name << " is released." << endl;
}

string CObject::description()
{
	return "CObject: " + m_Name;
}

string CObject::getName()
{
	return m_Name;
}

int tolua_CObject_alloc(lua_State *L)
{

	lua_newuserdata(L, sizeof(CObject *)); //2
	lua_newtable(L); //3
	lua_getfield(L, 1, "className"); //4
	const char *name = luaL_checkstring(L, 4);
	lua_getglobal(L, name); //5 -> 4
	lua_remove(L, 4);
	lua_getfield(L, 4, "description");  //5
	lua_setfield(L, 3, "__tostring");
	lua_setfield(L, 3, "__index");
	lua_setmetatable(L, 2);
	return 1;
}

int tolua_CObject_release(lua_State *L)
{
	CObject *p = *(CObject **)lua_touserdata(L, 1);
	delete p;
	return 0;
}

int tolua_CObject_init(lua_State *L)
{
	CObject **p = (CObject **)lua_touserdata(L, 1);
	const char *name = luaL_checkstring(L, 2);
	*p = new CObject(name);
	lua_settop(L, 1);
	return 1;
}

int tolua_CObject_description(lua_State *L)
{
	CObject *p = *(CObject **)lua_touserdata(L, 1);
	lua_pushstring(L, p->description().c_str());
	return 1;
}

int tolua_CObject_getName(lua_State *L)
{
	CObject *p = *(CObject **)lua_touserdata(L, 1);
	lua_pushstring(L, p->getName().c_str());
	return 1;
}

int tolua_CObject(lua_State *L)
{
	lua_newtable(L);
	lua_pushstring(L, "CObject");
	lua_setfield(L, -2, "className");
	lua_pushcfunction(L, tolua_CObject_alloc);
	lua_setfield(L, -2, "alloc");
	lua_pushcfunction(L, tolua_CObject_release);
	lua_setfield(L, -2, "release");
	lua_pushcfunction(L, tolua_CObject_init);
	lua_setfield(L, -2, "init");
	lua_pushcfunction(L, tolua_CObject_description);
	lua_setfield(L, -2, "description");
	lua_pushcfunction(L, tolua_CObject_getName);
	lua_setfield(L, -2, "getName");
	return 1;
}

int main (int argc, const char * argv[]) {
    lua_State *L = lua_open();
	luaL_openlibs(L);

	lua_register(L, "CClass_CObject", tolua_CObject);

	while ( getchar() != 'e') {
		if ( luaL_dofile(L, "hello.lua") ) {
			cout < < lua_tostring(L, -1) << endl;
		}
	}

	lua_close(L);
    return 0;
}

hello.luaのコード

print( "Hello Lua!" )

CObject = CClass_CObject()

obj = CObject:alloc():init("anObject")
print( obj )
print( obj:getName() )
obj:release()

やってることは、前回のLua上でオブジェクトシステムを作ったやり方とほぼ同じで、それをC言語からやっています。重要な点と言えば、クラスを公開する場合は、ライトユーザーデータではなく、フルユーザーデータのほうを使う点です。ライトユーザーデータのほうは、単にポインタを数値的にluaとやり取りするだけなので、メタテーブルを設定することができません。しかし、フルユーザーデータのほうでは、メタテーブルを持つことができます。したがって、フルーユーザーデータを作成し、そこの領域にインスタンスのアドレスを入れることによって、Lua側でメソッドを実行した時にthisポインタを得ることが可能になります。

メタテーブルの”__gc”を設定することによって、Lua側でガベージコレクトされた時に、インスタンスをdeleteすることも可能ですが、Luaから見えないところ(C++側など)でインスタンスが保持されている場合も考えられるので、なんらかの規則を導入しない限りお勧めできません。

ちなみに、前回のオブジェクトシステムと組み合わせて、C++側から公開されたクラスを、Lua側で継承させることができそうですが、この方法ではまだ不十分です。フルユーザーデータは、あくまでテーブルではないので、Lua側で勝手にインスタンス変数を追加することができません。まだ試していないのですが、おそらく”__newindex”のメタテーブルを使って、インスタンス変数の追加を実現することも可能でしょう。

Luaでオブジェクト指向

Lua5.1.3でオブジェクトシステムを作ってみました。軽くObjective-C風です。

print( "Hello Lua!" )

-- クラスを作成する関数
function Class(name, super)
	local t = {}
	t.className = name
	t.superClass = super
	setmetatable( t, { __index = super } )
	return t
end

-- Objectクラスの宣言
Object = Class( "Object" )

-- インスタンスを作成
function Object:alloc()
	local t = {}
	setmetatable( t, { __index = self, __tostring = self.description } )
	return t
end

-- 初期化
function Object:init()
	return self
end

-- __tostringの実装
function Object:description()
	return self.className
end

View = Class( "View", Object )

function View:init( name, x, y, w, h)
	self = View.superClass.init(self)
	self.name = name
	self.x = x
	self.y = y
	self.w = w
	self.h = h
	return self
end

function View:description()
	return string.format( "%s { name = %s, x = %d, y = %d, w = %d, h = %d }", self.className, self.name, self.x, self.y, self.w, self.h)
end

function View:draw()
	return self.name
end

TextView = Class( "TextView", View )

function TextView:init( name, x, y, w, h, text)
	self = TextView.superClass.init(self, name, x, y, w, h)
	self.text = text
	return self
end

function TextView:description()
	return string.format( "%s { name = %s, x = %d, y = %d, w = %d, h = %d, text = %s }", self.className, self.name, self.x, self.y, self.w, self.h, self.text)
end

view = View:alloc():init( "aView", 20, 20, 43, 40 )
textView = TextView:alloc():init( "aTextView", 0, 0, 40, 40, "Text")
print( view )
print( textView )
print( view:draw() )
print( textView:draw() )

出力:

Hello Lua!
View { name = aView, x = 20, y = 20, w = 43, h = 40 }
TextView { name = aTextView, x = 0, y = 0, w = 40, h = 40, text = Text }
aView
aTextView

スーパークラスのメソッドの呼び出し方法があまりよくないです。クラスメソッド的な感じで呼び出して、第一引数にselfを呼び出すという、多少強引な呼び出し方。もっとスマートにできないものか…

やってみた感想としては、メソッド呼び出す時は「:」で、インスタンス変数のときは「.」というところで、書き間違いそうで怖いです。柔軟なスクリプト言語のデメリットですね。

NSTaskでコマンドを実行

Cocoaからコマンドライン型のプログラムを実行したい時は、NSTaskを使います。NSTaskを使うと、かなり簡単に外部のプログラムを実行することができます。単に実行するだけならば、おそらくリファレンスを見るだけですぐ使い方がわかると思いますので、出力を読み取る方法を説明します。

単に読み取る場合、次のようにします。

NSTask *task = [[NSTask alloc] init];
NSPipe *pipe = [[NSPipe alloc] init];
[task setLaunchPath:@"/bin/ls"];
[task setStandardOutput:pipe];
[task launch];

NSFileHandle *handle = [pipe fileHandleForReading];
NSData *data = [handle  readDataToEndOfFile];
NSString *string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];

NSLog(string);

[task release];
[pipe release];

最も簡単な出力を読み取る方法は、たぶん上記のコードです。readDataToEndOfFileは、すべて読み取るまでブロッキングが発生します。つまり、このメソッドの場合、プログラムが終了するまでブロッキングが発生するので、NSTaskのwaitUntilExitを使う必要はありません。NSFileHandleにはavailableDataというメソッドもありますが、こちらも全くpipeにデータが無い場合、ブロッキングが発生するので、実行出力が出続けるような場合、GUIを使ったアプリケーションで使いたい場合は、他の方法が必要になります。

出力が出るたびに読み取りたい場合、NSFileHandleのreadInBackgroundAndNotifyメソッドを呼び出します。これは、データが読み取り可能になった時に、NSFileHandleReadCompletionNotification通知が送られます。このブログでは一度も紹介したことありませんが、NSNotificationのパターンです。

- (IBAction)action:(id)sender
{
	task = [[NSTask alloc] init]; //インスタンス変数あたりで宣言しておく
	pipe = [[NSPipe alloc] init]; //インスタンス変数あたりで宣言しておく

	[task setStandardOutput:pipe];
	[task setLaunchPath:@"/bin/ls"];
	[task launch];

	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(readData:) name:NSFileHandleReadCompletionNotification object:nil];
	[[pipe fileHandleForReading] readInBackgroundAndNotify];
}

- (void)readData:(NSNotification *)notification
{
	NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
	NSString *string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];

	NSLog(string);

	if ( [task isRunning] ) {
		[[pipe fileHandleForReading] readInBackgroundAndNotify];
	} else {
		[task release];
		[pipe release];
		[[NSNotificationCenter defaultCenter] removeObserver:self];
	}
}

データの読み取りは、notificationのuserinfoにNSFileHandleNotificationDataItemをキーとしてNSDataで格納されています。readInBackgroundAndNotifyは、一度通知したらそれでおしまいになるので、もう一度通知を受信したい場合は、readInBackgroundAndNotifyメソッドを再度実行します。

最後に、二つのコマンドラインプログラムを、パイプで繋ぐ方法です。ターミナルで|を使って実行するやつです。
ls -an 一時ディレクトリ | head を実行する例です。

- (IBAction)action:(id)sender
{
	task1 = [[NSTask alloc] init]; //インスタンス変数
	task2 = [[NSTask alloc] init]; //インスタンス変数
	outPipe = [[NSPipe alloc] init]; //インスタンス変数

	//パイプを繋ぐ
	NSPipe *pipe = [NSPipe pipe];
	[task1 setStandardOutput:pipe];
	[task2 setStandardInput:pipe];
	[task2 setStandardOutput:outPipe];

	[task1 setLaunchPath:@"/bin/ls"];
	[task1 setArguments:[NSArray arrayWithObjects:@"-an", NSTemporaryDirectory(), nil]];
	[task2 setLaunchPath:@"/usr/bin/head"];
	[task1 launch];
	[task2 launch];

	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(readData:) name:NSFileHandleReadCompletionNotification object:nil];
	[[outPipe fileHandleForReading] readInBackgroundAndNotify];
}

- (void)readData:(NSNotification *)notification
{
	NSData *data = [[notification userInfo] valueForKey:NSFileHandleNotificationDataItem];
	NSString *string = [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] autorelease];

	NSLog(string);

	if ( [task2 isRunning] ) {
		[[outPipe fileHandleForReading] readInBackgroundAndNotify];
	} else {
		[task1 release];
		[task2 release];
		[outPipe release];
		[[NSNotificationCenter defaultCenter] removeObserver:self];
	}
}

nConverter betaをリリースします

nConverter ウェブサイト

もともと個人的に使ってたものですが、少々作りかえて一般にリリースすることにしました。まともに作り直したら、意外と苦労しました。

一言で言えば、ニコニコ動画の動画を保存するアプリケーションです。保存形式が選べるのが売りで、iPhone用に保存してiTunesに追加にチェックを入れれば、全自動で動画をiPhoneに入れることもできます。

ちなみに、それほどバージョンアップとか考えていませんので、このままベータで終わる可能性が高いです。あと、そろそろニコニコ動画の仕様が大幅に変わったりするんじゃないかと、勝手に妄想してます。