Fork me on GitHub

2009/12/31

はてな
最近、GoのGTKバインディングを作ってるのだけど、先日ようやく簡易twitterクライアントを作れるまでに至ったのだが、そのtwitterクライアントの「タイムライン更新」ボタンを押した時に画面をブロックさせずに画面を更新する方法を考えてた。
Perl-GTKならばCoroを使ってこうするだろうか。
use strict;
use warnings;
use Gtk2 '-init', '-threads-init';
use AnyEvent;
use Coro;
use Coro::Timer;

my $window = Gtk2::Window->new('toplevel');
$window->signal_connect(
    destroy => sub {
        exit;
    }
);
my $vbox = Gtk2::VBox->new;

my $button = Gtk2::Button->new('click me');
$button->signal_connect(
    clicked => sub {
        $button->set_sensitive(0);
        async {
            Coro::Timer::sleep 3;
            Gtk2::Gdk::Threads->enter;
            $button->set_sensitive(1);
            Gtk2::Gdk::Threads->leave;
        };
    }
);
$vbox->add($button);

$window->add($vbox);
$window->show_all;

Gtk2->main;
ここでCoro::Timer::sleepしているのは、生のsleepを使うとCoroがスケジュール割り当てとして介入出来なくなるのを回避する為。非同期で動作はするものの、スレッド動作になるので「Gtk2::Threads->enter」と「Gtk2::Threads->leave」でUIに関連する処理をブロックしなければなりません。つまり例えばボタン押下時の処理が5段階あり、その都度UIの何かしらを変更しなければならない場合、「Gtk2::Threads->enter」と「Gtk2::Threads->leave」でそれぞれブロックしなければならない事になります。
平たく言うと、スレッドを使う予定の無かったUI処理を、スレッド対応にするには多少のコード修正が必要になるという事です。
今回、goで作った簡易twitterクライアントの「タイムライン更新」ボタンを非同期対応するのに費やしたコードはたった2行。元のコード
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        button.SetSensitive(false);
        if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
            // タイムライン取得、表示処理
        }
        button.SetSensitive(true);
    }, nil);
となっていたコードに「go」で実行されるブロックを追加しただけ。
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        button.SetSensitive(false);
        go func() {
            if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
                // タイムライン取得、表示処理
            }
            button.SetSensitive(true);
        }();
    }, nil);
何が言いたいのかというと、groutineはgo組み込みであって、Coroがsleepやhttp_getを置き換えようとしている(CORE::GLOBAL上書きもあるけど)物とは違い、言語として非同期をサポートしているという事。もちろんgoにおいてもボタンクリック時にsleepを呼ぶとUIはブロックされるのだけど、実際sleepなんて使わないし、そもそもgoroutineはスレッドじゃない。
そう、goroutineはスレッドじゃない。
A Tutorial for the Go Programming Language

Go has its own model of process/threads/light-weight processes/coroutines, so to avoid notational confusion we call concurrently executing computations in Go goroutines.

http://golang.org/doc/go_tutorial.html
実際にはgoが内部でscheduleを行って実行する処理連鎖で実現している。なのでgtkを呼び出す場合においてもgdk_threads_enter/gdk_threads_leaveを呼び出す必要もない。なぜなら並列とは言えど同時実行ではないのでコンテキストスイッチも意識しなくて良い。
故に上記の様な 「go func(){}()」ブロックだけで非同期処理が書けるという訳。例えばcursesでCUIアプリを書いても画面が崩れる事は無いだろう(未確認)。

UIの開発に有利な言語にはある程度、共通する物があると思う。
  • 匿名関数をコールバックとして扱える
  • サードパーティでも良いので非同期処理の仕組みがある
  • GCが効く
この内、2個か3個あてはまるとUIを扱う上で有利になると思う。Javascriptはこの点(組み込みで非同期が扱える:というかそういうものしか扱えない?)素晴らしいし、jQueryを使えばAjax/UIとして作り手側はかなりの恩恵が得られる。Perlは「sub {}」で匿名関数が使えるがので優秀だが非同期となるとスレッドに頼らざるを得ないのが難点。rubyも同様。pythonは匿名関数が使えないのが少し悲しい(でもpygtkは好きだ)。結局の所、メインループと非同期処理をどう扱うかがネックになっている。例えば各言語でCOMを扱う場合、STA(Single Thread Apartment)しか対応していないCOMは、言語側でメインループをグルグル回すしかない。まぁ言語として用途が違うんだ(JavascriptはそもそもUI記述言語だろ...とか)から、比べちゃならんだろ...とも言えるし。
goにおいては上記3点あてはまるが、デストラクタが無いのが一番の難点。
これからのUIはどんどん非同期な物になって行くんだろうな。

