gmtime/localtimeと閏秒の面倒くさい関係

Posted on

お久しぶりです、ψ(プサイ)です。

今日は今やってるプロジェクトとは一切関係なく、gmtime/localtimeの実装が気になったので、読んで分かったことをまとめておきます。

軽くおさらいですが、gmtime/localtimeは、unixtime(1970年1月1 00:00:00 AM UTCからの経過秒数)をtm構造体に入った年・月・日・時間・分・秒に変換してくれるCの標準ライブラリ関数です。unixtimeのままでは人間が到底読めないので、表示する時などに必要な関数です。

思えば、このgmtime/localtimeの実装は結構めんどくさそうです。すごくざっくりで良いなら、

年=(int)1970+(unixtime/(24*365*3600))

みたいな感じで簡単に算出できますが、これでは全然駄目な点がたくさんあります。

  • うるう年は?
  • うるう秒は?
  • (localtimeの時)時差は?

どうやるか自体は大体想像できると思いますが、折角なので読んでみました。そんな具体的な話には興味ないという方は飛ばして下さい。

newlibのgmtimeを読んでみよう

newlibは標準Cライブラリの軽量な実装です。組み込みの分野でよく使われていて、コンパクトなのが特徴だそうです。

ためしに、このライブラリでのgmtimeを読んでみます。newlib/libc/time以下のフォルダにあります。

gmtime.c

struct tm *
_DEFUN (gmtime, (tim_p),
	_CONST time_t * tim_p)
{
  _REENT_CHECK_TM(_REENT);
  return gmtime_r (tim_p, (struct tm *)_REENT_TM(_REENT));
}

gmtimeはグローバル変数のtm構造体を返すのですが、ユーザの指定したtm構造体に値を格納するgmtime_rという関数もあるので、それに処理を投げているようです。その先は…

gmtime_r.c

struct tm *
_DEFUN (gmtime_r, (tim_p, res),
	_CONST time_t * tim_p _AND
	struct tm *res)
{
  return (_mktm_r (tim_p, res, 1));
}

localtimeと処理を共用しているようで、その共通の関数に投げてます。3つ目の引数の「1」は、gmtを使うという意味のようです。

mktime_r.c

