mitolab's diary

東南アジアで頑張って生きてる人のブログ

socket.io(SIOSocket)をネタにJavascriptCoreに入門してみる with Swift

Motivation

かつてSIOSocketなるSocket.IOiosクライアントがありました。

"かつて"というのも、このクライアントはObjective-Cで書かれているのですが、2015年の3月にSwift製公式クライアントが発表されたことにより、お役御免で開発STOPとなってしまいました。

ただ、僕はこのライブラリ面白いなーと思ったところがあって、それはminifyしたjsのコードと少しのソースで、Objective-C製ライブラリの如く使えるようにしていたところです。

例えば、

io.on('join', function (args) {});

みたいなのは、

[self.socket on: @"join" callback: ^(SIOParameterArray *args){}];

こんなふうに書けていました。

こういうのを、少しの労力で作れるなら、javascript界(以後jsと書きます)の膨大な資産をios界でも気軽に使いまわせるのでは?と思い興味を持っていました。

ということで、今回はこのSIOSocketをSwiftで換装してみて、時が来たら同様のことができるように前知識を蓄えておこう、というコンテンツになります。

JavaScriptCoreについて

今回のようにjsのコードをベースにしたswiftクライアントを作る上で欠かせないのがJavaScriptCore.framework。 これは端的に言うと、JavascriptObjective-C(Swift)をブリッジしてくれるObjective-C APIです。2013年からあって、Mac/iOS両方で使えます。

※ 元々Objective-CAPIなので、Swiftでは使いにくい部分がままあります。追ってその点についても触れていければと思います。

JavascriptCoreを使うには

frameworkとして提供されているので、Linked Frameworks and Libraries のところからいつもの様に追加して、使いたいところで import JavaScriptCore してあげれば使えるようになります。

JavascriptCoreImport.png

Swift上でjsのコードを実行する

基本的な例を挙げてみます。

// 1. コンテキストを作成
let context:JSContext = JSContext()

// 2. javascript(ここではfactorial function)をコンテキストに渡して評価する
context.evaluateScript("var factorial = function(n) { if(n<0){return;}if(n===0){return 1;}return n*factorial(n-1); }")

// 3. コンテキストには既にfactorialメソッドが登録されているので、それを呼ぶ
let result: JSValue = context.evaluateScript("factorial(3)")

// 4. 返ってきた値をよしなに変換して使う
print(result.toInt32())

また、3の手順は、以下のように呼ぶこともできます。

let factorial:JSValue = context.objectForKeyedSubscript("factorial")
let result:JSValue = factorial.callWithArguments([3])

以下、各用語や概念の補足です。

JSContext

1つのJSContextインスタンス(コンテキストと呼ぶ)を生成したとき:

  • コンテキストはグローバル変数で、jsでいうwindowオブジェクトに相当する
  • コンテキストを通して、jsに変数を登録したり、jsの実行結果を取得したりできる
  • コンテキストを通して取得/生成したJSValue(後述)は、コンテキストに強参照を持つ

JSVirtualMachine

1つのJSVirtualMachineインスタンス(vmと呼ぶ)を生成したとき:

  • vm上に複数のコンテキストを持つことができる
  • vm上に存在する複数のコンテキスト間で変数を共有できるが、vmをまたがる共有はできない(それぞれのJSVirtualMachineはそれぞれのガベージコレクターとヒープをもって管理しているため)
  • 1つのvmにつき同時に実行できるスレッドは1つ(= jsを複数同時に実行したいなら複数vmを立ち上げる必要がある)

JSValue

1つのJSValueインスタンスを取得/生成したとき:

JSVirtualMachine, JSContext, JSValueの関係図

vmImage.png

WWDC2013のセッションスライドに少し加筆しました

func evaluateScript(script: String!) -> JSValue!

評価の結果返り値があれば、ここで取得することもできます。ここでは返り値は特に指定していないので、undefinedになります。

func objectForKeyedSubscript(key: AnyObject!) -> JSValue!