10年程前のUIと言えば、非同期なUIはあまり見られなかった。VB6においても定期的に「DoEvents」を実行してプログレスバーを更新するという、なんちゃって非同期処理が横行していた頃だ。最近はVB.NETでコンポーネント自身がスレッドサポート(実際にはデリゲート)しているので簡単に非同期処理が書ける様になったが、これら全てにおいてもやはり
非同期対応するつもりの無かったUI処理を、非同期対応にするには多少のコード修正が必要になる
には違い無いし、その点でgoには期待している。
Posted at 00:49 in ソフトウェア::lang::go | WriteBacks (1)
Tagged as: golang, gtk, ui
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2009/12/22

はてな
Go binding for GTK GoのGTKバインディングを現在作業中なのですが、ようやく簡単なTwitterクライアントくらいならば作れる程度に出来上がってきたのでご報告。
mattn's go-gtk at master - GitHub

Go binding for GTK

http://github.com/mattn/go-gtk
イベントハンドラがJavaScriptっぽく書けるので、結構見通し良いコードになります。
package main

import "gtk"
import "gdk-pixbuf"
import "unsafe"
import "http"
import "json"
import "io"
import "os"
import "strconv"
import "strings"
import "reflect"

func url2pixbuf(url string) *gdkpixbuf.GdkPixbuf {
    if r, _, err := http.Get(url); err == nil {
        n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
        t := r.GetHeader("Content-Type");
        b := make([]byte, n);
        io.ReadFull(r.Body, b);
        var loader *gdkpixbuf.GdkPixbufLoader;
        if strings.Index(t, "jpeg") >= 0 {
            loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/jpeg");
        } else {
            loader, _ = gdkpixbuf.PixbufLoaderWithMimeType("image/png");
        }
        loader.SetSize(24, 24);
        loader.Write(b);
        loader.Close();
        return loader.GetPixbuf();
    }
    return nil;
}

func main() {
    gtk.Init(&os.Args);
    window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
    window.SetTitle("Twitter!");
    window.Connect("destroy", func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        gtk.MainQuit();
    }, nil);

    vbox := gtk.VBox(false, 1);

    scrolledwin := gtk.ScrolledWindow(nil, nil);
    textview := gtk.TextView();
    textview.SetEditable(false);
    textview.SetCursorVisible(false);
    scrolledwin.Add(textview);
    vbox.Add(scrolledwin);

    buffer := textview.GetBuffer();

    tag := buffer.CreateTag("blue", map[string] string {
        "foreground": "#0000FF", "weight": "700" });
    button := gtk.ButtonWithLabel("Update Timeline");
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        if r, _, err := http.Get("http://twitter.com/statuses/public_timeline.json"); err == nil {
            n, _ := strconv.Atoi64(r.GetHeader("Content-Length"));
            b := make([]byte, n);
            io.ReadFull(r.Body, b);
            j, _ := json.Decode(string(b));
            arr := reflect.NewValue(j).(*reflect.SliceValue);
            for i := 0; i < arr.Len(); i++ {
                data := arr.Elem(i).(*reflect.InterfaceValue).Elem().(*reflect.MapValue);
                icon := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("profile_image_url")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                var iter gtk.GtkTextIter;
                buffer.GetStartIter(&iter);
                buffer.InsertPixbuf(&iter, url2pixbuf(icon));
                name := data.Elem(reflect.NewValue("user")).(*reflect.InterfaceValue).Elem().(*reflect.MapValue).Elem(reflect.NewValue("screen_name")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                text := data.Elem(reflect.NewValue("text")).(*reflect.InterfaceValue).Elem().(*reflect.StringValue).Get();
                buffer.Insert(&iter, " ");
                buffer.InsertWithTag(&iter, name, tag);
                buffer.Insert(&iter, ":" + text + "\n");
            }  
        }
    }, nil);
    vbox.PackEnd(button, false, false, 0);

    window.Add(vbox);
    window.SetSizeRequest(300, 400);
    window.ShowAll();
    gtk.Main();
}
実行すると以下の様な画面になります。
go-gtk-twitter-client
まだ、完成じゃないです。がんばります!
Posted at 00:26 in ソフトウェア::lang::go | WriteBacks (0)
Tagged as: golang, twitter
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2009/12/04

