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

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

超遅い
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要素しか取得できない)

まだ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")

量を減らして妥協
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
)
この方法だとドキュメンテーションコメントを取れないため、そこにアノテーションが書かれていても拾えない。

パースのアルゴリズムを考える
とりあえず、文字列を [
]
{
}
,
:
文字列
その他(数値やboolean)
のトークンに分割することはできたので、それをCMakeの変数として纏めるアルゴリズムを考える。
変数スコープの都合上、CMakeのfunctionを再帰的に使うことができないので自前でスタックを持って foreach
1つで済ませる必要がある。これが結構つらい。。
スタックポインタを math
コマンドで手動計算しつつ アキュムレーター(acc)と現在のコンテキスト(ctx) をスタック状に持つ。これだな。コンテキストは、オブジェクト、配列、初期状態の3種で、さらにオブジェクトは :
を受けたかでkey(:
の前)とvalue(:
の後)に分けることになる。