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されてたりしたので、原因が分からず、最小構成で再現させるのに苦労した

豆知識として以下を得た

ちなみに、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について以下が分かった

  1. 単体で実行するとFAILする(期待通り)
  2. FAILするテスト(01.test.jsなど)のあとに実行するとFAILする(期待通り)
  3. 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-circusrun_startrun_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される

runkit.com

すると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 ./tmpgdbを起動、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 + 10x7fffffffd8d4 になる

ここは、さっき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

  • Phoenixでサーバーを書いた
  • docker compose の ecs integrationでデプロイした
  • AWSのいろんなサービスがいい感じに起動して、便利

docker compose ecs integrationとは

2020年11月に発表されて、待望の機能が来たと結構な反響があったように思います

仕事でPhoenixのアプリを作る機会があり、そこでこのスタックを採用したのでハマったところを紹介していきます

概要、必要なこと、アーキテクチャなどはほぼ以下のドキュメントに書いてあるのでそちらをご覧ください

docs.docker.com

ふわっとまとめると、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を使うとデプロイはこんな感じになるはず

  1. サーバーを書く
  2. buildしてDocker imageに固める
  3. ECRなどのDocker image hosting serviceにpushする
  4. docker compose up

port:80が使えない

Unix系のOSにて、デフォルトでは1024以下のportはroot権限がないと使用できないらしい

これを回避する方法は以下

  1. nginxでリバースプロキシを立てて、アプリサーバーを直接ALBに紐付けない
  2. rootユーザーでアプリサーバーを起動する
  3. port mappingする

2はセキュリティ的に推奨される感じではなさそうです

3はecs integrationではサポートされていません

このあたりはいかに詳しいです

別の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であることが分かっていなかったため、いろんなクエリでググった末に以下のページにたどり着いてようやく理解できました

Amazon ECS サービスでタスク間の通信を許可する

ということで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

  • 「低レイヤを知りたい人のためのCコンパイラ作成入門」をやっている
  • 関数定義が動くようになった
  • 「低レイヤを知りたい人のためのCコンパイラ作成入門」はいいぞ

はじめに

1年ぐらい前にこれが公開されて、ずっとやりたいと思っていたがやれてなかったので今年の2月ぐらいから地道に進めていた。

www.sigbus.info

やっとこさ関数を定義して、関数を呼び出せるようになったのでここにいたるまでの振り返り

成果物はこれ

GitHub - sasurau4/9ccs

やって楽しいポイント

loxのときのように別の言語で実装しても良かったが、Cをほとんど書いたことがなかったので、紹介されている通りCで実装した。

assemblyとかレジスタはそもそもよく分かってなかったので、基礎の基礎から説明されてて楽しい。

tokenizeらへんはloxで一度通っているのですっと頭に入ってくる。再帰下降構文解析とBFNらへんは何度やっても人類の叡智という感じがする。

スタックマシンが実際にどうやって使われてるかもようやっと少し理解できた。

makeの使い方も触れるしよい。

途中に挟んであるコラムが面白いので、コンパイラを実際に作らないでも読み物として楽しむだけでも面白い。

Cの歴史も少し触るので、現在から見ると明らかに不自然に感じる文法が採用された時代背景や思想、普段使っているJSなんかが高級言語であることがなんとなく分かる。

ハマったポイント

どのへんにハマったかをつらつら書く

ここに取り上げていないものはREADMEに書いてある。

GitHub - sasurau4/9ccs

Cのコードで困ったら9cc本体を見れば雰囲気は分かるので、それでなんとかなる。

GitHub - rui314/9cc: A Small C Compiler

アセンブラで詰まったら付録1を見たり、Compiler Explorerと自分の出力したアセンブラを比較したりすると分かる。

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/core7.13.1から指定されている

エラーにならないB PJのsemverのversionは5.7.1

これは、@babel/core7.12.3から指定されている

@babel/coreが上がってるのは気づいていたが、minor versionだから大丈夫やろと油断していた

いつ上がったのか@babel/coreのリリースブログchangelogを読んでみるが、それっぽい記述がない

blameするとこいつで変わったらしい

github.com

BABEL_8でbreakingするよ的なflagが立っていて、@babel/coreのv7.13系からsemverがv7系に上がっている

これがPR

github.com

PRマージされた後に、@babel/coreとwebpackを併用するとsemverが7.0.0に固定されてissueが起こると書いてある

このコメント、ブラウザでコードエディタやってて@babel/core使ってると書いてあるので、まったく同じユースケースでびっくり

そのあとsemver@v7.1系を使うとBREAKINGなので、6.3系にダウングレードするPR出されて、まだリリースされていない。これが2日前。

github.com

ということで、次のbabelがリリースされたら解決しそう

おまけ1: semverのpreload機構

2019年12月ぐらいからちょこちょこissueにはなっていたらしい github.com

ドキュメントが更新されたりしていたが、以下のPRで廃止になっている

github.com

おまけ2: semverのsemver

github.com

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を発表しました。

reactnative.dev

hermes、今のところはAndroidでopt inして始めて使えるエンジンですが、react-native-reanimatedのv2-alphaがhermesしかサポートしていなかったり、react-native本体でもiOShermesを動かせるようにするPRが作られていたりとecosystemがhermesを前提にしてきそうな動きがあります。

docs.swmansion.com github.com

もちろん、すぐにhermesがデフォルトになるということはなさそうですが、hermesをonにしたときのAndroidのパフォーマンス向上は捨てがたいです。

engineering.fb.com

ということであるプロジェクトで無邪気にhermesを有効化したんですが、react-nativeのアプリがクラッシュするようになりました。

クラッシュメッセージはUncaught ReferenceError: Property 'context' doesn't exist

なんでや。

evalの挙動

react-nativeは、普段はJavaScriptCoreSafariに積まれてるやつ)でJSを動かしています。なのでWebで動くものは基本的に動きます。

ところが、hermesはreact-native向けに新しく開発されたエンジンなので、一部機能が動きません。また、ドキュメント上でサポートから除外される機能が列挙されています。

Local mode eval() (use and introduce local variables)

github.com

犯人はこいつでした。

Local mode eval()とはなんぞやということで、困ったときのMDN。

eval 関数を eval 以外の名前を参照して呼び出すことで間接的に使用した場合、ECMAScript 5 以降ではローカルスコープではなくグローバルスコープで機能します。これは例えると、関数定義によりグローバル関数が作成されるため、評価されたコードはその呼び出されたスコープ内のローカル変数にアクセスできなくなる、ということです。

developer.mozilla.org

つまり、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

developer.mozilla.org

なんと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でも件のライブラリが動くようになりました。