はてな
GtkTreeModelとか面倒くさい部分に差し掛かってます。でもこれないとコンボボックスもツリーもリストも作れないんすよね。
go-gtk-20091204
いまんところこんな感じ。
イベントクロージャが内包出来てJavaScriptを書いているかの様にGTKプログラミングが出来ています。
    //--------------------------------------------------------
    // GtkButton
    //--------------------------------------------------------
    button := gtk.ButtonWithLabel("Button with label");
    button.Clicked(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
        print("button clicked: ", button.GetLabel(), "\n");
        dialog := gtk.MessageDialog(
            button.GetTopLevelAsWindow(),
            gtk.GTK_DIALOG_MODAL,
            gtk.GTK_MESSAGE_INFO,
            gtk.GTK_BUTTONS_OK,
            entry.GetText()
        );
        dialog.Response(func(w *gtk.GtkWidget, args []unsafe.Pointer) {
            println("Dialog OK!")
        }, nil);
        dialog.Run();
        dialog.Destroy();
    }, nil);
    buttons.Add(button);
完成はまだまだ先です。頑張ります。
Posted at 00:30 in ソフトウェア::lang::go | WriteBacks (0)
Tagged as: go, golang, gtk
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark

2009/11/28

はてな
やっぱり新しい言語が出来たらGUIだよね!
って事でGTKバインディング作ります!

mattn's go-gtk at master - GitHub

gtk extension for go

http://github.com/mattn/go-gtk
Goでは継承が使えないので、例えばGtkBoxを継承したGtkVBox/GtkHBox、GtkWidgetを継承したGtkWindow/GtkButton/GtkLabel...が上手く表現出来ません。
出来ないというか、GtkButtonにGtkWidgetと同じメソッドを生やそうと思うと、同じコードを書かなければなりません。
色々と模索した結果、gtk.Window()やgtk.Button()で返すのは常にGtkWidgetとし、GtkWindowやGtkButton独自のメソッドを呼び出したい場合には
(&gtk.GtkWindow{window.Widget}).SetTitle("GTK Go!");
こういう風に型変換して貰う事にしました。まぁコンポジションぽい物?
C言語バージョンにおいても同じ処理をする時には
gtk_window_set_title(GTK_WINDOW(w), "GTK Go!");
とする訳だし、似てると言えば似てる訳だし...
要は、Widget毎にGtkWidgetからGtkXXXへの変換関数なんか作ってらんねぇよ。バカ!
という訳です。

現状、コールバック(gtk_signal_connect)も動作しています。少しハマった理由としては、goroutineを勝手にスレッドだと思っていたので、スレッド間通信を想定してpipeのコードを書いていましたが、よく考えたらscheduled coroutineだし、自分のwrite pipeを自分でreadしてどうするよ...って気付いたのです。
結局変数ポーリングに変えて現状上手く動いています。
GoでGTKのプログラミングをすると、以下の様なコードになります。
package main

import (
  "os";
  "gtk";
  "unsafe";
)

func main() {
    gtk.Init(&os.Args);
    window := gtk.Window(gtk.GTK_WINDOW_TOPLEVEL);
    (&gtk.GtkWindow{window.Widget}).SetTitle("GTK Go!");

    vbox := gtk.VBox(0, 1);

    label := gtk.Label("ハローワールド");
    (&gtk.GtkBox{vbox.Widget}).PackStart(label, 0, 1, 0);

    entry := gtk.Entry();
    (&gtk.GtkEntry{entry.Widget}).SetText("入力エリア!");
    vbox.Add(entry);

    button := gtk.ButtonWithLabel("こんにちわ!こんにちわ!");
    button.Connect("clicked", func(widget *gtk.GtkWidget, data unsafe.Pointer){
        println("button clicked");
        println((&gtk.GtkButton{button.Widget}).GetLabel());
    }, nil);
    vbox.Add(button);

    window.Add(vbox);

    window.ShowAll();
    gtk.Main();
}
そして実行画面。
go-gtk
まだ、GtkWindow/GtkButton/GtkLabel/GtkVBox/GtkHBox/GtkEntryのそれぞれ一部のメソッドしか動いていませんが、宜しければ遊んでみて下さい。
そしてもし「おおし!おいらも作るお!」って方が居られたら、ぜひgithubでforkして下さい。

Posted at 00:55 in ソフトウェア::lang::go | WriteBacks (0)
Tagged as: go, golang, GTK
Bookmarks: このエントリーのtweets add to hatena add to hatena | add to delicious.com | add to livedoor.clip add to livedoor.clip | add to buzzurl add to buzzurl | add to fc2bookmark add to fc2bookmark | add to Yahoo Bookmark add to Yahoo Bookmark | add to Pookmark add to Pookmark