Edited at

SwiftでJavaScript

More than 5 years have passed since last update.

案ずるより書くが易し…

Swiftのウリの一つは(Objective-)?Cに直アクセスできることですが、それが意味するのは、JavaScriptに直アクセスできるということでもあったのです。


とりあえずJS使うだけなら

こんだけ。

import JavaScriptCore

let ctx = JSContext()
let ary = [0, 1, 2, 3]
var jsv = ctx.evaluateScript(
"\(ary).map(function(n){return n*n})"
)
println(jsv)
var a = jsv.toArray()
println(a)

はい。見てのとおり、import JavaScriptCoreして、JSContext()でJSの実行環境をこしらえて、それに.evaluateScript()String食わせれば、おしまい。

実行結果はJSValueという型で、見ての通りObjective-Cに対応する型へ変更するメソッドもついてます。


JSにSwiftの値を渡すには?

しかしこれだけではつまらない。Swiftの値をいちいち文字列化して.evaluateScript()のは面倒そう。直接JSの変数にアクセスすることはできないでしょうか?

そう。Objective-Cならね。

ctx[@"theAnswer"] = @42;

これが出来るなら、当然Swiftでも

ctx["theAnswer"] = 42

とかできそうなのですが、残念ながらObjective-Cのsubscriptまでは実装されていなかったようです。

というわけでbridgeを書きます。ここではプロジェクト名がjsになっていますが、実際に使う際には各自読み替えてください。

js-Bridging-Header.h

#import <JavaScriptCore/JavaScriptCore.h>

JSValue *getJSVinJSC(JSContext *ctx, NSString *key);
void setJSVinJSC(JSContext *ctx, NSString *key, id val);

jsc.m

#import <JavaScriptCore/JavaScriptCore.h>

JSValue *getJSVinJSC(JSContext *ctx, NSString *key) {
return ctx[key];
}
void setJSVinJSC(JSContext *ctx, NSString *key, id val) {
ctx[key] = val;
}

あとは、

main.swift

extension JSContext {

func get(key:NSString) -> JSValue {
return getJSVinJSC(self, key)
}
func set(key:NSString, _ val:AnyObject) {
setJSVinJSC(self, key, val)
}
}

とSwiftでしておけば、

ctx.set("foo", [1,2,3])

jsv = ctx.evaluateScript("bar = foo.map(function(n){return n*n})")
println(ctx.get("bar")) // [1, 4, 9]

とあいなります。めでたしめでたし…?


JSにSwiftのクロージャを渡すには?

しかし残念ながら、Objective-Cのid、SwiftのAnyObjectは、関数までは扱ってくれないのです。

ctx.set("add", { (x:Double,y:Double) in x + y }) // error

「(Double,Double)->DoubleはAnyObjectではありませんっ」って怒られちゃいます。

さあ、どうする?

結局こうしました。ちょっと泥臭い方法ですが、ほとんどのシーンでこれで間に合うはずです。

js-Bridging-Header.h に追加

void setB0JSVinJSC(JSContext *ctx, NSString *key,id(^block)());

void setB1JSVinJSC(JSContext *ctx, NSString *key,id(^block)(id));
void setB2JSVinJSC(JSContext *ctx, NSString *key,id(^block)(id,id));

jsc.m に追加

void setB0JSVinJSC(

JSContext *ctx, NSString *key, id (^block)()
) {
ctx[key] = block;
}
void setB1JSVinJSC(
JSContext *ctx, NSString *key, id (^block)(id)
) {
ctx[key] = block;
}
void setB2JSVinJSC(
JSContext *ctx, NSString *key, id (^block)(id, id)
){
ctx[key] = block;
}

Swiftに追加

extension JSContext {

func set(key:NSString, _ blk:(()->AnyObject!)?) {
setB0JSVinJSC(self, key, blk)
}
func set(key:NSString, _ blk:((AnyObject!)->AnyObject!)?) {
setB1JSVinJSC(self, key, blk)
}
func set(key:NSString, _ blk:((AnyObject!,AnyObject!)->AnyObject!)?) {
setB2JSVinJSC(self, key, blk)
}
}

.setが複数あることに着目。こうすることで、単一のメソッドで通常の値も関数も扱えます。

要するに、引数が0,1,2個の場合にそれぞれbridgeするというわけです。

// block w/ no argument

ctx.set("hello") { ()->AnyObject! in
return "Hello, JS! I am Swift."
}
println(ctx.evaluateScript("hello"))
println(ctx.evaluateScript("hello()"))

// block w/ 1 argument

ctx.set("square") { (o:AnyObject!)->AnyObject! in
if let x = o as? Double {
return x * x
}
return nil
}
println(ctx.evaluateScript("square"))
println(ctx.evaluateScript("square()"))
println(ctx.evaluateScript("square(6)"))

// block w/ 2 arguments

ctx.set("multiply") { (o:AnyObject!, p:AnyObject!)->AnyObject! in
if let x = o as? Double {
if let y = p as? Double {
return x * y
}
}
return nil
}
println(ctx.evaluateScript("multiply"))
println(ctx.evaluateScript("multiply()"))
println(ctx.evaluateScript("multiply(6)"))
println(ctx.evaluateScript("multiply(6,7)"))

見ての通りAnyObjectゆえ型チェックと変換の手間は避けられませんが、それを差し引いてもあっけにとられるほど簡単です。

三つ以上の値を渡したいなら、Arrayを経由した方が楽でしょう。

// for any more arguments, just use array instead

ctx.set("sum") { (o:AnyObject!)->AnyObject! in
if let a = o as? NSArray {
// since the NSArray elements are also AnyObject,
// we can't reduce()
var result = 0.0
for v in a {
if let n = v as? Double {
result += n
} else {
return nil
}
}
return result
}
return nil
}
println(ctx.evaluateScript("sum([0,1,2,3,4,5,6,7,8,9])"))

以上の結果は、Githubにもおいときます。

もっとエレガントな方法を見つけたという方は、ぜひご報告を。

Enjoy!

Dan the Polyglot