コンテキスト上にある変数にアクセスすることができます。上記の場合はfactorial変数を取り出しています。これは、Objective-Cであれば、

context["factorial"] というふうにsubscriptをつけることで取り出せたのですが、swiftだと上記のようにメソッドを呼ばなければいけない上に、ドット記法(例えばwindow.onload)で取り出すのもできないようです。

callWithArguments(arguments: [AnyObject]!) -> JSValue!

先に取り出したfactorial関数に引数を渡すことができます。面白いのは、引数が配列になっていて、且つAnyObject!になっていること。つまり、JavascriptCoreさんが、

  • 配列のindex順にjsのfunctionに渡してくれる
  • swift側の型とjs側の型を自動的に変換してくれる

ということになります。

js上でswiftのコードを実行する

大きく分けて2種類の方法があります。

1. クロージャを使う

まず基本ルールとして、

  1. JSValueのキャプチャは避ける -> 代わりに引数として渡す
  2. JSContextのキャプチャは避ける -> 代わりにJSContext.currentContext()を使う

と、WWDC2013セッションのビデオでは言っていましたが、普通にキャプチャすると強参照になってリークしやすくなるので、weakを使いましょうということと認識しました(後述しますがunownedはJavaScriptCoreObjective-CAPIということで使えない模様)。

※ ちなみに、objcのブロックとswiftのキャプチャの違いは、

をご参照ください。

では、クロージャを使った例を示します。

let context = JSContext()
let say: @convention(block) String -> String = { str in
    return "say \(str)!"
}
context.setObject(unsafeBitCast(say, AnyObject!.self), forKeyedSubscript: "say")
print(context.evaluateScript("say('hello')"))

// もちろんこのようにも実行できる
let sayFunc = context.objectForKeyedSubscript("say")
print(sayFunc.callWithArguments(["hello2"]))

上記は、jsのコードにswifitで書いたfunctionを渡し、jsのコードとして実行するサンプルです。 あまり見かけない単語が幾つか出てきています。

@convention(block)

AppleのドキュメントのClosuresの部分をみると、Objective-CのブロックとSwiftクロージャは互換性があるので、@convention(block)アトリビュートをつければ使えるよとのこと。

元々、JavaScriptCore.frameworkがObjective-C APIなので、このような処理をする必要が出ています。

unsafeBitCast

上記@convention(block)アトリビュートによって、sayオブジェクトはObjective-Cのブロックとしてコンパイル時には扱われますが、そのままだと、context.setObject~のところで、エラーが出てしまいます。objectをAnyObject!として渡さなければならないからです。なのでここはエラーをさけるため、unsafeBitCastを使っています。

<<余談>>

元々Objective-CAPIなので、Objective-Cで書くととても楽です。特にblockのところはObjective-Cだと、このように書けます。

JSContext *context = [[JSContext alloc] init];
context[@"say"] = ^(NSString *str) {
    return [NSString stringWithFormat:@"say %@!", str];
}
NSLog(@"%@", [context evaluateScript:@"say('hello')"]);

Swiftより断然直感的ですね...。

2. JSExportを使う

ここは今回使っていないので、説明は以下の参考リンクをご参照ください。

JacaScriptCoreの参考リンク

Integrating JavaScript into Native Apps - Apple WWDC 2013 Java​Script​Core Written by Nate Cook — January 19th, 2015 JavaScriptCore Changes for Swift JavaScriptCore.framework の普通な使い方 #cocoa_kansai

SIOSocketをSwift製にする上で気づいた点など

SIOSocket.swiftのソース

https://github.com/mitolog/SIOSocket-swift

todo:

  • テスト書いて、実際にすべてのargumentがおくれるか確認(文字列パラメータしかチェックしてないッス...)
  • JSExportを使ってみる
  • UI適当すぎ

socket.ioを使うには、UIWebView()を使う必要がある

socket.ioのクライアントAPIのページに書いてあるように、socket.ioをスタンドアローンで使う場合、ioオブジェクトはwindowのプロパティとして配置されます。

