addEventListenerが虚空に消える話
TL;DR
- ブラウザでのJSの実行順序は難しい
- ありがとう、JS toolchains
- HTMLは偉大
はじめに
とあるアプリでscript tagに書いたJSが動かなくなったので修正しようとなった
構成はLaravel v6 + Vue v2でblade templateとVueで出来ているMPA
コードを見たら、とあるinputのfocusイベントにaddEventListenerしたcallbackが発火してない
よくあるDOMContentLoadedの前にgetElementByIdがnull返してるかと思いきや、consoleを見てもエラーは出ていない 🤔
だが、Chrome devtools のEvent Listenersにも該当のlistenerが登録されていない 🤔
何が起こっているんだ...
とりあえず、該当箇所を雑にdocument.addEventListener('DOMContentLoaded', () => { /* 処理 */ })
にしたら動くようになった
windowのload eventでもOKだった
我々調査隊は真相を調べるためにアマゾンの奥地へと足を向けた...
再現コード
アマゾンを探索した末に現象再現したコードがこちら
<head> </head> <body> <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script> <div id="app"> <form> <input type="text" id="hoge"> </form> </div> <script> const hoge = document.getElementById("hoge") window.debugHoge = hoge hoge.addEventListener("focus", () =>{ console.log("focused!") }) </script> <script> var app = new Vue({ el: '#app' }) </script> <script> const changedHoge = document.getElementById("hoge") console.log("isSame?: ", window.debugHoge === changedHoge) </script> </body>
手元にコピペで index.html
して、python -m http.server 8036
して、 http://localhost:8036 するとただのinputがあるだけのウェブページが表示される
ここのinputにfocusしたときにconsoleに focused!
と表示されてほしいが、それがされないというのが今回の事象
再現してしまえば、発火するはずのEventListenerが虚空に消えた理由は簡単で、Vueがcomponentをregister(用語が合ってるか分からない)した際に、index.htmlのinput ElementとVueの配下になったDOM Elementが別物になってしまったというオチだった
isSame?: false
なので、scriptタグ直書きでキャプチャされているhoge elementとVueの配下になったhoge Elementが違うことが分かる
仮想DOMライブラリの外で起こった出来事は関知できないので、それはそう
この状態なら気づけるが、実際は、Vueインスタンスをnewする部分が別ファイルでdeferされてたりしたので、原因が分からず、最小構成で再現させるのに苦労した
豆知識として以下を得た
- debugging - How to set breakpoints in inline Javascript in Google Chrome? - Stack Overflow
- Node.isEqualNode() - Web API | MDN
- <script>: スクリプト要素 - HTML: HyperText Markup Language | MDN
- defer、inlineだと意味ないの初知り
ちなみに、new Vueの部分をeventListenerより上に持ってくると動くようになる
こんなに雑なHTMLでもきちんとinputが描画されるHTMLとJSの複雑な実行順序が実装されているブラウザの偉大さに改めて感服してしまった
ちなみに、Reactだとこうなる
こうなります
<head> </head> <body> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <div id="app"> <input type="text" id="hoge"> </div> <script> const hoge = document.getElementById("hoge") hoge.addEventListener("focus", () =>{ console.log("focused!") }) </script> <script> const domContainer = document.querySelector('#app'); const root = ReactDOM.createRoot(domContainer); root.render( React.createElement('input', {id: 'hoge'}) ) </script> </body>
これは明らかに後からcallされるrenderの中身にappの中身が書き換わるのが分かりやすい
まとめ
JSの実行順序は難しい
おしまい
best-test-frameworkの話
TL, DR
- 自分だけのJSテストフレームワークを作ろう
- concurrentで動かすと分からん
- jestのmonorepoはよく考えて分けられているので、設計に迷ったら参考にしたい
はじめに
仕事でハンズオンをしている
「君だけのJest、best-testing-frameworkを作ろう」という内容
てくてく進めていくと、jestっぽいフレームワークができていき、nodejsでcli toolを作るときにあるあるなコードが書けるようになるので、ハンズオンとしてめっちゃ良い
今回は、これでconcurrentでハマったのでそれの話
ハマり
ハンズオンに従って、jest-circus
packageを追加して、以下のコードを追加した
const {testResults} = await run(); testResult.testResults = testResults; testResult.success = testResults.every((result) => !result.errors.length);
これで、node index.mjs
を走らせると、FAILするはずのcircus.test.js
がPASSするパターンに4,5回に1回遭遇するようになった
はい、concurrent周りのデバッグ大変なやつです、本当にありがとうございました
というわけで、せっせとparallelらへんのコードをsequencialにします
const hasteMap = new JestHasteMap.default({ extensions: ['js'], maxWorkers: 1, name: 'best-test-framework', platforms: [], rootDir: root, roots: [root], }) const { hasteFS } = await hasteMap.build() const worker = new Worker(join(root, 'worker.js'), { enableWorkerThreads: false, numWorkers: 1, forkOptions: { silent: false } })
workerの数を1個にして、workerのconsole.log
を表示するためにforkOptionのsilentをfalseにする
テストの実行順を固定するためにhasteFSで取得したtest fileの配列をそのままコードに書く
const testFiles = [ '<YOUR PATH>/best-test-framework/tests/01.test.js', '<YOUR PATH>/best-test-framework/tests/02.test.js', '<YOUR PATH>/best-test-framework/tests/03.test.js', '<YOUR PATH>/best-test-framework/tests/04.test.js', '<YOUR PATH>/best-test-framework/tests/05.test.js', '<YOUR PATH>/best-test-framework/tests/06.test.js', '<YOUR PATH>/best-test-framework/tests/mock.test.js', '<YOUR PATH>/best-test-framework/tests/circus.test.js' ]
circus.test.js
の内容はこちら
// tests/circus.test.js describe('circus test', () => { it('works', () => { expect(1).toBe(1); }); }); describe('second circus test', () => { it(`doesn't work`, () => { expect(1).toBe(2); }); });
色々テストファイルを絞ったり増やしたりを繰り返すとcircus.test.js
について以下が分かった
- 単体で実行するとFAILする(期待通り)
- FAILするテスト(
01.test.js
など)のあとに実行するとFAILする(期待通り) - PASSするテスト(
02.test.js
など)のあとに実行するとPASSする(🤔)
テスト実行した際の結果に依存してなんか変わっているらしい
ということでおもむろにjest-circusに飛んで中身を読む
READMEにあるようにflux baseのstate managementをするtest runnerらしい
ファイルを眺めてもeventHandler.ts
とかそれっぽいのが並んでいる
どんなイベントが来ているのか雑にlogを仕込んで走らせてみると3の場合、add_test
イベントらへんのエラーハンドリングコードに引っかかっているっぽい
case 'add_test': { const {currentDescribeBlock, currentlyRunningTest, hasStarted} = state; const {asyncError, fn, mode, testName: name, timeout} = event; if (currentlyRunningTest) { currentlyRunningTest.errors.push( new Error( `Tests cannot be nested. Test "${name}" cannot run because it is nested within "${currentlyRunningTest.name}".`, ), ); break; } else if (hasStarted) { state.unhandledErrors.push( new Error( 'Cannot add a test after tests have started running. Tests must be defined synchronously.', ), ); break; }
hasStarted
がtrueなので、unhandledErrorsに落ちている
ハンズオンのコードに戻って以下のようにやると、きちんとFAILするようになった
const {testResults, unhandledErrors} = await run() testResult.testResults = testResults testResult.success = testResults.every((r) => !r.errors.length) && unhandledErrors.length === 0
こうすると不思議なのが、2の挙動
jest-circus
のrun_start
とrun_finish
イベントは対かと思いきや、run_finish
ではなんのstateも変更されない
case 'run_start': { state.hasStarted = true; global[TEST_TIMEOUT_SYMBOL] && (state.testTimeout = global[TEST_TIMEOUT_SYMBOL]); break; } case 'run_finish': { break; }
つまり、3が起こるなら、2もunhandledErrorsに入ってきて、PASSになるのでは?という疑問
ということでもう一度コードに立ち返ると何が起こっているかが分かる
こちらが01.test.js
expect(1).toBe(2);
このコードをevalすると、特にtest関数やdescribe関数にwrapされているわけでもないので、即座にErrorがthrowされる
するとcatch句に落ちるので、await run()
までたどり着かない
よって、jest-circus
の出番はなく、次のテストファイル(circus.test.js)を実行しにいく
そこにdescribeとtest関数が書かれているので、jest-circus
のstateが変更され、テストがきちんとFAILするという流れになっている
これらが合わさると、冒頭のこれになっていたのでした
これで、
node index.mjs
を走らせると、FAILするはずのcircus.test.js
がPASSするパターンに4,5回に1回遭遇するようになった
ちなみに、ここまで色々やってきたがこの挙動を期待通りにするにはjest-circus
からresetState
関数をimportしてきてevalの前に実行するようにすれば良い
try { resetState() eval(code) const {testResults, unhandledErrors} = await run()
最後に
ハンズオンをやっている最中に気になったことを深ぼったら、jestのstate managementのコードを読みに行くことになって楽しい
flux baseのstate management、ウェブアプリケーションだけじゃなくこういうところにも使われているという身近な例でした
おしまい
スタックとヒープの違いにはまった話
TL; DR
- ヒープとスタックは違う
- メモリは0と1
- gdb楽しい
はじめに
以前に引き続き、C compilerを作っている
ポインタが思いの他簡単に実装できて感動していたら、ポインタと数字の加減算でめちゃくちゃにハマった
あっちこっちデバッグして、原因を探っていったら飛ぶように1週間ほど経っていたが、さきほど原因が判明してめっちゃ感動して、この記事を書き出した
書き上げるのに2日ぐらいかかってしまった
原因を探っていく過程で、スタックマシンとメモリについての理解が深まった
その記録
簡単なポインタの加減算
実装しようとしたのは、compiler bookのここ
「まだ配列がないので、mallocする」と書いてあったのでmalloc関数を使うのかと思いきや、mallocを使って配列をアロケートする方法がよく分からず肝心のテストが書けない
秘伝のあんちょこ、本家9ccのcommit logをたどってそれっぽい機能を実装したcommitを眺める
配列を宣言して、そこに値を代入すればいいと分かったので真似してテスト用の関数を書く
int *alloc2(int x1, int x2) { static int arr[2]; arr[0] = x1; arr[1] = x2; return arr; }
これで以下の簡単なポインタの加減算は動くようになった
int main() { int *p = alloc2(3, 6); return *(p + 1); // 6 }
ここまではそんなにかかっていない
すこし複雑なポインタの加減算
bookに書いてあるテストケース例ではテスト用関数から返り値でポインタを返すのではなく、アサインしたいポインタを渡して関数内でアサインする必要があったので、以下のテスト用関数を書いた
void allocp4(int **p, int x1, int x2, int x3, int x4) { int arr[4]; arr[0] = x1; arr[1] = x2; arr[2] = x3; arr[3] = x4; *p = arr; return; }
友人に指摘されるまでポインタのポインタ(int **p)という概念が出てこず、compileが通らなくてハマった
ポインタは難しい
このときにstaticが必要ないのでは?と思って、試しに外してテストを書いてみたのが、この後の大ハマりの原因になる
テストケース例のコード
int main() { int *p; allocp4(&p, 1, 2, 4, 8); int *q; q = p + 3; return *q; // 実行ごとに不定の値 🤔 }
明らかにポインタが指している値がおかしなことになっている
こういうときはとりあえず、シンプルなものに書き直して様子を見るに限る
シンプルにしたテスト関数
void allocp2(int **p, int x1, int x2) { int arr[2]; arr[0] = x1; arr[1] = x2; *p = arr; return; }
テストケース
int main() { int *p; allocp2(&p, 30, 31); int *q; q = p + 1; return *q; // -> 0 🤔 }
31になってほしいが、思ったとおりにいかない🤔
加算する値を0にしても、返ってくる値は0のままで特に変化なし
加算する値を2以上にすると、実行ごとに変わる不定の値になって想定通り
もう1つシンプルにする
int main() { int *p; allocp2(&p, 30, 31); return *(p + 1); // -> 31 👍 }
これは想定通りに動く
動いたので1つ複雑度を上げる
int main() { int *p; int q; allocp2(&p, 30, 31); q = *(p + 1); return q; // -> 0 🤔 }
qをポインタではない普通の変数で宣言したが、やはり動かない
このあたりで変数宣言が実装していないのに初期化式が動いていたことを思い出して、きちんと実装し直す
しかし、結果は変わらない
ここまでで5日ぐらいかかっている
Assemblyのデバッグ
ここまでテストケースのcompileそのものは成功している
compile段階でSEGVしてくれた方が原因の特定はしやすい
思い当たる節もないので、地道にparserの出力したASTがおかしくないか、codegenでassebmblyにどのnodeから生成されたかのコメントもつけて書き出してみる
特におかしいところは見受けられない
ここまでやって打つ手がなくなったので、ついに出力されたassemblyを下の方から直接編集して途中の結果を雑にretしてどこからおかしいのか確認した
gdbを使わなかった理由は使い方がよく分からなかったため
すると、おかしくなっている行を特定できた
以下が出力されたassemblyの一部
... call allocp2 push rax # ND_ASSIGN start gen_lbal lhs mov rax, rbp sub rax, 16 push rax # ND_ASSIGN end gen_lbal lhs # ND_ASSIGN start gen rhs # ND_DEREF start # gen lhs mov rax, rbp sub rax, 8 push rax pop rax mov rax, [rax] push rax # <== A1 ==> # gen rhs push 1 # <== A2 ==> pop rdi pop rax push rax mov rax, 4 imul rdi, rax pop rax add rax, rdi ...
A1の時点では*(p + 1);
のうち、pの値、つまりpの指すメモリアドレス(pはポインタなので)がスタックトップに積まれている
なので、以下をA1に挿入すると、pが指しているアドレスの値をretできる
pop rax mov rax, [rax] mov rsp, rbp pop rbp ret
これは想定通りに30
が返ってくる
次にA2の時点では1をスタックにpushしている
ということで、2回popして値を見れば同じように30
が返ってくるはず
pop rax pop rax mov rax, [rax] mov rsp, rbp pop rbp ret
ところが、結果は1
push 1
で pの指すメモリアドレスが1に書き換えられてしまっているらしい
ここまで特定できたので、ようやくgdbの使い方を少し調査してメモリアドレスの変化を観察することにした
調査に使った資料はこのあたり
- gdbを使う - Qiita
- gdbの主要コマンド
- gdbの使い方のメモ - ももいろテクノロジー
- gdbでアドレスが指す先のアドレスをdereferenceするコマンドを作った - φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ
おもむろにgdb ./tmp
でgdbを起動、disp/i $pc
でステップ実行ごとに次に実行されるassemblerを表示するようにして、b main
でmainの定義位置にbreak pointを置いて、いざrun
する
disas
して、assemberを表示し、アドレス指定でallocp2の呼び出しと先程のA1の直前にbreak pointを設定する
最初のbreak pointでのrbpとrspのアドレスと値
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd900 0x7fffffffd900
c
して、allocp2の呼び出しにいって、ni
して、rbpとrspを確認
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd8f0 0x7fffffffd8f0
$rbpからそれっぽいところまでメモリを10進数出力して対応を見てみる
現状の9ccsは変数のメモリを決め打ちで$rbpから8byteのoffsetで置いてるのでこんな感じ
(gdb) x/-80bd $rbp + 8 0x7fffffffd8b8: -25 -40 -1 -1 -1 127 0 0 0x7fffffffd8c0: 31 0 0 0 30 0 0 0 0x7fffffffd8c8: -8 -40 -1 -1 -1 127 0 0 0x7fffffffd8d0: 30 0 0 0 31 0 0 0 0x7fffffffd8d8: 0 110 9 127 53 41 -38 58 0x7fffffffd8e0: 0 -39 -1 -1 -1 127 0 0 0x7fffffffd8e8: 126 81 85 85 85 85 0 0 0x7fffffffd8f0: -16 -39 -1 -1 -1 127 0 0 <- q, $rsp 0x7fffffffd8f8: -48 -40 -1 -1 -1 127 0 0 <- p 0x7fffffffd900: 0 0 0 0 0 0 0 0 <- $rbp
30と31とallocp2で配置されたらしき数字が確認できる
0x7fffffffd8f8
の値はpointerのpで10進数ではなくアドレスなので確認してみる
(gdb) x/a 0x7fffffffd8f8 0x7fffffffd8f8: 0x7fffffffd8d0
確かにアドレスが入っていて、0x7fffffffd8d0
を指している
さっき出力したメモリを見ると確かに30を指している
せっかくgdbがexpressionが使えるので、$rbpからのオフセットでも見てみる
(gdb) x/a $rbp - 8 0x7fffffffd8f8: 0x7fffffffd8d0 (gdb) x/db *(int **) ($rbp - 8) 0x7fffffffd8d0: 30
この時点ではきちんと30が入っている
では c
して、A1の直前のbreak pointまでいって、ni
する
rbpとrspを確認
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd8d8 0x7fffffffd8d8
10進数出力
(gdb) x/-80bd $rbp + 8 0x7fffffffd8b8: -25 -40 -1 -1 -1 127 0 0 0x7fffffffd8c0: 31 0 0 0 30 0 0 0 0x7fffffffd8c8: -8 -40 -1 -1 -1 127 0 0 0x7fffffffd8d0: 30 0 0 0 31 0 0 0 <- *p 0x7fffffffd8d8: -48 -40 -1 -1 -1 127 0 0 <- $rsp 0x7fffffffd8e0: -16 -40 -1 -1 -1 127 0 0 0x7fffffffd8e8: 0 0 0 0 0 0 0 0 0x7fffffffd8f0: -16 -39 -1 -1 -1 127 0 0 <- q 0x7fffffffd8f8: -48 -40 -1 -1 -1 127 0 0 <- p 0x7fffffffd900: 0 0 0 0 0 0 0 0 <- $rbp
スタックが伸びて$rspがpの指しているポインタの直前まで来ている
(gdb) x/db *(int **) ($rbp - 8) 0x7fffffffd8d0: 30
まだこの段階では*p
は30だが、大変不穏な気配
ではいざ問題のA2のinstruction、つまり、push 1
に進めるためにまたni
する
rbpとrspを確認
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd8d0 0x7fffffffd8d0
メモリダンプ
(gdb) x/-80bd $rbp + 8 0x7fffffffd8b8: -25 -40 -1 -1 -1 127 0 0 0x7fffffffd8c0: 31 0 0 0 30 0 0 0 0x7fffffffd8c8: -8 -40 -1 -1 -1 127 0 0 0x7fffffffd8d0: 1 0 0 0 0 0 0 0 <- *p, $rsp 🤯 0x7fffffffd8d8: -48 -40 -1 -1 -1 127 0 0 0x7fffffffd8e0: -16 -40 -1 -1 -1 127 0 0 0x7fffffffd8e8: 0 0 0 0 0 0 0 0 0x7fffffffd8f0: -16 -39 -1 -1 -1 127 0 0 <- q 0x7fffffffd8f8: -48 -40 -1 -1 -1 127 0 0 <- p 0x7fffffffd900: 0 0 0 0 0 0 0 0 <- $rbp
push 1
はcompiler bookにもあるように以下と同じ
sub rsp, 8 mov [rsp], 1
ということで、 pの指していたアドレスの値が1に書き換わってしまった
念の為、オフセットでも確認しておく
(gdb) x/db *(int **) ($rbp - 8) 0x7fffffffd8d0: 1
やはり、1になってしまっている
そして、この後は、pの指しているアドレスに対して +4 するので 結果 p + 1
が 0x7fffffffd8d4
になる
ここは、さっき1に書き換わったところなのでderefすると値が0になる
そして、qに0が代入されて0がreturnされるという挙動になっていた
ということで、stackが書き換わるのを見て「これ、stackとheapの違いというやつでは?」と気づく
staticをつける
ここまでの調査からして、自分で書いたcompiler部分ではなくテスト関数のallocp2の実装がおかしいのは明らかだったのでとりあえずそっとstatic
を戻してみた
void allocp2(int **p, int x1, int x2) { static int arr[2]; arr[0] = x1; arr[1] = x2; *p = arr; return; }
int main() { int *p; int q; allocp2(&p, 30, 31); q = *(p + 1); return q; // 31 👍 }
同じようにA1の直後のダンプたち
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd8d8 0x7fffffffd8d8 (gdb) x/-80bd $rbp + 8 0x7fffffffd8b8: -25 -40 -1 -1 -1 127 0 0 0x7fffffffd8c0: -26 -40 -1 -1 -1 127 0 0 0x7fffffffd8c8: -51 83 85 85 85 85 0 0 0x7fffffffd8d0: 31 0 0 0 30 0 0 0 0x7fffffffd8d8: 32 -128 85 85 85 85 0 0 <- $rsp 0x7fffffffd8e0: -16 -40 -1 -1 -1 127 0 0 0x7fffffffd8e8: -8 -40 -1 -1 -1 127 0 0 0x7fffffffd8f0: -16 -39 -1 -1 -1 127 0 0 <- q 0x7fffffffd8f8: 32 -128 85 85 85 85 0 0 <- p 0x7fffffffd900: 0 0 0 0 0 0 0 0 <- $rbp (gdb) x/a ($rbp - 8) 0x7fffffffd8f8: 0x555555558020 <arr.2155> (gdb) x/db *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 30
pの指すアドレスがRBPの近く(スタック)ではなく、まったく違う場所(ヒープ)を指していることが分かる
ということで、A2の直後のダンプ
(gdb) i r rbp rsp rbp 0x7fffffffd900 0x7fffffffd900 rsp 0x7fffffffd8d0 0x7fffffffd8d0 (gdb) x/-80bd $rbp + 8 0x7fffffffd8b8: -25 -40 -1 -1 -1 127 0 0 0x7fffffffd8c0: -26 -40 -1 -1 -1 127 0 0 0x7fffffffd8c8: -51 83 85 85 85 85 0 0 0x7fffffffd8d0: 1 0 0 0 0 0 0 0 <- $rsp 0x7fffffffd8d8: 32 -128 85 85 85 85 0 0 0x7fffffffd8e0: -16 -40 -1 -1 -1 127 0 0 0x7fffffffd8e8: -8 -40 -1 -1 -1 127 0 0 0x7fffffffd8f0: -16 -39 -1 -1 -1 127 0 0 <- q 0x7fffffffd8f8: 32 -128 85 85 85 85 0 0 <- p 0x7fffffffd900: 0 0 0 0 0 0 0 0 <- $rbp (gdb) x/a ($rbp - 8) 0x7fffffffd8f8: 0x555555558020 <arr.2155> (gdb) x/db *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 30
最初に動いていたポインタとINTの演算も命令数が少なく、Stackが破壊されないのでたまたま動いていただけなのであった
めでたしめでたし
最後に
自分で書いてきたコンパイラだが、ポインタを扱いだしたあたりからなぜ動いてるのかうまく説明できる自信がなかった
今回のハマりはstaticを不用意に消すというポカから発生したが、結果的にgdbで遊んだりメモリの中身を見て、スタックマシンの概念をより具体的に理解できたので良かった
compiler bookのコラムにあったスタックの伸び方の話や、スタックとヒープが違うものだということがだいぶ体感できた
レジスタとメモリをつかったスタックマシン、本当に人類の叡智の結晶という感じがしてすごい(こなみ
おしまい
コラム: メモリは0と1
メモリに置いてある値を解釈するという概念が難しくて、pointerの指している値を出すのにめっちゃ苦労した
例えばこれ
(gdb) x/db *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 30
これは、$rbp - 8
をintへのポインタとして解釈して、 その値をd(ecimal)でb(yte)で区切って表示する
これをこうすると謎の巨大数値がでてくる
(gdb) x/dg *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 133143986206
これは、b(yte)がg(iant word)になって、64byteで解釈されている
実体的にはこう
(gdb) x/tg *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 0000000000000000000000000001111100000000000000000000000000011110
tはbinaryなので、binary表現になった
これだと長すぎて分かりづらいので分割
(gdb) x/2tw *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 00000000000000000000000000011110 00000000000000000000000000011111
w(ord)なので32byteで区切った
もうちょい区切る
(gdb) x/8tb *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 00011110 00000000 00000000 00000000 00011111 00000000 00000000 00000000
ここまでくると一番最初の30
の正体が判明して 0x555555558020
から 8byte取ってきて10進数で解釈した 00011110
の値となる
ちなみに、byteでもhalfwordでもwordでも同じ
ただ、gにすると64byteになって変わる
(gdb) x/xg *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 0x0000001f0000001e (gdb) x/dg *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 133143986206
これで、133143986206
の正体も判明して、0x555555558020
から64byte取ってきて10進数で解釈するとこんな巨大な値になった
16進数だとだいぶ分かりやすい
コードを書いているとあまりに自然に10進数が使えるので忘れがちだけど、メモリがあくまで0と1を記憶しているだけのもので、工夫で10進数を扱えるようにしているというのがこれでしっくりきた
コラム2: メモリの値確認
xでメモリの値を確認するのがめっちゃ大変
上の例と同じくこう
(gdb) x/db *(int **) ($rbp - 8) 0x555555558020 <arr.2155>: 30
$rbp - 8 が指してるアドレスが知りたい
(gdb) x/a ($rbp - 8) 0x7fffffffd8f8: 0x555555558020 <arr.2155>
0x7fffffffd8f8
が指してるアドレスが指している値が知りたい
(gdb) x/a 0x7fffffffd8f8 0x7fffffffd8f8: 0x555555558020 <arr.2155>
これはアドレスそのもので、アドレスが指している値じゃない
(gdb) x/db *(0x7fffffffd8f8) 0x55558020: Cannot access memory at address 0x55558020
だめ
(gdb) x/db int 0x7fffffffd8f8 A syntax error in expression, near `0x7fffffffd8f8'.
だめ
(gdb) x/db * (int *) 0x7fffffffd8f8 0x55558020: Cannot access memory at address 0x55558020
だめ
(gdb) x/db * (int **) 0x7fffffffd8f8 0x555555558020 <arr.2155>: 30
できた
Cの有効な文法を書くのに未だに慣れない
特にポインタのポインタみたいなのが難しい
docker compose cliでecsにデプロイした話
TL; DR
docker compose ecs integrationとは
- Deploy applications on Amazon ECS using Docker Compose | Containers
- Docker Compose for Amazon ECS Now Available - Docker Blog
2020年11月に発表されて、待望の機能が来たと結構な反響があったように思います
仕事でPhoenixのアプリを作る機会があり、そこでこのスタックを採用したのでハマったところを紹介していきます
概要、必要なこと、アーキテクチャなどはほぼ以下のドキュメントに書いてあるのでそちらをご覧ください
ふわっとまとめると、docker-compose.ymlに基づいてAWSのリソースを良い感じに用意してくれるコマンドです
ハマったところ
ECSって何、ECRって何
本当に無知でdockerをデプロイできるサービスぐらいにしかECSを理解していなかった
そのため、ECRって何という状態だった
- ECSはElastic Container Service
- ECRはElastic Container Registry
ECRはDocker HubみたいにDocker imageをhostingできるサービス
ECSはcluster
を作成して、task
を定義して、service
を実行するものっぽい
clusterはEC2なんかのコンピューティングリソース
taskはその名の通り仕事、実体的には単数 or 複数のdockerのcontainer定義
serviceはtaskをclusterで何個走らせるかを決める
これらを統合して、docker compose cliを使うとデプロイはこんな感じになるはず
- サーバーを書く
- buildしてDocker imageに固める
- ECRなどのDocker image hosting serviceにpushする
docker compose up
port:80が使えない
Unix系のOSにて、デフォルトでは1024以下のportはroot権限がないと使用できないらしい
- javascript - Error: listen EACCES 0.0.0.0:80 OSx Node.js - Stack Overflow
- unix - Why are ports below 1024 privileged? - Stack Overflow
これを回避する方法は以下
- nginxでリバースプロキシを立てて、アプリサーバーを直接ALBに紐付けない
- rootユーザーでアプリサーバーを起動する
- port mappingする
2はセキュリティ的に推奨される感じではなさそうです
3はecs integrationではサポートされていません
このあたりはいかに詳しいです
- ECS integration Compose features | Docker Documentation
- ECS integration composefile examples | Docker Documentation
- Docker-ECS load balancer, HTTPS and routing · Issue #1472 · docker/compose-cli · GitHub
別のcontainerで立つ以上portのabstractionは利がない & compose specがroutingに関することに関与しないのでECS integrationでもそれを踏襲する方針のようです
この事実が分かるまで、いつものlocal開発向けのdocker-compose.ymlのように80:4000
をやろうとしてdocker compose up
でエラーを踏んでissueを漁りドキュメントを読むというのを繰り返しました
ということで、素直に1でいきます
nginx.confを書いて、アプリサーバーと同じくbuildしてECRにpushして解決
その際に、後述のサービス検出の話でハマりました
サービス検出
英語だとService Discovery
恥ずかしながらService Discoveryが何を指しているかさっぱり分かっていなかったが少し理解した
nginxでリバースプロキシを立てたが、どうやってnginxに来た通信をpPhoenixのアプリサーバーに向けるのかという話
つまり、ECSでcontainer間通信を実現する方法が分からなくてハマりました
実はドキュメントに書いてあります
Deploying Docker containers on ECS | Docker Documentation
Services are registered automatically by the Docker Compose CLI on AWS Cloud Map during application deployment. They are declared as fully qualified domain names of the form:
<service>.<compose_project_name>.local.
app.hoge-project.local
のようなドメインでサービスのcontainer間で通信ができるようになります
当初やりたいことがService Discoveryであることが分かっていなかったため、いろんなクエリでググった末に以下のページにたどり着いてようやく理解できました
ということでnginx.confにphoenixのアプリサーバーのドメイン名を書いて完了
Migration
これはハマったというよりもハックで乗り切った感じの話
基本的にサーバーを書いていると発生するMigrationですが、それをECSのサービスとして定義するとdocker compose cliの仕様上、ちょっと困ったことになります
docker compose convert
をすると分かりますが、service.deploy.replicas
に値を設定しないとECSのサービス定義のdesiredCountは1にデフォルトで設定されます
そのため、DB migrationのサービスが起動 -> 正常終了 -> desiredCountが1なので、再度起動 -> 正常終了のループを延々と繰り返します
最初はdocker compose conver
で生成されたCloudFormation templateをカスタマイズしようと思いましたが、上手いこといかなかったので方針を切り替えました
結果、docker compose upを走らせた後に、migrationの終了を確認してからマネジメントコンソールでNumber of Taskを0にするように手動で回避しています
この辺、上手い方法があったら教えていただけると 🙏
Command support
ecs contextでsupportされているコマンドは筆者が試した限りでは以下だけでした。
- docker compose up
- docker compose down
- docker compose logs
- docker compose convert
runが使えないので上述のDB migrationで困ったりしてました
runはsupportしてほしいという要望が既に上がっています
Add docker run support to ECS · Issue #829 · docker/compose-cli · GitHub
ちなみにdocker compose down
をするとupで作成されたすべてのAWSリソース(既存のリソースは除く)が破棄されるのでお気をつけください
既存のAWSリソースの使用
ここに説明があります
Deploying Docker containers on ECS | Docker Documentation
VPCだけでいけるかと思いましたが、ドメインのヒモ付のため、LBも既存のものを使うようにしました
最後に
docker compose ecs integration、まだ荒削りな部分もありますが、production運用も十分に視野に入ってくると思います
もちろん、凝ったことをやろうとすると足りない部分も多いのでそのあたりはissueに起票したりでfeedbackしていくと良さそうです
これで、好きな言語やフレームワークでサーバーをcontainerにしてデプロイするのが簡単になりました
どんどん使っていきたいところ
おしまい
「低レイヤを知りたい人のためのCコンパイラ作成入門」をやっている話
TL; DR
はじめに
1年ぐらい前にこれが公開されて、ずっとやりたいと思っていたがやれてなかったので今年の2月ぐらいから地道に進めていた。
やっとこさ関数を定義して、関数を呼び出せるようになったのでここにいたるまでの振り返り
成果物はこれ
やって楽しいポイント
loxのときのように別の言語で実装しても良かったが、Cをほとんど書いたことがなかったので、紹介されている通りCで実装した。
assemblyとかレジスタはそもそもよく分かってなかったので、基礎の基礎から説明されてて楽しい。
tokenizeらへんはloxで一度通っているのですっと頭に入ってくる。再帰下降構文解析とBFNらへんは何度やっても人類の叡智という感じがする。
スタックマシンが実際にどうやって使われてるかもようやっと少し理解できた。
makeの使い方も触れるしよい。
途中に挟んであるコラムが面白いので、コンパイラを実際に作らないでも読み物として楽しむだけでも面白い。
Cの歴史も少し触るので、現在から見ると明らかに不自然に感じる文法が採用された時代背景や思想、普段使っているJSなんかが高級言語であることがなんとなく分かる。
ハマったポイント
どのへんにハマったかをつらつら書く
ここに取り上げていないものはREADMEに書いてある。
Cのコードで困ったら9cc本体を見れば雰囲気は分かるので、それでなんとかなる。
GitHub - rui314/9cc: A Small C Compiler
アセンブラで詰まったら付録1を見たり、Compiler Explorerと自分の出力したアセンブラを比較したりすると分かる。
アセンブラは手で書き換えて、gdbで行実行しつつ、レジスタの中身を確認するとより分かりやすかった。
SIGSEGV
友人曰く、C言語名物らしい。
初めて出たとき、例のアレじゃんとテンションが上がった。
とはいえ、それも最初だけで、実装を進めていると頻繁に出て来る上に、デバッグが非常に困難なのでこれは嫌になるわけですわという気持ちになった。
デバッグが非常に困難というのは、こいつが出ると普段JSや他の言語なんかで使うprintなど一切の出力がされなくなる。
JSのthrow errorと同じように発生したソースのlineとなんで発生したかも出るだろうし、そこまでに実行されるprintfなんかの処理は出力されるだろうと油断していたのでこれには真顔になってしまった。
仕方ないので記憶の片隅からgdbというツールでデバッグ出来たはずと思い出し、それの使い方を覚えて、ある程度はなんとかなるようになった。
gdbは便利。もう手放せなくなってしまった。
このあたりを一通り踏んだので、Rustがsafeな書き方をしているとSegmentation faultが出ないというのがどれだけありがたいかを体感できるようになった。
インクリメントの前置と後置
トークナイザを実装しているときにインクリメントの後置と+=
の演算子が同じ意味だと勘違いしていて、トークナイザが上手いこと動かなくなった。
これは、JSでも同じ仕様。
let x = 3; const y = x++; console.log(`x:${x}, y:${y}`); // expected output: "x:4, y:3" let a = 3; const b = ++a; console.log(`a:${a}, b:${b}`); // expected output: "a:4, b:4" let p = 3; const q = p+=1 console.log(`p:${p}, q:${q}`) // expected output: "p:4, q:4"
インクリメントの前置と+=
が同じ意味になる。
フィボナッチ数列の実行速度の比較
関数定義ができるようになって、再帰でフィボナッチ数列が動くようになった記念で、以前作ったlox compilerと実行時間を比較してみた。
以前のloxの記事はこれ
Rustでlox言語のinterpreterを作っている話 - sasurau4のブログ
まずはlox版のフィボナッチ数列の実装
fun fib(n) { if (n < 2) return n; return fib(n - 1) + fib(n - 2); }
9ccsのフィボナッチ数列実装
fibonacci(x) { if (x < 2) return x; return fibonacci(x - 1) + fibonacci(x - 2); }
実装されてる言語機能がほぼ一緒なので、実装もほぼ同じものになる。
こちらが実行にかかった時間
実行関数 | 9ccs | lox compiler |
---|---|---|
fib(20) | 0.002 sec | 1 sec |
fib(30) | 0.04 sec | 56 sec |
fib(38) | 0.6 sec | 2721 sec |
fib(40) | 1.5 sec | -- (長すぎて未測定) |
fib(50) | 191 sec | -- (長すぎて未測定) |
9ccsはtimeコマンドで計ったのに対して、loxは実装したclock関数で測定していて、双方とも1回ずつしか実行していないので、正確な比較ではないが、ざっくり比較でもこれぐらい差がある。
引数が38と中途半端なのはloxの39の実行終了に時間がかかりすぎて、途中で打ち切ってデータが取れた最後の値が38だったため。
9ccsの方が圧倒的に早いが、それでもこの実装だと引数60で30分程度はかかりそうな気配がする。計算量の爆発怖い。
テスト
フィボナッチ数列で比較するために久しぶりにlox compilerを動かしたがfor文が動かなくなっていて悲しかった。
Cコンパイラ作成入門ではきちんとテストケースを書いて、毎回テストケースが通るように実装しているので、こういうことがない。
テスト書くの大事。
最後に
セルフホストまでは行けるかわからないが、本に紹介されているテストケースをCで書き直すまではやりたい。
とりあえず、ポインタが扱えるようになるとぐっとCっぽくなる気がするので、次はそれをやる。
おしまい。
@babel/core@7.13.7とnode-semver@7.0.0とwebpackを併用してハマった話
TL;DR
@babel/core@7.13.7以下をwebpackと一緒に使うとnode-semver@7.0.0でハマるかもしれない
はじめに
仕事でブラウザ上のエディタでmarkdownを書くとリアルタイムにReactでどんなコンテンツが表示されるかを確認できるコンテンツシミュレーターみたいなものを使っている。
このシミュレーターで書けるmarkdownは本来のmarkdownをちょっと拡張して、独自にJS書けたり、インラインでJS関数やオブジェクトの参照ができたりする。
このシミュレーターが既存のPJでは動いているのに、別のPJで新しくセットアップしようとしたら動かないという事象を調査したら、意外なところまで行ったのでその経緯と顛末を書く
発端
あるPJ(以下A PJ)でこんな感じのmdを書くとシミュレーターのコンパイルが失敗するという事象で困ってると相談を受けた。
こんにちは、${context.name}さん。
->
JavaScriptの構文エラーが発生しました ...(Trace的なやつ)
本当はこうなってほしい
こんにちは、${context.name}さん。
->
こんにちは、田中さん
コンテンツを以下のようにすると、特に問題なくシミュレーターが動くので、どうも${context.name}
のところのJSの評価がうまく動かないらしい
こんにちは、世界
なぜエラーになるのか
このシミュレーターの仕組みは、エディタ上で与えられたmdコンテンツをworkerでコンパイルして、JSを評価して表示するコンテンツを生成する。
コンパイルでmarkedとbabelを使っている。
表示するためにReactのコンポーネントを書いておくという感じ。
とりあえずTraceが出てるので、宇宙より壮大なnode_modules配下のbabelされたりしたJSたちにひたすら潜ってlogを挿したりしながら、エラーメッセージをたどっていく。
これが宇宙の旅?
すると最終的に以下に辿り着いた
transform error Error: [BABEL] /[anonymous]: Cannot find module './functions/satisfies' (While processing: "base$0") at webpackEmptyContext (eval at ../../node_modules/@babel/core/node_modules/semver sync recursive (82363a058d1f41f6955e.worker.js:1872), <anonymous>:2:10) at lazyRequire (index.js?0488:5) at Object.get [as satisfies] (index.js?0488:12) at Object.assertVersion (config-api.js?bf03:91) at eval (index.js?8294:252) ...
なんだか、@babel/coreが抱えているsemverでmoduleが読み込めないらしい
node_modulesの宇宙からsemverのpackage.json
を見つけ出してmainのindex.js
を読んでみる
const lrCache = {} const lazyRequire = (path, subkey) => { const module = lrCache[path] || (lrCache[path] = require(path)) return subkey ? module[subkey] : module } const lazyExport = (key, path, subkey) => { Object.defineProperty(exports, key, { get: () => { const res = lazyRequire(path, subkey) Object.defineProperty(exports, key, { value: res, enumerable: true, configurable: true }) return res }, configurable: true, enumerable: true }) } lazyExport('re', './internal/re', 're') ...(略) lazyExport('satisfies', './functions/satisfies') ...(略)
lazyyRequireの機構を自前で実装しているらしい。
logを差し込んで試すと、const module = lrCache[path] || (lrCache[path] = require(path))
がORの右辺に落ちてrequire(path)
でmoduleが見つからずにエラーになっているっぽい。
bundle時に静的に決まっていないrequireをbundlerで解決できないのはそれはそうという感じ(webpackの挙動に詳しくないので違うかもしれない)
なぜ動いているのか
エラーの原因は分かったが、今度はなぜ以前設定したPJ(以下B PJ)では今でも動いているのか謎なので同様に調査
semberのpackage.json
のmainにあるsemver.js
がこちら
exports = module.exports = SemVer var debug /* istanbul ignore next */ if (typeof process === 'object' && process.env && process.env.NODE_DEBUG && /\bsemver\b/i.test(process.env.NODE_DEBUG)) { debug = function () { var args = Array.prototype.slice.call(arguments, 0) args.unshift('SEMVER') console.log.apply(console, args) } } else { debug = function () {} } ...(略)
なるほど、全然違う
というかmainに書いてある値が違うのでsemverのバージョン違いが原因らしい
そのsemverはどこからやってきたのか
エラーになるA PJのsemver
のversionは7.0.0
これは、@babel/core
の7.13.1
から指定されている
エラーにならないB PJのsemverのversionは5.7.1
これは、@babel/core
の7.12.3
から指定されている
@babel/coreが上がってるのは気づいていたが、minor versionだから大丈夫やろと油断していた
いつ上がったのか@babel/coreのリリースブログやchangelogを読んでみるが、それっぽい記述がない
blameするとこいつで変わったらしい
BABEL_8でbreakingするよ的なflagが立っていて、@babel/core
のv7.13系からsemver
がv7系に上がっている
これがPR
PRマージされた後に、@babel/coreとwebpackを併用するとsemverが7.0.0に固定されてissueが起こると書いてある
このコメント、ブラウザでコードエディタやってて@babel/core使ってると書いてあるので、まったく同じユースケースでびっくり
そのあとsemver@v7.1系を使うとBREAKINGなので、6.3系にダウングレードするPR出されて、まだリリースされていない。これが2日前。
ということで、次のbabelがリリースされたら解決しそう
おまけ1: semverのpreload機構
2019年12月ぐらいからちょこちょこissueにはなっていたらしい github.com
ドキュメントが更新されたりしていたが、以下のPRで廃止になっている
おまけ2: semverのsemver
node-semverのレポジトリを調査していたら、node-semverがsemverに従ってないというコメントがついていて、メンテナからするとそんなことはないよなあと思ったので貼っておく
おわりに
ということで、@babelのv7.13系の踏むのが難しいバグを踏み抜いたお話でした。
@babelのv7.13.0は2021-02-22にリリースされて、まだ4日しか経っていないのにもうv7.13.7なので、すぐにfixされるでしょう。
@babelもnode-semverももともとはnode.jsで使うことを想定されていて、ブラウザ環境で使われるのはエコシステムが発展したが故なので、この辺にハマるときはきっちりハマります。
メンテナはユースケースが複雑なので非常に大変だと思いますが、メンテしてもらっている身だとありがたい限りです。自分もブログを書いて他にハマる人がいたときの一助になればよいなと思いつつ、おしまい。
JSでnew Functionの使いどころ
はじめに
markdownを書くと、ソシャゲみたいにキャラと対話してるUIが作れるというクローズドなライブラリを仕事でメンテしています。
ADVゲームのシナリオをエンジニアじゃなくても書けるようにするやつですね。
それのメンテでJSのFunctionのconstructorを使う機会があったので、それの経緯を書き連ねていきます。
markdownでシナリオ
markdownでシナリオがどういう感じかというとこんな感じ。
# first-conversation こんにちは、${context.name}さん。 このトークはサンプルです。 # single-select-list 元気ですか? 選択肢を選んでください。 - [元気です](multiple-select-list?mt.fine=1) - [元気ではないです](multiple-select-list?mt.fine=2) # multiple-select-list 複数選べる選択肢はこんな感じになります。 - [ ] [お好み焼き](?liked=お好み焼き) - [ ] [**たこ焼き**](?liked=たこ焼き) - [ ] [もんじゃ焼き](?liked=もんじゃ焼き)
H1ごとに段落になっていて、H1の内容は内部的なidとして使われるだけで会話の内容には影響がありません。
markdownといいつつ、独自記法を含んだmarkdownっぽい何かです。
例えば${context.name}
は、対話のためのReact componentに渡すpropsの中にあるcontextオブジェクトの値を参照してこんにちは、fooさん。
になります。
ここで内部的にはeval()
を使っていました。
hermes
この対話エンジン、reactで動くことはもちろん、react-nativeでも、cliでも動きます。
ところで、Facebookは2019年7月にreact-native向けにhermesというJS engineを発表しました。
hermes、今のところはAndroidでopt inして始めて使えるエンジンですが、react-native-reanimatedのv2-alphaがhermesしかサポートしていなかったり、react-native本体でもiOSでhermesを動かせるようにするPRが作られていたりとecosystemがhermesを前提にしてきそうな動きがあります。
もちろん、すぐにhermesがデフォルトになるということはなさそうですが、hermesをonにしたときのAndroidのパフォーマンス向上は捨てがたいです。
ということであるプロジェクトで無邪気にhermesを有効化したんですが、react-nativeのアプリがクラッシュするようになりました。
クラッシュメッセージはUncaught ReferenceError: Property 'context' doesn't exist
なんでや。
evalの挙動
react-nativeは、普段はJavaScriptCore(Safariに積まれてるやつ)でJSを動かしています。なのでWebで動くものは基本的に動きます。
ところが、hermesはreact-native向けに新しく開発されたエンジンなので、一部機能が動きません。また、ドキュメント上でサポートから除外される機能が列挙されています。
Local mode eval() (use and introduce local variables)
犯人はこいつでした。
Local mode eval()とはなんぞやということで、困ったときのMDN。
eval 関数を eval 以外の名前を参照して呼び出すことで間接的に使用した場合、ECMAScript 5 以降ではローカルスコープではなくグローバルスコープで機能します。これは例えると、関数定義によりグローバル関数が作成されるため、評価されたコードはその呼び出されたスコープ内のローカル変数にアクセスできなくなる、ということです。
つまり、evalを直接呼び出したときはlocal scopeを参照しますが、別名をつけて間接的に呼び出すとglobal scopeを参照します。
そして、hermesはlocalモードでのeval呼び出しをサポートしていないので、globalに存在しないcontext
というオブジェクトを呼び出そうとしてクラッシュしていました。
なるほど理解。
ということで、おもむろにhermesを手元でbuildしてMDNのサンプルのscriptを走らせるとどんな挙動をするか見てみます。
console.logが動かないのでprintに置き換え。
"use strict"; function test() { var x = 2, y = 4; print(eval('x + y')); // 直接呼び出し、ローカルスコープを使用し、結果は 6 となる var geval = eval; // グローバルスコープでのeval呼び出しと同等 print(geval('x + y')); // 間接呼び出し、グローバルスコープを使用し、 x は未定義となるため ReferenceError が発生する (0, eval)('x + y'); // 間接的な呼び出しのもう一つの例 } test();
これをこうじゃ
> ./build/bin/hermes test.js test.js:25:9: warning: Direct call to eval(), but lexical scope is not supported. print(eval('x + y')); // 直接呼び出し、ローカルスコープを使用し、結果は 6 となる Uncaught ReferenceError: Property 'x' doesn't exist at eval (JavaScript:1:1) at test (test.js:25:13) at global (test.js:30:5)
hermes、evalを直接呼んでるけど、lexical scopeはサポートされてないよとwarningを出してくれて大変親切。
MDNと違って、最初のevalがglobal scopeなので結果が6にならずにReferenceErrorになりました。
原因は分かったのでこれをfixします。
new Functionの使いどころ
MDNのevalのページにeval() を使わないでください!
という項があって、そこでnew Functionする方法が書いてあります。
ということで、FunctionのMDN
なんとFunctionのconstructorに引数を与えるとglobal scopeで実行される関数が動的に作れるらしいです。
const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6)); // expected output: 8
つまり、これをこうしてやれば動くのでは?
const testContextProperty = new Function("context", "return context.foo"); const testCallContext = new Function("context", "return context.bar()"); const context = { foo: 'foo', bar: function() { return 'bar' } } print('access foo: ' + testContextProperty(context)) print('call bar: ' + testCallContext(context))
hermesで実行
> ./build/bin/hermes test.js access foo: foo call bar: bar
動きました 🎉
ということで、これを使ってevalを置き換えて、無事hermesでも件のライブラリが動くようになりました。