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

# Motivation

かつて[SIOSocket](https://github.com/MegaBits/SIOSocket)なる[Socket.IO](http://socket.io/)のiosクライアントがありました。

"かつて"というのも、このクライアントはObjective-Cで書かれているのですが、2015年の3月に[Swift製公式クライアント](https://github.com/socketio/socket.io-client-swift)が発表されたことにより、お役御免で開発STOPとなってしまいました。

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

例えば、

```js:javascript
io.on('join', function (args) {});
```

みたいなのは、

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

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

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

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

# JavaScriptCoreについて

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

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

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

![JavascriptCoreImport.png](https://qiita-image-store.s3.amazonaws.com/0/46489/3f6dd170-05f4-1974-431a-c62a76a766c2.png)

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

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

```swift
// 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の手順は、以下のように呼ぶこともできます。

```swift
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インスタンスを取得/生成したとき：

 - JValueインスタンスは、jsの値/オブジェクトへの強参照を保持している
 - JValueインスタンスは、紐付いているコンテキストを強参照で保持している
 - JValueインスタンスには、swiftで扱える型にするために、toInt32()やtoDouble()といったメソッドが用意されている

### **JSVirtualMachine, JSContext, JSValueの関係図**

![vmImage.png](https://qiita-image-store.s3.amazonaws.com/0/46489/1227650a-03cc-799d-6998-f56b8fb5cb27.png)

<font size="2">[WWDC2013のセッション](https://developer.apple.com/videos/play/wwdc2013-615/)スライドに少し加筆しました</font>

### **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はJavaScriptCoreがObjective-C製APIということで使えない模様)。

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

 - [Appleリファレンス: Closures の部分](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html)
 - [qiita記事: Objective-CのBlocksとSwiftのクロージャでは変数のキャプチャが違う](http://qiita.com/1234224576/items/7d26b98d5731a8e7af93)

をご参照ください。

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

```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のドキュメント](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html)のClosuresの部分をみると、Objective-CのブロックとSwiftのクロージャは互換性があるので、\@convention(block)アトリビュートをつければ使えるよとのこと。

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

#### unsafeBitCast

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

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

```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](https://developer.apple.com/videos/play/wwdc2013-615/)
[Java​Script​Core Written by Nate Cook — January 19th, 2015](http://nshipster.com/javascriptcore/)
[JavaScriptCore Changes for Swift](https://developer.apple.com/library/ios/releasenotes/General/iOS90APIDiffs/Swift/JavaScriptCore.html)
[JavaScriptCore.framework の普通な使い方 #cocoa_kansai](http://www.slideshare.net/tomohirokumagai54/java-scriptcoreframework)

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

## SIOSocket.swiftのソース
https://github.com/mitolog/SIOSocket-swift

todo: 

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

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

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

なので前提として、

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

必要があります。

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

### 1. UIWebViewを生成し、JSContextを引き出す

```swift: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?](http://stackoverflow.com/questions/18920536/why-use-javascriptcore-in-ios7-if-it-cant-access-a-uiwebviews-runtime)

### 2. window.onloadコールバックをjsに登録する

```swift: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] というキャプチャは使えない

最初、

```swift:SIOSocket.swift_97行目
let onLoad: @convention(block) () -> Void = { [unowned socket] in
```

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

## callWithArguments()スゲェ

```swift: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はこの部分元々以下のように書いていたのですが、上のようにするほうが楽だしスマートな気がします。

```objc:socket.io.js.h
/*!
 *  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のリファレンス](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/StringsAndCharacters.html)の*Special Characters in String Literals*の部分を参考に。

# 換装しての感想

JavaScriptCoreに関しては、[WWDC2013のセッションビデオ](https://developer.apple.com/videos/play/wwdc2013-615/)を見るのが一番理解が進みましたので、超オススメです。

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

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

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

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

# References

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

[socket.ioクライアントAPI](http://socket.io/docs/client-api/)