クリスマスはNoiz2saをChromeで遊ぼう!

Posted on

Native Clientは死んだんだ
いくら呼んでも帰っては来ないんだ
もうあの時間は終わって、君も人生と向き合う時なんだ


弾幕の海にたゆたう、アブストラクトシューティングNoiz2sa。

ChromeにNoize2saを移植してみた

Google ChromeにNoiz2saを以前移植して公開してあったのですが、こちらでは書きそびれてたので、クリスマスというタイミングに書くことにします。

ABA GamesのSTG “Noiz2sa”をブラウザで遊べるようにしてみました。

20121224

Chrome ウェブストア – noiz2sa

 ChromeのNative Clientという仕組みを使っていて、これを使うと比較的簡単にネイティブのゲームをブラウザ上で動かすことが出来るようになります!

GamepadのAPIがうまく動かないので残念ながらゲームパッドでは動く動かないのですが、キーボードでならガリガリ動くのでJoyToKeyでも使ってやってみてください!…だったら普通にWindows版で遊ぶ?そ、そうですよね…。

特に何もしてないけどクロスプラットフォーム

NativeClientは、何もしなくてもクロスプラットフォームに対応していて、今回の拡張もLinux/Windows/MaxOSXで動きます。ちょうべんりー。ちなみに開発自体はLinuxでやってます。

NativeClientのしんどいところ

NativeClientはセキュリティ上の要請から、色々制限があります。普通に計算をさせる分には困らないですし、グラフィックやサウンドの出力も、NaCl Portsがあるお陰で普通に使う分には困りませんが、ファイルアクセスやネットへの接続などでは独自のライブラリを使わなければなりません。

今回ゲームということでネットワークは使っていないのですが、タイトルの「noiz2sa」のロゴ画像(ビットマップ)や弾幕の定義データ(XML)、BGMや効果音のファイルなど、比較的数は少ないもののファイルアクセスがあり、これに対処する必要がありました。

ファイルはすべてゲームバイナリ内に

Native ClientにはURLLoaderというクラスがあるので、これを使えば任意のURLの内容を取得できます。これをローカルファイルの代わりに使えば良さそうです…が、今回はゲームなのでローディングを避けたいのと、大した容量でもない(8MBくらい)なので、全部バイナリ内に入れてしまうことにしました。

具体的にいうと…

struct FileEntry{
const char* filename;
const size_t size;
const char* data;
};
extern const struct FileEntry gFileImages[];
extern const size_t gNumberOfFileImages;

Noiz2saNaCl/nacl/file_data.h at master ・ ledyba/Noiz2saNaCl ・ GitHub

というような構造体をつくって、

static const char ImageData[8810309] = [具体的なデータ>];
const size_t gNumberOfFileImages=96;
const struct FileEntry gFileImages[96] = {
{
.filename="boss/forward_3way.xml",
.size=818,
.data=&ImageData[0],
},
(ファイルのエントリをたくさん)
};

Noiz2saNaCl/nacl/file_data.c at master ・ ledyba/Noiz2saNaCl ・ GitHub

として全データを格納します。

これを読み書きするためのfopen、fread、fcloseなどの関数を独自に定義して

extern size_t nacl_ftell (FILE *file);
extern int nacl_fseek (FILE *file, size_t offset, int whence);
extern FILE *nacl_fopen (const char *filename, const char *modes);
extern size_t nacl_fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
extern size_t nacl_fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
extern int nacl_fclose(FILE *fp);
extern int nacl_feof(FILE *stream);
extern int nacl_ferror(FILE *stream);
extern void nacl_rewind(FILE *stream);
extern int nacl_fgetc(FILE *stream);
extern int nacl_fflush(FILE *stream);

最後にマクロで普通のfopenを乗っ取ります

#define fopen(n,m)  nacl_fopen(n,m)
#define fclose(f) nacl_fclose(f)
#define ftell(f) nacl_ftell(f)
#define fseek(f,o,w) nacl_fseek(f,o,w)
#define fread(p,s,n,f) nacl_fread(p,s,n,f)
#define fwrite(p,s,n,f) nacl_fwrite(p,s,n,f)
#define feof(f) nacl_feof(f)
#define ferror(f) nacl_ferror(f)
#define rewind(f) nacl_rewind(f)
#define getc(f) nacl_fgetc(f)
#define fgetc(f) nacl_fgetc(f)
#define clearerr(f) nacl_clearerr(f)
#define fileno(f) -1
#define fflush(f) nacl_fflush(f)

Noiz2saNaCl/nacl/storage.h at master ・ ledyba/Noiz2saNaCl

ソースコードを改変できるので別に乗っ取る必要はないのですが、NativeClientの元の記事がそういう感じで乗っ取ってたのでそうしときました。

ディレクトリ内のファイルを列挙するAPIであるreaddirなどもおんなじ感じで乗っ取っております。

この方法はGoogle先生謹製の記事を参考にしていて、ソースも一部拝借してます。Case Study: Porting MAME to Native Client – Native Client — Google Developers

さてどうしたものか

NativeClient、ファイルが絡むと一気にめんどくさくなります。既存の技術(Flashとか)を置き換えるには少し不便ですね。


WordPressに移行した&支援スクリプト書いた

Posted on

諸事情でWordPressをインストールして別箇所で使っていたのですが、これは素晴らしいですね!!!
何が素晴らしいって、もう

  • UIは使いやすいし!
  • アップデートは自動だし!
  • 固定ページとかの編集も簡単だし!
  • テンプレートを簡単に変更できるし!
  • プラグインは豊富だし!
  • かわいいテンプレート一杯あるし!
  • かわいいテンプレートがたくさんあるし!
  • かわ(ry

もう移行しない理由とかないよねーないよねー、ということでちょっと重い腰を上げて移行!することにしました。言語別にパッケージが分かれてるのは正直ダサいと思いますが、不満はそれぐらいです。

はてな記法を捨てたい。

はてな記法のプラグインを使ってて依存していたため移行をためらっていたのですが、今回この負の連鎖を断ち切るべく、プラグインの変換エンジン部分を適当に引っ張ってきて、エクスポートしたデータのはてな記法の部分を変換する(!)スクリプトを書きました。

  • github: ledyba/mt-hatena-converter

「ブログ記事のエクスポート」で引っ張ってきたexport*****.txtファイルを、このスクリプトに渡して下さい

perl format.pl export******.txt

ごめんなさい:追記部分の変換を忘れてました。使用時には注意してくださいね…。

今回Perl使ったのは初めてなのですが、空文字列をfalseとして認識する使用で随分ひっかかりました。もうやめようよそういう仕様…ね?

 


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標準で「うるう秒を考慮しない」としてる点だと思います。今更変えられないのが非常に…悔しいですね…。