なので前提として、

  • UIWebView()のJSContextを使う
  • UIWebView()にhtmlをロードする
  • ロードが完了(window.onload)してから、ioオブジェクトを取り出す

必要があります。

SIOSocketでは、以下のように実現しています。

1. UIWebViewを生成し、JSContextを引き出す (SIOSocket.swift_84行目)

socket.javascriptWebView = UIWebView()
            if let ctx = socket.javascriptWebView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") {
                socket.javascriptContext = ctx as! JSContext
            } else {
                response(nil)
                return nil
            }

via stack overflow - Why use JavaScriptCore in iOS7 if it can't access a UIWebView's runtime?

2. window.onloadコールバックをjsに登録する(SIOSocket.swift_97行目~206行目)

let onLoad: @convention(block) () -> Void = {
    /* some code here */
}
socket.javascriptContext.setObject(unsafeBitCast(onLoad, AnyObject!.self), forKeyedSubscript: "swift_onloadCallback")
            socket.javascriptContext.evaluateScript("window.onload = swift_onloadCallback;")
            socket.javascriptWebView.loadHTMLString("<html/>", baseURL: nil)

jsに渡すクロージャで[unowned self] というキャプチャは使えない(SIOSocket.swift_97行目)

最初、

let onLoad: @convention(block) () -> Void = { [unowned socket] in

としていたのですが、unownedがエラーになってしまい、weakに変えたらコンパイル通りました。恐らくswiftにはあって、Objective-Cにはないからだと思います。多分。

callWithArguments()スゲェ(SIOSocket.swift_108行目)

let io = context.objectForKeyedSubscript("io")
let swiftSocket = io.callWithArguments([
    hostUrl, [
      "reconnection": reconnectAutomatically,
      "reconnectionAttempts": attemptLimit == -1 ? "Infinity" : attemptLimit.description,
      "reconnectionDelay": Int(withDelay) * SIOSocketConsts.MSEC_PER_SEC,
      "reconnectionDelayMax": Int(maximumDelay) * SIOSocketConsts.MSEC_PER_SEC,
      "timeout": Int(timeout) * SIOSocketConsts.MSEC_PER_SEC,
      "transports": withTransports
      ]
    ])

これだけのパラメータを軽々と自動的に変換してくれるので、感動でした。SIOSocketはこの部分元々以下のように書いていたのですが、上のようにするほうが楽だしスマートな気がします。

/*!
 *  socket.io client constructor format.
 */
static NSString *socket_io_js_constructor(NSString *hostURL, BOOL reconnection, NSInteger attemptLimit, NSTimeInterval reconnectionDelay, NSTimeInterval reconnectionDelayMax, NSTimeInterval timeout, NSArray *transports) {
  NSString *constructorFormat = @"io('%@', {  \
      'reconnection': %@,                     \
      'reconnectionAttempts': %@,             \
      'reconnectionDelay': %d,                \
      'reconnectionDelayMax': %d,             \
      'timeout': %d,                          \
      'transports': [ '%@' ]                  \
  });";

  return [NSString stringWithFormat: constructorFormat,
      hostURL,
      reconnection? @"true" : @"false",
      (attemptLimit == -1)? @"Infinity" : @(attemptLimit),
      (int)(reconnectionDelay * MSEC_PER_SEC),
      (int)(reconnectionDelayMax * MSEC_PER_SEC),
      (int)(timeout * MSEC_PER_SEC),
      [transports componentsJoinedByString:@"', '"]
  ];
}

JSContextにアクセスするスレッドはどこでもいい

元々のSIOSocketでは、onloadコールバックでioオブジェクトを取得する際に利用したスレッドを保持しておき、そのスレッド上でのみJSContextにアクセスしていましたが、WWDC2013のビデオを見てみたら、JSContextはスレッドセーフとのこと。実際に、元々performSelectorでスレッドを指定して実行していた箇所を外してみたら、うまく動いたので問題ないと思います。

ただし、JSVirtualMachineの補足のところで書いたように、1つのvmにつき同時に実行できるスレッドは1つ(= jsを複数同時に実行したいなら複数vmを立ち上げる必要がある)らしいので、そこは要注意です。

jsを文字列リテラルとして埋め込む場合は、エスケープに気をつけろ

当初は、staticなglobal変数としてsocket.io.jsを読み込んでいました。その際は、以下のエスケープを施しました。

\ -> \\
" -> \"
' -> \'
tab -> \t
line feed -> \n

エスケープ文字列に関しては、AppleのリファレンスSpecial Characters in String Literalsの部分を参考に。

換装しての感想

JavaScriptCoreに関しては、WWDC2013のセッションビデオを見るのが一番理解が進みましたので、超オススメです。

socket.ioの場合、windowプロパティのonloadをキャッチする必要があったので、UIWebViewからJSContextを引き出せる部分が結構キモだなと思いました。

socket.ioみたく、UIを伴わないjsで且つサイズが小さいものだと、swift製クライアントを作る必要性はあるのかな〜と感じました(Underscore.jsとか?)。

ただぶっちゃけ、objcで書いてbridging-headerで読み込んで使ったほうが楽かもしれない。

あと、他にJavaScriptCoreを使ったライブラリがあったら教えていただきたいです。

References

[Node.js] Socket.ioで双方向通信チャットアプリを構築 〜 JSおくのほそ道 #005 ↑ サンプルを作るうえで一部ソースを使わせていただきました。ありがとうございました。

socket.ioクライアントAPI

デジタルノマドに評判のチェンマイでフリーランスエンジニアが生活してみての感想

tumblrからはてなに引っ越してきました。結構というかかなり快適...。はてなさんよろしくおねがいします。

軽く自己紹介

@mito_log 。 沖縄のIT会社に4年ほど務めた後、主にフリーランスiOSデベロッパとして沖縄・東京で約1年活動後、約300万の貯蓄をもって海外ノマド旅行を開始。約半年間、世界各地を放浪しつつ、お仕事したり、もの作ったり、面白そうな人を訪ねたり、イベントに参加しつつ、今後注力することを見つける予定。 今興味ある分野としては、再利用エネルギーや環境問題。

今はチェンマイにきて約3週間がたち、上記とは別で進行中の自社プロダクトを完成させるため、もう1ヶ月滞在する予定。

チェンマイってどんなとこ?

IT業界では言わずと知れたデジタルノマドが集まる、タイの北方720キロくらいにある、タイ王国第二の都市です。

ノマドリストなる世界各地のデジタルノマドスポットをランキング化しているサイトでは、堂々の1位を獲得。

nomadlist.com

1位になる要因としては、主に、

  • 年中過ごしやすい気候
  • 物価が安い
  • インターネットが安定している
  • ご飯が美味しい
  • 治安がいい
  • アクティビティが多い

とかかなぁと思います。大体 「チェンマイ ノマド」でググるといろいろ出てきます。

あと、日本との時差は-2時間です。

チェンマイでの暮らし

そんなデジタルノマド天国なチェンマイ、私がフリーランサーとして約3週間滞在してみての感想を書きます。

TOC:

  • MAYA最強説
    • コワーキングスペース&カフェ CAMP最強
    • AISのSIMを使えば更に最強
    • 食事のバリエーション多く、コスト低い
    • 映画館が併設されていて、安く映画を見られる
    • その他good points
  • チェンマイの疑問点
  • チェンマイはこんな人におすすめ

MAYA最強説

MAYA(地元の人はメィヤ~と発音する)とは、5F建てのショッピングモールで、イオンモールとか駅直結のデパートのようなものを想像してもらえればいいかもしれません。そこでは、洋服屋さん、フードコート、ゲームセンター、映画館、カフェがあったり、地元美術大学の展示をしていたり、室内外問わずイベントをしていたりします。

※ プチ情報:市街(いわゆるお堀付近)からであれば大体ソンテウという乗り合いタクシーで20~30バーツ/人でいけるはず。複数人で乗ると20バーツで交渉しやすい。

コワーキングスペース&カフェ CAMP最強

で、ノマドワーカーにとって何がいいかというと、そこの5Fに入居しているC.A.M.Pというカフェが素晴らしいのです。詳細は以下の記事が参考になります。

チェンマイのコワーキングスペース「C.A.M.P. 」はノマドワーカーにおすすめの仕事場だった
dotsnest.com

ほぼ上記で漏れはないのですが、プチ情報を挟むと、チェンマイ大学や高校?が近いので、夕方以降は学生さんが流れ込んできます。日中はデジタルノマドな欧米人の方が多めです。

たまたま隣に座ったpeterさんはbitcoin関連の仕事をしていて、「weekdayはいつもここにいるぜ、毎日ボタンを押してるぜ、日本には居合の勉強でいったことがあるぜ」って言っていました。bitcoinよく知らなかったので、調べてみよう...みたいな自身の知識と交友の幅を広げるような出来事もありました。

ほぼ完璧なC.A.M.Pですが、唯一欠点として挙げるとすれば、たまに停電することです。ただ割りとすぐ復旧するので、それまではテザリングで我慢です。

AISのSIMを使えば更に最強

上記記事中にもありましたが、AISという日本で言うプロバイダのような会社と本カフェが提携しているようで、AISの特定のSIMを契約していると、無制限にWifiが使えます。私もその恩恵を受けて日中は大抵ここに入り浸っています。SIMカードは以下記事のものを使っています。

ノマドワーカー必見!チェンマイでsimカードを購入するなら、AISがおすすめ。
dotsnest.com

1つ付け加えると、泊まる宿によって違うと思いますが、安宿の場合はwifiが貧弱なことが多いです。安宿でなくても、例えばアパート/コンドミニアムなんかだと結局集合住宅なので回線が詰まりやすいです。そこで、AISのSIMカードで通信できる量を多めに持っておいて、宿では通信量を減らしつつテザリングっていうのも、ありだと思います。

食事のバリエーション多く、コスト低い

また、MAYAがいいのは、美味しいご飯が安く食べられて、しかもバリエーションが多いことです。

パッタイ(タイ風やきそば)
f:id:mitolab:20151108183407j:plain

ベトナムヌードル
f:id:mitolab:20151108183402j:plain

カオソーイ(グリーンカレー沖縄そばみたいな)
f:id:mitolab:20151108183355j:plain

マンゴーライス(マンゴー+もち米+ココナッツミルク)
f:id:mitolab:20151108183350j:plain

ココイチのカレー
f:id:mitolab:20151108183337j:plain

etc... 色々なものが食べられます。

大体30~60B(約105~210円)で食べられます! ただココイチは例外で、税金含めて約1000円弱しました。日本と変わらない値段でした。

映画館が併設されていて、安く映画を見られる

日本公開より一足先に映画を見ることができます。例えば2015年11月8日で公開しているのは、

007シリーズ最新作のスペクター
f:id:mitolab:20151108184115j:plain

この作品は、日本だと、12月一般公開予定です。

気になる料金は、サービスデーで100B(約350円)からあってシートによって料金が変わります。激安!

ただ唯一の欠点は、日本語字幕がないこと。英語字幕もありますが、タイ語の作品の場合のみです。 なので、上記のような映画だと英語を頑張って聞き取ることになります。。

あと、映画館で面白いのは2つあって、1つは、映画が始まるまでがやたら長いこと。こないだ見たピーターパンだと、30分くらい広告+プミポン国王を敬うタイムでした。長い。。。

で、もう一つは、お察しの通り、プミポン国王を敬うタイムです。

上記のような、国王の人生のタイムラインを追っかけるような動画が数分間流れて、その間皆起立してそれを見ていなければいけません。従わない場合は不敬罪で逮捕されるとか...。

私はこの事を知らないでダラっとポップコーンをつまんでいたところ、急に周りが起立し始めたので非常に焦りました(もちろん私も起立しました)。

いずれにしても、作業の息抜きに映画館にすぐいけるので最高です。

その他good points

その他の良い点としては、

トイレがウォシュレット完備できれい

f:id:mitolab:20151108183434j:plain

このとおりである。もちろん紙は流せないので、付属のゴミ箱に捨てます。

水・金はナイト・マーケットをしている

f:id:mitolab:20151108185929j:plain

これは開店前の準備中ですが、こんな感じ。

タイ人スタイルいい、かわいい方多い

すみません、チキン野郎なので写真ないです。。

美女大国タイの美人女優・アイドル・モデル TOP20ランキング | ASEAN 海外移住 アジア タイのススメ

このあたりでご確認ください。

チェンマイの疑問に思った点

上記までで大体いいところは述べたので、疑問に思う点をいくつか。

排ガスが結構きつい

どこのサイトでも空気が美味しいといいますが、市街地は車やバイクがガンガン通り、しかも開発中の建物も多く、工事用の車両もあったりで土埃もわりとあります。現地の人もマスクしているほどです。私も一つ買いました。現地デザインのものです。

少し話はそれますが、 タイは、近年まで国策として車の購入を補助する制度を設けていたためか、急激に車の量が増え、結構いい値段しそうな車(しかも軽自動車はほとんど見ない)が多く街を走っています。しかもほとんどが日本車です(こちらの調査を見ると2014年で日系企業のシェアは8割以上)。

また、どこかの記事で読んだのくらいなので要確認ですが、国民は2~3年ローンで車を購入したけれども、その影響で他の出費を切り詰めたため、2013年に補助金制度を廃止してからは、個人消費が低下したそう。

こういう状況を鑑みると、貨幣価値の違いで良い生活をさせていただいている自分はなんだか申し訳ないなぁと思えてきます。

仕事に対する取り組み方が違う...?

これは、日本と比べたらどこもそうなのかもしれないですが、例えば料理店で接客業をしているにも関わらず、常にスマフォをいじっていて、お客さんが来ても基本的に自分の用事が済んだら客の相手をするとか、呼ばれたら対応するとかそんな感じです。また、おしゃべりは当たり前で、オープン直後とかは化粧している方もちらほら。就業時間内なんじゃねーのかよ、と突っ込みたいところですが、ここはタイ。日本の常識では捉えてはいけません。

歩道に段差が多い

街では、基本的に歩道は段差だらけです。案外普通に歩いてても上下運動が多いので、歩道を避け気味になります。。 車いすの方や松葉杖の方は凄い大変そうです。

f:id:mitolab:20151108194005j:plain

バイクに乗っているのにヘルメットを全然被っていない

単純に危ないでしょう、と思います。

f:id:mitolab:20151108194550j:plain

やたらストローを使う

基本的にどんな飲み物でもストローを使って飲んでいる印象です。単純に資源の無駄だなぁと思った次第。

こないだ参加してみた Creative Connectというイベントで、「1日に出すゴミをゲーム感覚で減らしていこう」というコンセプトでアプリを発表されていたタイの方がいました。その方もストローは無駄だとおっしゃっていました。

※ racoon-challengeというアプリ名で、詳細はこちら。日本で同様のコンセプトのアプリならピリカ

チェンマイはこんな人におすすめ

ということで、少なくとも、私のようなフリーランスで場所にとらわれず仕事をされたい日本の開発者さんには自信をもっておすすめできると思います。 それから、スタートアップの集中開発合宿とかにいいかなと思います。ご飯は美味しいし、海外の開発者とも触れ合えるし、文中では触れませんでしたが、贅沢なコンドミニアムを1ヶ月単位から借りられます。私の感覚だと、メンバー3,4名で1万バーツ(約3.5万円)くらいのところを借りるとちょうどいい気がします。