Open5

そもそもCMakeは巨大JSONを処理できるのか問題

okuokuokuoku

Clangが生成するASTのJSONは結構巨大なサイズになる。これを処理するためにNode.jsでも使おうと思ってたけど、CMakeで処理が間に合うならビルドにくっつけられるし有利なのではないか問題。

okuokuokuoku

超遅い

file(READ out.json file)

# Assumes JSON have {id: <ID>, inner: [JSON]}
# nodes_<ID>: JSON of node
# links_<ID>: Array of innner:

macro(json_node_flatten id)
    message(STATUS "Flatten: ${id}")
    set(__json "${nodes_${id}}")
    string(JSON __r ERROR_VARIABLE __x GET "${__json}" inner)
    if(__x)
        # No inner. Terminate here.
    else()
        # Has inner. Recurse after added to the dictionary
        string(JSON nodes_${id} SET "${__json}" inner 0) # Clear `inner` first
        string(JSON __j LENGTH "${__r}")
        set(__queue)
        foreach(idx RANGE "${__j}")
            message(STATUS "Enter: ${idx}")
            # Save inner content to nodes_<ID>
            string(JSON __jj GET "${__r}" ${idx})
            string(JSON __jjj GET "${__jj}" id)
            set(nodes_${__jjj} "${__jj}")
            list(APPEND __queue "${__jjj}")
        endforeach()
        foreach(nid IN LISTS __queue)
            message(STATUS "Enter: ${nid}")
            json_array_flatten(${nid})
        endforeach()
    endif()
endmacro()

string(JSON rootid GET "${file}" id)
set(nodes_${rootid} "${file}")
json_node_flatten(${rootid})

これをやると巨大なJSONを毎回パースするので超遅い。Kitwareの人は一度に取得する方法が有ると書いているが、これは単に間違っている https://discourse.cmake.org/t/string-json-get-slow/7740/2 (GETは1要素しか取得できない)

okuokuokuoku

まだ1分半掛かる

string(SUBSTRING) が遅いということで、文字列 → 他トークンの順に2段階でパースしてみた。たぶんこれ以上の速度を出すのは厳しいというところまで来ているが1分30秒掛かる。。

まぁこれは jq あたりで事前パースするしかないかな。。いや結構惜しいところまで行ってる気はするけど。。

cmake_minimum_required(VERSION 3.20)
file(READ out.json file)

function(preparse_json out json uniq)
    string(ASCII 1 ESC_DQUOTESC)
    string(ASCII 2 ESC_BSLASH)
    string(ASCII 3 ESC_SEMI)
    string(ASCII 4 ESC_BL)
    string(ASCII 5 ESC_BR)
    string(ASCII 6 ESC_CL)
    string(ASCII 7 ESC_CR)

    message(STATUS "Replace key chars")
    # Pass1: Replace all key characters
    string(REPLACE "\\\"" "${ESC_DQUOTESC}" __input "${json}")
    string(REPLACE "\\" "${ESC_BSLASH}" __input "${__input}")
    string(REPLACE ";" "${ESC_SEMI}" __input "${__input}")
    string(REPLACE "[" "${ESC_BL}" __input "${__input}")
    string(REPLACE "]" "${ESC_BR}" __input "${__input}")

    # Pass2: Generate string list
    message(STATUS "Generate string list")
    string(REGEX REPLACE "\"" ";" __strspace "${__input}")

    # Pass3: Generate token list
    message(STATUS "Generate token list")
    set(idx 0)
    set(instr OFF)
    foreach(str IN LISTS __strspace)
        if(instr)
            set(tkn_${idx} "STR")
            set(frag_${idx} "${str}")
            set(instr OFF)
            math(EXPR idx "${idx}+1")
        else()
            # Parse token
            string(REGEX REPLACE "[${ESC_BL}${ESC_BR}{}]" ";" pick "${str}")
            string(REGEX MATCHALL "[${ESC_BL}${ESC_BR}{}]" lis "${str}")
            foreach(p l IN ZIP_LISTS pick lis)
                set(tkn_${idx} "RAW")
                set(frag_${idx} "${p}")
                math(EXPR idx "${idx}+1")
                set(tkn_${idx} "${l}")
                math(EXPR idx "${idx}+1")
            endforeach()
            set(instr ON)
        endif()
    endforeach()
    message(STATUS "idx: ${idx}")
endfunction()

preparse_json(out "${file}" "qq")
okuokuokuoku

量を減らして妥協

Cソース中のコメントを除去、つまり一旦プリプロセスしてからdump-astすることでJSONのサイズは 50MiB → 15MiBになって処理時間も30秒ほどになった。これで妥協するか。。

diff --git a/apigen/rip.cmake b/apigen/rip.cmake
index e2533e8..d573b1b 100644
--- a/apigen/rip.cmake
+++ b/apigen/rip.cmake
@@ -91,7 +91,14 @@ file(WRITE apigen.c "#include<SDL.h>\n")
 execute_process(COMMAND
     ${clang} --target=wasm32 ${includes}
     ${sysroot}
-    -Xclang -ast-dump=json -c apigen.c
+    -E apigen.c
+    OUTPUT_FILE outn.i.c
+)
+
+execute_process(COMMAND
+    ${clang} --target=wasm32 ${includes}
+    ${sysroot}
+    -Xclang -ast-dump=json -c outn.i.c
     OUTPUT_FILE out.json
 )

この方法だとドキュメンテーションコメントを取れないため、そこにアノテーションが書かれていても拾えない。

okuokuokuoku

パースのアルゴリズムを考える

とりあえず、文字列を [ ] { } , : 文字列 その他(数値やboolean) のトークンに分割することはできたので、それをCMakeの変数として纏めるアルゴリズムを考える。

変数スコープの都合上、CMakeのfunctionを再帰的に使うことができないので自前でスタックを持って foreach 1つで済ませる必要がある。これが結構つらい。。

スタックポインタを math コマンドで手動計算しつつ アキュムレーター(acc)と現在のコンテキスト(ctx) をスタック状に持つ。これだな。コンテキストは、オブジェクト、配列、初期状態の3種で、さらにオブジェクトは : を受けたかでkey(:の前)とvalue(:の後)に分けることになる。