struct tm *
_DEFUN (_mktm_r, (tim_p, res, is_gmtime),
	_CONST time_t * tim_p _AND
	struct tm *res _AND
	int is_gmtime)
{
  long days, rem;
  time_t lcltime;
  int y;
  int yleap;
  _CONST int *ip;
   __tzinfo_type *tz = __gettzinfo ();
  /* base decision about std/dst time on current time */
  lcltime = *tim_p;

この関数は結構長いので、ちょっとずつ読んで行きましょう。まずはローカル変数の定義です。昔のCの規格でも動くように、ローカル変数は必ず関数(もっというとスコープ)の一番最初で宣言されているみたいです。std/dstという文字列が見えますね…サマータイムと通常時間の事です。私は最初、すっかり存在を忘れてました。。。もしオレオレ実装を作っていたらすっかり忘れていた所です。

  days = ( (long)lcltime) / SECSPERDAY;
  rem = ( (long)lcltime) % SECSPERDAY;
  while (rem < 0) 
    {
      rem += SECSPERDAY;
      --days;
    }
  while (rem >= SECSPERDAY)
    {
      rem -= SECSPERDAY;
      ++days;
    }

1970年の元旦からの日数と、残りの秒数を求めているみたいです。

if文の分岐先が中々不思議ですね。あまりを求めるときのあまりは、法(割る数)の符号に合わせて決めるはずだったと思うので、マイナスになることは無いし、法を超える事も無いはずだと思うのですが、もしかするとCの仕様ではそう決まってないのかも…?? よくわかりません(

  /* compute hour, min, and sec */  
  res->tm_hour = (int) (rem / SECSPERHOUR);
  rem %= SECSPERHOUR;
  res->tm_min = (int) (rem / SECSPERMIN);
  res->tm_sec = (int) (rem % SECSPERMIN);
  /* compute day of week */
  if ( (res->tm_wday = ( (EPOCH_WDAY + days) % DAYSPERWEEK)) < 0)
    res->tm_wday += DAYSPERWEEK;

時間・分・秒・曜日は特に何も考えなくても機械的に決定できるので、問題ないです。

 /* compute year & day of year */
  y = EPOCH_YEAR;
  if (days >= 0)
    {
      for (;;)
	{
	  yleap = isleap(y);
	  if (days < year_lengths[yleap])
	    break;
	  y++;
	  days -= year_lengths[yleap];
	}
    }
  else
    {
      do
	{
	  --y;
	  yleap = isleap(y);
	  days += year_lengths[yleap];
	} while (days < 0);
    }
  res->tm_year = y - YEAR_BASE;
  res->tm_yday = days;

少し長いですが、1,970年の元旦からの日数から、年を求めています。isleap関数はうるう年の時は1を、そうでないときは0を返す関数で、year_lengthsは{365,366}の配列です。このふたつを組み合わせるとその年の日数をうるう年を考慮した上で知ることができます。

基本的には1年ずつ年数を調べて、最初に計算した1970年からの経過時間であるdaysから引いていって、1年に満たなくなるまでループを回す、っていう、素朴な実装です。

最初に分岐がありますが、相変わらず、days<0になるのはいまいちどういうケースなのか想像できないです。。。

そして最後の-YEAR_BASEですが、tm構造体のtm_yearの値は1900年からの年数を表す事になってるので、これで引いています。

  ip = mon_lengths[yleap];
  for (res->tm_mon = 0; days >= ip[res->tm_mon]; ++res->tm_mon)
    days -= ip[res->tm_mon];
  res->tm_mday = days + 1;

ここまでで、daysは1年のうちでの日数を表しているので、年でやったのと同じように何月なのかを計算しています。mon_lengthsは…

static _CONST int mon_lengths[2][MONSPERYEAR] = {
  {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},
  {31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
} ;

こんな感じの配列で、うるう年を考慮した月の日数の一覧です。

最後の+1ですが、tm_mdayは1から始まるきまりになってるので、その調整です。

そして最後の最後にgmtimeとlocaltimeでの分岐処理が入ります。

  if (!is_gmtime)
//省略
  else
    res->tm_isdst = 0;

gmtの時はtm_isdts=0に設定してるだけで簡単です。gmtにはサマータイムとかは無く、またunixtimeはGMTの1970年の元旦からの秒数なので時差もありません。

localtimeの時の処理はかなり骨がおれます。if文の中身を読んで行きましょう。

      if (_daylight)
	{
	  if (y == tz->__tzyear || __tzcalc_limits (y))
	    res->tm_isdst = (tz->__tznorth 
			     ? (*tim_p >= tz->__tzrule[0].change 
				&& *tim_p < tz->__tzrule[1].change)
			     : (*tim_p >= tz->__tzrule[0].change 
				|| *tim_p < tz->__tzrule[1].change));
	  else
	    res->tm_isdst = -1;
	}
      else
	res->tm_isdst = 0;

_daylightはグローバル変数で、サマータイムを使う可能性があるときにセットされます。

res->tm_isdstの判定なのですが、すいません、よくわかりません。__tznorthがfalseの時、条件が反転するみたいです。謎い…。サマータイム、嫌すぎる

→コメント覧参照ですが、これはどうやら南半球でのサマータイム(10月〜翌年3月ぐらい)の判定と、北半球でのそれとでの差のようです。これも最初まったく考えが至りませんでした。…北半球から出たことないです…。

      offset = (res->tm_isdst == 1 
		  ? tz->__tzrule[1].offset 
		  : tz->__tzrule[0].offset);
      hours = (int) (offset / SECSPERHOUR);
      offset = offset % SECSPERHOUR;

      mins = (int) (offset / SECSPERMIN);
      secs = (int) (offset % SECSPERMIN);
      res->tm_sec -= secs;
      res->tm_min -= mins;
      res->tm_hour -= hours;

サマータイムを考慮しつつ、時差を取得して、その分を足したり引いたりしている処理です。この時、機械的に足すので、時間が24を超えたり、マイナスになるかもしれません。ので、次で調整します。

      if (res->tm_sec >= SECSPERMIN)
	{
	  res->tm_min += 1;
	  res->tm_sec -= SECSPERMIN;
	}
      else if (res->tm_sec < 0)
	{
	  res->tm_min -= 1;
	  res->tm_sec += SECSPERMIN;
	}
      if (res->tm_min >= MINSPERHOUR)
	{
	  res->tm_hour += 1;
	  res->tm_min -= MINSPERHOUR;
	}
      else if (res->tm_min < 0)
	{
	  res->tm_hour -= 1;
	  res->tm_min += MINSPERHOUR;
	}
      if (res->tm_hour >= HOURSPERDAY)
	{
	  ++res->tm_yday;
	  ++res->tm_wday;
	  if (res->tm_wday > 6)
	    res->tm_wday = 0;
	  ++res->tm_mday;
	  res->tm_hour -= HOURSPERDAY;
	  if (res->tm_mday > ip[res->tm_mon])
	    {
	      res->tm_mday -= ip[res->tm_mon];
	      res->tm_mon += 1;
	      if (res->tm_mon == 12)
		{
		  res->tm_mon = 0;
		  res->tm_year += 1;
		  res->tm_yday = 0;
		}
	    }
	}
       else if (res->tm_hour < 0)
	{
	  res->tm_yday -= 1;
	  res->tm_wday -= 1;
	  if (res->tm_wday < 0)
	    res->tm_wday = 6;
	  res->tm_mday -= 1;
	  res->tm_hour += 24;
	  if (res->tm_mday == 0)
	    {
	      res->tm_mon -= 1;
	      if (res->tm_mon < 0)
		{
		  res->tm_mon = 11;
		  res->tm_year -= 1;
		  res->tm_yday = 364 + isleap(res->tm_year + 1900);
		}
	      res->tm_mday = ip[res->tm_mon];
	    }
	}

かなり長いですが、これはかなり単純で、例えば分が60を超えていたら調整のためにその上の時間を足して、…という処理を延々年まで行なっていってるだけです。

以上です。いかがだったでしょうか。一つ一つの処理はそこまで複雑ではないのですが、やはり長いですし、細かい例外もちゃんとフォローしてありましたね。ライブラリは偉大です。

閏秒は????????

さて、ここまでで気づきませんでしたか。

閏秒という概念が登場しなかった、という事実を。

そう、実はposix標準ではgmtime/localtimeは閏秒を考慮しないらしいですえっそれ普通に困るのでは

が、しかし、glibcのソースを眺めてみると、めんどくさくてあまり読んでないのですが、次の内部関数を見ると、どうやらうるう秒を考慮していそうです。

time/tzfile.c

void
__tzfile_compute (time_t timer, int use_localtime,
		  long int *leap_correct, int *leap_hit,
		  struct tm *tp)
  /* Apply its correction.  */
  *leap_correct = leaps[i].change;
  if (timer == leaps[i].transition && /* Exactly at the transition time.  */
      ( (i == 0 && leaps[i].change > 0) ||
       leaps[i].change > leaps[i - 1].change))
    {
      *leap_hit = 1;
      while (i > 0
	     && leaps[i].transition == leaps[i - 1].transition + 1
	     && leaps[i].change == leaps[i - 1].change + 1)
	{
	  ++*leap_hit;
	  --i;
	}
    }
}

むー、なんか複雑になってきました。

ということは:同じ時刻、違うunixtime?

閏秒を考慮する場合としない場合で、当然時刻の計算が違ってくるはずです。glibはうるう秒を考慮に入れてるっぽいので、適当に決めたunixtime=1354320000で、差をチェックしてみましょう。

ソースはこちらです。

#include 
#include 
int main()
{
	struct tm* result;
	time_t constTime = 1354320000L;
	result = gmtime(&constTime);
	printf("%04d/%02d/%02d %02d:%02d:%02d\n",
			result->tm_year+1900,
			result->tm_mon+1, //1月=0
			result->tm_mday,
			result->tm_hour,
			result->tm_min,
			result->tm_sec);
	return 0;
}

まずはnewlibでやってみます。なんかprintfがうまく動かないので、gdbでステップ実行しました(情弱)

結果がこんな感じ

# gdb ./gmtime_test
(略)
(gdb) print *result //resultの中身を表示するよ
$2 = {tm_sec = 0, tm_min = 0, tm_hour = 0, tm_mday = 1, tm_mon = 11, 
  tm_year = 112, tm_wday = 6, tm_yday = 335, tm_isdst = 0}
# -> 2012/12/01 00:00:00

で、Fedora 17のデフォルトのglibc(2.15)とリンクした方でテストすると、

# ./gmtime_test
2012/12/01 00:00:00

あれれ???一緒ですね…

さらに泥沼が待っているとはこの時まだ私は知らないのであった…

タイムゾーンの設定が、2つある

glibcのところで、うるう秒を考慮にいれているようなソースがありましたね。うるう秒というのはうるう年とは違って、ランダムに挿入されます。ので、どこかに「うるう秒の一覧」を記録しておかないと、正しく計算することができません。

「うるう秒一覧」が何処にあるかというと、fedora 17の場合、/usr/share/zoneinfoというフォルダの中にあり…

Africa	    Chile    GB		Indian	     Mideast   posixrules  US
America     CST6CDT  GB-Eire	Iran	     MST       PRC	   UTC
Antarctica  Cuba     GMT	iso3166.tab  MST7MDT   PST8PDT	   WET
Arctic	    EET      GMT0	Israel	     Navajo    right	   W-SU
Asia	    Egypt    GMT-0	Jamaica      NZ        ROC	   zone.tab
Atlantic    Eire     GMT+0	Japan	     NZ-CHAT   ROK	   Zulu
Australia   EST      Greenwich	Kwajalein    Pacific   Singapore
Brazil	    EST5EDT  Hongkong	Libya	     Poland    Turkey
Canada	    Etc      HST	MET	     Portugal  UCT
CET	    Europe   Iceland	Mexico	     posix     Universal

こんな感じでたくさん入ってます。この中には時差その他のデータが入っていて、うるう秒のデータも含めることができるそうです。詳しくは:tz database – Wikipedia

これを日本について見てみると…

% zdump Japan -v
Japan  -9223372036854775808 = NULL
Japan  -9223372036854689408 = NULL
Japan  Sat Dec 31 14:59:59 1887 UTC = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
Japan  Sat Dec 31 15:00:00 1887 UTC = Sun Jan  1 00:00:00 1888 JST isdst=0 gmtoff=32400
(略)
Japan  Fri Sep  7 16:00:00 1951 UTC = Sat Sep  8 01:00:00 1951 JST isdst=0 gmtoff=32400
Japan  9223372036854689407 = NULL
Japan  9223372036854775807 = NULL

となってて、2012年の7月にあった最新のうるう秒などのデータが入ってるようには見えません。

実はright/Japanというファイルもあって、こちらを見てみると…

% zdump right/Japan -v
right/Japan  -9223372036854775808 = NULL
right/Japan  -9223372036854689408 = NULL
right/Japan  Sat Dec 31 14:59:59 1887 UTC = Sun Jan  1 00:18:58 1888 LMT isdst=0 gmtoff=33539
(略)
right/Japan  Fri Sep  7 16:00:00 1951 UTC = Sat Sep  8 01:00:00 1951 JST isdst=0 gmtoff=32400
(略)
right/Japan  Sat Jun 30 23:59:60 2012 UTC = Sun Jul  1 08:59:60 2012 JST isdst=0 gmtoff=32400

2012年7月の表記があるので、ちゃんとうるう秒が反映されているようだ、とわかります。

タイムゾーンを正しい方に設定してみる

やっと正しい方が分かった所で、この正しい方にタイムゾーンを設定してみましょう。TZ環境変数に正しい方のファイルへのパスを指定すると、どのタイムゾーンのファイルを使うか設定することができます。

glibcの方から先にしてみましょう。

% TZ=/usr/share/zoneinfo/right/Japan ./gmtime_test  
2012/11/30 23:59:35

ふぅ。。。。さっきは12/01でしたが、うるう秒を考慮した結果25秒ほど過去に戻って11月になってしまいました。

一応、うるう秒を考慮にいれていないはずであるnewlibも試して見ました。

% TZ=/usr/share/zoneinfo/right/Japan gdb ./gmtime_test_newlib
(gdb) print *result
$1 = {tm_sec = 0, tm_min = 0, tm_hour = 0, tm_mday = 1, tm_mon = 11, 
  tm_year = 112, tm_wday = 6, tm_yday = 335, tm_isdst = 0}
# -> 2012/12/01 00:00:00

おおお、やっと差が出ましたね!つ、疲れた…。まとめましょう。

ちなみに毎度毎度TZ環境変数にいれなくても、システム全体で正しい方を使うようにもできます

まとめ

  • gmtime/localtime、簡単そうな処理ですがサマータイムとかうるう秒を考えたらやっぱりライブラリ使いましょう
  • gmtime/localtimeはposix標準ではうるう秒を考慮しません
  • glibcでは考慮しますが、デフォルトでは無効化されていて、タイムゾーンファイルの指定を変更することで「正しい」時間を表示するように出来ます。
  • する場合としない場合で、2012年現在25秒も違います。

なんていうか、…大変ですね(しみじみ)。一番の問題はやっぱりposix標準で「うるう秒を考慮しない」としてる点だと思います。今更変えられないのが非常に…悔しいですね…。

SElinuxはミリしらだけどアクセス許可を追加してみた

Posted on

 SELinuxってたまに聞くけど中身は全然知らなかったのですが、今回Fedora 17のChromeでNativeClientが動かなかったので設定することにしました。

SELinuxって何じゃらホイ

 SELinuxって名前は聞いたことあって、セキュリティ系の機能らしいというのは知っているのですが、それ以上のことはよく知りませんでした…。

 というわけで、調べてみました。第1回 セキュアOS機能「SELinux」の基本的な仕組み。ちょっと古い記事ですが、まあ問題ないでしょう。

私の理解したところによると。

 今までの普通のLinuxには、chmodで設定するような、ユーザーやグループを用いたアクセス制御がありますが、この場合は万が一root権限で動くプロセスを奪取されるとすべてにアクセスし放題になってしまってよろしくありません。というわけで、SELinuxではそれとは別にプログラム単位で色々な制限を、ファイルへのアクセス制限だけでなく、もっと色々な行動への制限が掛けられるようになっている…ということみたいです。

 …間違ってたらゴメンネ。例えばhttpdにはネットワーク接続の権限が必要だけど、passwdへのアクセスは要らないから、これを制限しよう、という発想だと理解しました。これをTE(Type-Enforcement)と言うそうです。

 RABCっていう別の仕組みもあるそうですが、今回引っかかったのはTEなのでこちらに話を絞ります

Fedora 17でGoogle ChromeのNative Clientが動かない

 さて。今回引っかかったのはGoogle ChromeのNative Clientです。SDKに入っているexampleを試しに動かしてみましたが…こんな感じで動いてくれません。

 20120825_01.png

 うまく行くとこうなるはずなんですけどー!

20120825_02.png

 で、まずはGoogleで検索してみたのですが、どうやら今回のSELinuxが問題で動かないようになっているようだ、という事が分かりました。

まずは全体を無効にしてみる

 まずは、SELinux全体を無効にしてみました。

% sudo setenforce Permissive

こうすると全体のSELinuxが無効になります。で、この状態でさっきのExampleが綺麗に表示されたので、SELinuxが問題だと分かります。

あとで有効に戻すには次のコマンドを使って下さい。無効にしたままは流石にどうかと思うので…

% sudo setenforce Enforcing

さらに細かい粒度で有効か無効かを調べる

 とりあえずSELinuxの問題であることがわかったので、Chromeのどういうモジュールなのかなあというのを調べるために、プロセスのドメインごとに有効無効を切り替えられるツールを使います

% sudo yum install policycoreutils-gui

20120825_03.png

どうやらChromeにはsandboxとsandbox_naclという2つのドメインが割り当てられているようです(この割り当て自体はFedoraの中の人がやってるようです)。どう考えても後者が怪しいので、Permissiveボタンを押してnaclの方をPermissiveモード(SELinux無効)にしてみると…やっぱり動作しました。これが問題みたいです。

AuditのログからSELinux用のモジュールを作る

 SELinuxを一時的に解除して動くようにしましたが、SELinuxはアクセス制御は行なっていませんが、ログを、Auditdというデーモンのログに記録しています。Fedoraの場合は/var/log/audit/audit.logに入っております。

 このAuditというデーモン、syslogdみたいな感じらしいです。でもSyslogdよりリッチなシステムらしいです。詳しくはLinux Audit 〜OSレベルの監査ログ徹底活用〜が参考になります。

 このログの中で、chrome nacl…といったキーワードを調べていくと、chrome_sandbox_nacl_tというのがnacl関連だと思われるので、それでgrepします。

%sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log

 さらにこれでログを抽出した上で、audit2allowというプログラムを用いて、SELinuxでの設定ファイル、モジュールを作る事ができます。

%sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log | audit2allow -a -l r
require {
type chrome_sandbox_nacl_t;
type chrome_sandbox_t;
class unix_dgram_socket { read write };
}
#============= chrome_sandbox_nacl_t ==============
allow chrome_sandbox_nacl_t chrome_sandbox_t:unix_dgram_socket { read write };

さらに、-M module名とすると、モジュールのコンパイルもしてくれます。

% sudo grep ":chrome_sandbox_nacl_t:" /var/log/audit/audit.log | audit2allow -a -l -r -M chrome_sandbox_nacl

 で、最後にsemodule -iを使ってインストールします。

% sudo module -i chrome_sandbox_nacl.pp

 以上です。さっきのGUIでnaclだけPermissiveにしていたのを元に戻してEnforcingにしても、ちゃんとnaclが動作するようになるはずです!

モジュールを作らなくても大丈夫な場合もある

 今回はモジュールを作りましたが、正直言って結構大工事でしたね。それ以外にも、getsebool/setseboolで設定できるフラグや、ファイルのタイプを変更する方法でもSELinuxの制限を何とか出来たりもするようです。が、今回は駄目でした。

SELinuxは分かりづらい。

 SELinuxは他のLinuxのデーモンなどと違い、設定ファイルがバイナリにコンパイルされた状態で保存されています。(/etc/selinuc/targetd/以下)。速度が重要な所ですから、多分そのためだとは思うのですが、いまいち分かりづらいです。

 上記で使ったaudit2allowの代わりに、audit2whyというのを使うと制限された理由を教えてくれたりもするようです。

type=AVC msg=audit(1345883976.560:36379): avc:  denied  { read write } for  pid=15643 comm="nacl_helper_boo" path="socket:[667837]" dev="sockfs" ino=667837 scontext=unconfined_u:unconfined_r:chrome_sandbox_nacl_t:s0-s0:c0.c1023 tcontext=unconfined_u:unconfined_r:chrome_sandbox_t:s0-s0:c0.c1023 tclass=unix_dgram_socket
Was caused by:
Missing type enforcement (TE) allow rule.
You can use audit2allow to generate a loadable module to allow this access.

Missingって…。Fedoraの中の人のミスじゃないかなあ、これ…。

参考にしたところ

差分アップデートを実装したさきゅばす2.0b3を公開しました

Posted on

リリースノート

 コメントの表示時間に関するバグの修正と、前回言っていた差分による自動アップデータを搭載したバージョンを公開します。

 自動アップデートを搭載したので、今後のアップデート作業は大分楽になると思います。とはいえ、最初のバイナリのサイズがまた増えてしまったのですが…。

変更履歴

  • コメントの表示時間がue/shitaと通常コメントで逆になっていた問題を修正しました。
  • 自動アップデータを搭載しました(bazaarを利用しています)
  • FFmpegのバイナリを最新のものに更新しました
  • libxml2とx264を更新しました。

ダウンロード

 いつも通りSourceforgeからどうぞ

 なお、zipと7zは内容は同じです。7zのほうがファイルサイズが小さいので7zが解凍できる人はどうぞ。

自動アップデータの使い方

 Wikiに書いておきました

20120824_01.png

 Bazaarの軽量チェックアウトを使っています。従来のSubversionより容量を使わなかったので急遽乗り換えました。

Chromeではメソッドをオブジェクトに直接入れてはいけない!?

Posted on

 短く言うと:アセンブラっぽく書く オブジェクト指向つかったら負け

 前の記事で書いたJavaScriptのファミコンエミュレータ「CycloaJS」を実装する際に使った最適化の手法について紹介します。

関数呼び出し、避けましょう

 まずはこんな関数から〜。

"use strict";
var TIMES = 100000000;
function Klass(){
	this.val = 0;
	this.add = function(val){
		this.val += val;
	};
}
function doBench() {
	var memory = new Uint8Array(1024*1024);
	log("calling func: "+cycloa.probe.measure(function(){
		var obj = new Klass();
		var i = TIMES;
		while(--i){
			obj.add(i&0xfff);
		}
	}));
	log("not calling func: "+cycloa.probe.measure(function(){
		var obj = new Klass();
		var i = TIMES;
		while(--i){
			obj.val += (i&0xfff);
		}
	}));
}

 cycloa.probe.measureっていうのは、実行時間を測ってミリ秒単位で返してくれるだけのユーティリティ関数です。最初のベンチマークでは変数に足し算する処理をクラスメソッドに移譲していますが、次のベンチマークでは移譲せずに直接書いてます。こんな例だと馬鹿馬鹿しいですが、規模が大きくなると前者みたいな方式の方が見通しがよくなったりするんですよね…。

 これを、Chrome21とFirefox14.0.1を用いてログを取ってみました。

Firefox14.0.01

calling func: 657
not calling func: 289
calling func: 772
not calling func: 284
calling func: 768
not calling func: 293

 関数呼び出しを加えただけでかなり遅くなります。C/C++やJavaのような感覚でgetter/setterなどを作ると一気に遅くなってしまいます。

 今回エミュレータでは、CPUのエミュレーション部分がメモリの読み書きを行う際のメソッドを、すべてコードジェネレータを用いてインライン展開しています。

 

  同様に、その他の通常ならば別のメソッドに分けるような処理も、すべて一つの関数内で行うようにしました。erbを用いて埋め込むなどしてできる限り可読性を下げないようにはしているものの、正直見難くなっているのが実情です…。

Chrome21

calling func: 592
not calling func: 592
calling func: 1327
not calling func: 590
calling func: 1035
not calling func: 587
calling func: 1037
not calling func: 585

 Chromeだと、最初の一回目だと差が出ない(インライン展開でも行なっているのでしょうか?)のですが、二回目以降関数呼び出しバージョンが極端に遅くなります。なぜだろと思ってプロファイラを有効にすると、なぜか差がでなくなってしまいます…。なぜなのかはよく分かりません。試しに、

var obj = new Klass();
function doBench() {
var memory = new Uint8Array(1024*1024);
log("calling func: "+cycloa.probe.measure(function(){
	var _obj = obj;
	var i = TIMES;
	while(--i){
		_obj.add(i&0xfff);
	}
}));
log("not calling func: "+cycloa.probe.measure(function(){
	var _obj = obj;
	var i = TIMES;
	while(--i){
		_obj.val += (i&0xfff);
	}
}));
}

 として、オブジェクトの生成を一度だけにしたとしても…

calling func: 575
not calling func: 565
calling func: 890
not calling func: 577
calling func: 881
not calling func: 572

 として、二度目以降やっぱり関数呼び出しバージョンが遅くなってしまいます。なぜでしょう…。

 

ローカル変数、使いましょう

 お次はこのコードです。

  • グローバル変数
  • ローカル変数(var variable = 0;)
  • thisオブジェクトのメンバ変数(this.variable)
  • ローカルオブジェクトのメンバ変数(var obj = 略;obj.variable)
  • thisオブジェクトのメンバオブジェクトのメンバ変数(this.obj.variable)

時間[ms] Chrome21 Firefox14
グローバル変数 302 622
ローカル変数 132 255
this.variable 586 280
var obj;obj.variable 580 280
this.obj.variable 655 292

 ChromeとFirefoxで随分特性が違いますが、できる限りローカル変数を使うのが一番なのは間違いありません。

 定数などはよくグローバル変数としてこのように、擬似名前空間を作った上で保持しておくのが割と綺麗な設計だと思いますが…

var namespace = {}; //ソフトウェア用の擬似名前空間
namespace.INT_MAX=0xffffffff; //32bitIntegerの最大値
namespace.Klass = function(){
	this.method = function(){
		var variable = namespace.INT_MAX; //定数を参照
		for(***){
			variable = (variable+1) & namespace.INT_MAX; //32ビットカウンターとして使う
		}
	};
};

 以上のように複数回定数を使う場合は、

var namespace = {}; //ソフトウェア用の擬似名前空間
namespace.INT_MAX=0xffffffff; //32bitIntegerの最大値
namespace.Klass = function(){
	this.method = function(){
		var tmpINT_MAX = namespace.INT_MAX; //一旦ローカル変数にコピー
		var variable = tmpINT_MAX; //上記のローカル変数の方を参照している
		for(***){
			variable = (variable+1) & tmpINT_MAX; //こちらもローカル変数を参照
		}
	};
};

 このように一旦ローカルに保持しています。…これだけでかなり見づらくなるのでおすすめしません…。

 さらに書き換えられる事の多い変数に関しては、

this.method = function(){
	//一旦ローカルにコピーする
	var variable = this.variable;
	//いっぱい書き換えられる
	variable += this.calc1();
	variable += this.calc2();
	//thisのメンバ変数に戻す
	this.variable = variable;
}

 のようにして、一旦ローカル変数にコピーして最後に再度戻すといった事もしています。書き戻す前にthis.variableにアクセスすると分かりづらいバグになるので非常に注意です。なんてまあ不毛なことを…

 

 さらに、機能ごとにオブジェクトを分割、たとえば今回はエミュレータなのでCPUとサウンドとビデオにクラスを分けて実装してしまうと…

function Video(){
	this.method = new run() {
		//描画
	};
	this.parameter = 0; //何か
}
function CPU(){
	this.method = new run() {
		//CPUの命令実行
	};
	this.parameter = 0; //何か
}
function Emulator(){
	this.videoModule = new Video();
	this.cpuModule = new CPU();
	this.run = function(){
		this.videoModule.run();
		this.cpuModule.run();
	};
}

 このように、this.subModule.run()のように呼びださなくてはいけなくなってしまい、1秒間に数千回呼ぶとコストが馬鹿にならないため…

function Emulator(){
	this.videoModule = new Video();
	this.cpuModule = new CPU();
	this.run = function(){
		//描画処理
		//...
		//CPUの命令実行
		//...
	};
	//変数名の衝突を回避
	this.__video__parameter = 0;
	this.__cpu__parameter = 0;
}

 というようにしてしまっています。こうすると1番目の関数呼び出しのコストも避けられます…が、まずモジュールごとの名前空間が無くなるため、そこに気を付けなければなりません。今回の例ではVideoとCPUのparameterという変数名が衝突しているため、__cpu__と__video__というように接頭辞を付けて区別してますがあまりにも不毛です

JSでファミコンエミュレータ書いてみた:CycloaJS

Posted on

タイトルのまんまです

去年書いて駒場祭で作り方に関する本も出したCycloa」というファミコンエミュレータを、今回JavaScriptに移植してみました。

20120822_01.png

ご好意により、 Denis GrachevさんがZXスペクトラム向けに開発し、ShiruさんとKulorさんがファミコンに移植したAlter Egoというアクションパズルゲームを一緒に配布させていただいています(これはオープンライセンスではありません)。

20120822_02.png

バイナリィランドのように対称性をテーマにしたゲームで、とっても面白いので皆さんやってみてください〜!

JavaScriptのエミュレータはJavaScript NES エミュレータや、jsnesなどが既にありますが、前者は軽量なもののスプライトの再現で一部端折っているところがあり、また後者は通常のPC用エミュレータと同レベルの正確なエミュレーションを行なっているものの、非常に重くミドルレンジクラスのPCでは60FPSのリアルタイムが出ないという問題がありました。

今回のエミュレータでは、通常のPC向けのエミュレーション精度を保ちつつ、どこまで高速化できるのか検証するのを主目的としました。

どれくらい高速化できたのかを確かめてみようというわけで、既存のエミュレータとのベンチマーク取って見ました。動作環境は次の通り。

  • Core2Duo E8400
  • DDR2-800 4GB
  • GeForce 9600GT
  • Fedora 17 x86_64
  • Chrome 21

C++版のオリジナルCycloaと、今回のCycloaJS、JavaScript NES エミュレータとjsnesについて、Google ChromeでFPSリミットを外してどれぐらいの速度が出るのかを調べます。AlterEgoの起動画面でのFPSを比較してみました。

20120822_03.png

  • jsnes: 58.8fps
  • CycloaJS: 169fps
  • Cycloa: 691fps
  • JavaScript NES Emulator: 194fps

jsnesはそもそもこのPCだとリアルタイムで描画できません。JavaScript NES Emulatorより遅いですが、このエミュレータよりスプライトの再現性が高いので、まあ良いのかなと…。流石にC++ネイティブの元のエミュレータには全然勝てません。それでも3〜4倍にまで縮まってるのは、流石Chromeと言ったところでしょうか…

ソースコード

ソースコードはgithubで公開中ですC++版も同様に公開中

具体的な手法に関しては

また後日ということで…とりあえず、もうJavaScriptはあんまり書きたくないですね…。HTML5こわい。。。

リーダブルコード:あたりまえのこと、でもだいじ

Posted on

 なんかネット上を見てたら評判が良いので買ってしまいました、「リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック」。

 具体例としょーもないイラスト(褒め言葉)を交えながら読みやすくてシンプルなコードを書くための軽い読み物です。ラノベより軽いです。

アタリマエのことしか書いてない

 本当に当たり前のことしか書いてない本です。ちゃんと説明する名前の変数を付ける、tmpとかの抽象的な名前は基本的に避けるべきだけど、スコープが短くて本当にtmpならそれはそれでアリ、一度に一つのことしかしないコードにしないと頭がこんがらがる、短絡評価とかを使った「頭いいコード」は逆に読みづらい、…突拍子のある事は一切書いてなくて、どれも本当に「まあそうだよねぇ」って言いながら読み進められます。…でもその「当たり前」の事が出来ないんですよねぇorz

 同様の「当たり前の事しか書いてない」っていう理由で「達人プログラマ」は読む必要無いんじゃない?って前書きました。達人プログラマは当時としては先進的(?)な内容を書いていたので今となってはその説明は要らないかな…という感じなのですが、こちらは各々の持ってる「読みやすいソースコードの基準、目安」みたいなものを改めて文章とソースコードを使って説明しなおしているような本で、一種のたたき台として、「あー、前そういうのあったなー」とか「あの前のコード、ここに出てる駄目コードそのものじゃん!」とか、あーでもないこーでもないと昔の所業を懺悔しながら読める本としてお勧めです。薄いし。

複数人で読んでコードレビューとか楽しそう

 そんなわけで、会社とかで複数人で一つのソースコードを書いているようなシチュエーションで、コード書いてる人が各々この本を読んで、今までのソースコードをレビューする、みたいな使い方をすると楽しそうです。こういう本が無いと何がどう悪いのかを0から説明しなければなりませんけど、レビューする人全員がこの本を読んでれば「ここはさ、あの本の**章に書いてある駄目なコードそのものじゃん?」みたいに簡単に説明できてやりやすそうです。

バグフィックスをしたさきゅばす2.0b2を公開しました

Posted on

リリースノート

公開から2ヶ月以上たったので、バグ修正バージョンのさきゅばす2.0b2を公開しました。

ほぼバグフィックスのみのバージョンです。その他、NGワードスクリプトのサンプルを増やしたりしたので参考にしてみてください。

変更履歴

  • Pythonを最新版に載せ替え、配布サイズを削減しました。
  • スペルミスなどを修正しました。
  • NGスクリプトのサンプルを追加しました。
  • 公式動画をDLできないバグを修正しました。
  • Windowsにおいて、NGスクリプトの改行コードがCRLFだった場合、エラーを発生させないように変更しました
  • ライセンス表示を、配布バイナリ内からWebサイト上に移動しました。

ダウンロード

 ダウンロードはSourceforge内のダウンロードページから行えます。

スクリーンショット

20120805.jpg

 ようせいさんかわいい

 …GUIの変更は殆どありません(ぇ

その他

 registっていう単語は無い(!)そうですが、ねこまたでバッチリ使ってしまっておりました…か、悲しい。

 ライセンス表示のためにファイルが一個増えてしまって、普通に読まれないReadmeがさらに読まれなくなるのではと思って減らしてみました。一緒に配布してるテキストは見づらいのでできるだけ減らしたいです。

 今回はPythonのバイナリをほぼ全とっかえなので全部配布し直すのは理にかなってるのですが、これだけバイナリが大きいとなるとやっぱり自動更新を考えた方がよさそうですね

トリビア:Google+アカウントにアップデートするときは気をつけないと全データが全部飛ぶ

Posted on

 泣きたい

 GoogleのGoogle+へのプッシュ具合は最近すごいですよね。今までほとんどGoogle+はつかっていなかったのですが、最近必要にせまられたので登録してみました。

 Google+ができてしばらくたってからのアカウントでは、gmailのアカウントでもPicasaのアカウントでも登録時にGoogle+にも一緒に登録することが必要になっていますが、比較的昔から使っているアカウントの場合には、Google+へ追加で登録を行う(Google曰く「アップデート」)ことになっています。

 今回、ふるいgmailアカウントをGoogle+にアップデートしようとしたら悲しい自体になったので周知徹底等させて頂きたく存じ上げます(敬語)

 Google+の登録時の画面で、性別と年齢を入れるわけですが…

20120711_01.png

 はまちちゃんも実名だけはやめとくよう言うし、私も実際その通りだと思うので、年齢も性別も毎度適当に入れています。で、今回は歌詞になってて覚えやすい鉄腕アトムの誕生日を入れて登録してみたのですが…

20120711_02.png

 …ん?

 20120711_03.png

 なん…だと…10年間分のメール…が…

 どうやら13歳以上じゃないと使えない、ということらしいです。小学生はgmailを使ってお友達とメールすることができないらしい!です。うーん、リアル小学生のときyahoo!mailもhotmailも取ったような気がするのですが…。Google先生は厳しいですね。Google社内にスーパー小学生エンジニアとか居ないのかしらん(ぉ

 Googleに身分証明書を送付するなどすれば解除と削除回避は可能だそうですが、Googleにそこまで個人情報を教える気にはどうしてもなれませんし、そもそも偽名だし…であきらめるしかなさそうです。規約上たぶん本名を入れることになってると思うので、私の自業自得ではあるのですが…悲しい。

 そういえば上の画像だと「波動関数 ぷさい」ですが、最初これははじかれました。Google+の命名ルールに従っていない、もっというと「本名じゃないでしょそれ」と言われてしまいました。でも昨今の「DQNネーム」とやらを見てると、これぐらい変わった名前の人がいてもおかしくないと思うのだけど…Googleに身分証明書送らないとアカウント作らせて貰えないのかな??そこまでしてやるもんんかなあ?

 ファーストサーバの中の人の気持ちがわかった気がします

HTTP1.1のLocationヘッダは、絶対URLでないとRFC違反

Posted on

さきゅばす2のバグ対処で知ったお話です。とりあえず、HTTPの基礎知識はあることを前提にします。

さきゅばす2では、公式動画のDLがどうしても出来ませんでした。こんなエラーが出ちゃうんです。

[E][    PyBridgeImpl] Python says:
(略)
HTTPError: HTTP Error 302: Found - Redirection to url '/watch/1339405721' is not allowed

どうやらリダイレクションの段階で問題が発生してるみたいですねぇ。

HTTPでのリダイレクション

とあるページにアクセスした時に他のページにジャンプしなおさせる「リダイレクション」ですが、このリダレクションはHTTPはレスポンスヘッダで301,302,303,307のレスポンスコードを返した上で、Locationヘッダを設定することで実現しています。

どこかのページのレスポンスヘッダで、

Location: http://www.google.com/

こういうふうなヘッダを返すと、そのページにアクセスした人がGoogleに無条件で飛ばされるわけですね。

で、ニコニコ動画のリダイレクションでは、

Location: /watch/1339405721

となっていて、http://www.nicovideo.jp/watch/1339405721 にジャンプするようになっていました。うーん、特に問題なさそう。

Locationヘッダは絶対パスしか認められない。

…ところがどっこい、実はLocationヘッダのURLは絶対パスしか認められないのです。RFC見ましょうか。

Location       = "Location" ":" absoluteURI

で、このabsoluteURIはRFC2396: Uniform Resource Identifiers (URI): Generic Syntaxで定義されておりまして、

absoluteURI   = scheme ":" ( hier_part | opaque_part )

ということで、URIスキーム(httpとかftpとか)から始まる絶対URIであることがわかります。

とはいえ、色々調べてみると、携帯とか一部のブラウザを除くと大体相対パスでも動くそうで、絶対URIを作成時に予測するのが難しい、他人に配布するスクリプトなどでは相対パスで指定していることが多いそうです。

 

ニコニコ動画の場合は絶対URIでも良いと思うのですが今回は相対パスで指定されていて、ふつうのブラウザなどではこれで意図通りに動くのですが、さきゅばす2のPythonでは最近のバージョンまで相対パスに対応していなかったため問題になったようです。で、最新のバージョンでは相対パスに対応しているので、配布バイナリを最新バージョンに差し替えることにしました。

で、ほかのヘッダではどうなってるの?

HTTPのヘッダには、ほかにもいくつかパスを指定できるものがありましたよね。いくつか調べてみました。

Request-URI

リクエストURIは、GET ****** HTTP/1.1みたいな形式で指定されるときの、あのURIです。

Request-URI    = "*" | absoluteURI | abs_path | authority

なんと…。実は絶対URI指定でも良いのですね。たまに絶対URIでのリクエストがこのサーバに飛んでくるのですが、てっきり不正なリクエストなんだと思ってました。…とはいえ、ほとんどのクライアントは絶対パスで取得するのがふつうなので、ちょっと警戒すべきだとは思いますけど…。

 

Content-Location:

Locationと似てますが別物です。リクエストされたコンテンツが別のところからも取得可能な時に使われる…そうですが、実際使われてるのを見たことがありません…。だれか教えて(ぇ

Content-Location = "Content-Location" ":"
( absoluteURI | relativeURI )

こっちは相対URIも可能です。同じサーバであることが自然だからなのかなあ。

Referer

リファラです。ほかのページのリンクなどをたどって来たことを示すわけですが…

Referer        = "Referer" ":" ( absoluteURI | relativeURI )

なんと!これも相対URIはOKでした。とはいえ、世の中のクライアントは大体絶対URIじゃないでしょうか?

以上です。たまにはRFC読んでみると意外な発見がいろいろありますね。

追記:RFC7321では相対URLが認められている

その後2012に出たRFC7231では相対URLもOKになりました。以下、引用です:

7.1.2. Location

   The "Location" header field is used in some responses to refer to a
   specific resource in relation to the response.  The type of
   relationship is defined by the combination of request method and
   status code semantics.

     Location = URI-reference

   The field value consists of a single URI-reference.  When it has the
   form of a relative reference ([RFC3986], Section 4.2), the final
   value is computed by resolving it against the effective request URI
   ([RFC3986], Section 5).

   For 201 (Created) responses, the Location value refers to the primary
   resource created by the request.  For 3xx (Redirection) responses,
   the Location value refers to the preferred target resource for
   automatically redirecting the request.

   If the Location value provided in a 3xx (Redirection) response does
   not have a fragment component, a user agent MUST process the
   redirection as if the value inherits the fragment component of the
   URI reference used to generate the request target (i.e., the
   redirection inherits the original reference's fragment, if any).

   For example, a GET request generated for the URI reference
   "http://www.example.org/~tim" might result in a 303 (See Other)
   response containing the header field:

     Location: /People.html#tim

   which suggests that the user agent redirect to
   "http://www.example.org/People.html#tim"