
一个 JSON 文件如何生成整套 C 选项 API——f3d options 代码生成机制图解与本地运行指南【免费下载链接】f3dFast and minimalist 3D viewer.项目地址: https://gitcode.com/GitHub_Trending/f3/f3df3d 是一个主打快而极简的 3D 查看器它的几十项渲染、交互、模型开关全部收进一个options对象可选项多到让人怀疑这堆 C 方法是不是手写的。答案是否定的整套options::set / get / reset / getRangeDomain的分支逻辑其实是 CMake 在 configure 阶段读一个 JSON 文件后拼出来的。本文以 cmake/f3dOptions.cmake 的_parse_json_option为核心带你看懂JSON 如何变成 C 选项 API读完后你能独立解释生成物长什么样、本地跑通一段等价代码并判断这种JSON 驱动生成相比手写注册表是否合适。上面这张 16:9 的defaultHDRI.png正是options里render.hdri.file一个path类型选项的兜底环境贴图——它是被选项系统管着的一张真实资源用来让你直观感受一下options.json背后控制的是真材实料的渲染行为。复制这段代码先跑起来3 个选项的最小 API在钻进 CMake 脚本前先把生成物最终长什么样跑起来。下面这段 C 是f3d_generate_options()会往 library/private/options_generated.h.in 里注入内容的缩小等价版只保留 3 个真实存在的选项结构、命名、异常语义都与仓库生成物一致// driver.cxx —— 镜像 f3d_generate_options() 生成的 options_generated 命名空间 #include iostream #include optional #include string #include variant #include vector #include stdexcept using option_variant_t std::variantbool, int, double, std::string, std::vectordouble, std::vectorint; // 生成到 public/options.h 的 ${_options_struct}有默认值 → 普通成员无默认值 → std::optional struct options { struct scene { struct camera { std::optionalint index; } camera; // scene.camera.index无 default_value } scene; struct render { std::optionaldouble line_width; // render.line_width无 default_value struct grid { int subdivisions 10; } grid; // render.grid.subdivisionsdefault_value: 10 } render; }; namespace options_generated { void set(options opt, std::string_view name, const option_variant_t value) { if (name scene.camera.index) opt.scene.camera.index std::getint(value); else if (name render.line_width) opt.render.line_width std::getdouble(value); else if (name render.grid.subdivisions) opt.render.grid.subdivisions std::getint(value); else throw std::runtime_error(Option std::string(name) does not exist); } option_variant_t get(const options opt, std::string_view name) { if (name scene.camera.index) return opt.scene.camera.index.value(); else if (name render.line_width) return opt.render.line_width.value(); else if (name render.grid.subdivisions) return opt.render.grid.subdivisions; else throw std::runtime_error(Option std::string(name) does not exist); } std::vectorstd::string getNames() { return { scene.camera.index, render.line_width, render.grid.subdivisions }; } } // namespace options_generated int main() { options opt; auto g options_generated; g::set(opt, render.grid.subdivisions, 20); g::set(opt, scene.camera.index, 2); g::set(opt, render.line_width, 0.5); std::cout subdivisions std::getint(g::get(opt, render.grid.subdivisions)) \n; std::cout camera.index std::getint(g::get(opt, scene.camera.index)) \n; std::cout line_width std::getdouble(g::get(opt, render.line_width)) \n; for (const auto n : g::getNames()) std::cout name: n \n; }复制源码后执行g即可无需 cmakeg -stdc17 driver.cxx -o driver ./driver输出概括subdivisions 20带默认值的普通成员被改写、camera.index 2与line_width 0.5无默认值被 set 后从无值变为有值随后逐行打印出全部选项的点分名。这就建立了第一个直觉所谓options的 API本质是一个if (name …)的长分支 一个成员结构体而这两样东西都不是人写的。代码藏在哪儿options.json 与 configure_file 的两段模板真正的隐藏不在 URL 里而在构建期。library/CMakeLists.txt调用include(f3dOptions) f3d_generate_options( INPUT_JSON …/options.json INPUT_PUBLIC_HEADER …/public/options.h.in INPUT_PRIVATE_HEADER …/private/options_generated.h.in DESTINATION …/binary OUTPUT_NAME options)f3dOptions.cmake里做的事可以拆成三步# 1) 把 JSON 读进来登记为依赖options.json 一改就触发重新 configure set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${…INPUT_JSON}) file(READ ${…INPUT_JSON} _options_json) _parse_json_option(${_options_json}) # 递归解析往 13 个 list 变量里追加分支 # 2) 把每个 list 用 ;\n else 之类的分隔符拼成一大串 C list(JOIN _options_setter ;\n else _options_setter) # 3) 注入模板占位符产出最终头文件 configure_file(…options.h.in …/public/options.h) configure_file(…options_generated.h.in …/private/options_generated.h)模板里的${_options_setter}、${_options_struct}、${_options_get_range_domain}就是元数据槽位CMake 解析 JSON 时给每个选项追加一条if (name …)片段list(JOIN …)把它们用;\n else粘成一条链configure_file再把粘好的代码填进槽位。换句话说选项 API 的源码其实分散在 library/options.json数据和 13 个 CMake list 变量拼接中间态里.h只是它们的投影。这也是CMAKE_CONFIGURE_DEPENDS登记 JSON 的原因——改 JSON 不改 C重配置后 API 自动变。先搞懂一个约定选项名是点分路径而不是成员名读这段生成逻辑前必须先记住一条约定否则会处处踩坑选项的名字是带点的完整路径而不是 C 成员名。结构体里字段叫subdivisions但对外名字是render.grid.subdivisions。名字是递归时靠_option_basename一点点拼出来的# 遇到分组没有 type 的对象就压栈basename grid. string(APPEND _option_basename ${_member_name}.) set(_option_name ${_option_basename}${_member_name}) # render.grid. subdivisions最小例子vertices scene / camera / index这层嵌套成员名是index可set(index, 2)会一路滑到else抛inexistent_exception只有set(scene.camera.index, 2)才命中分支。混淆成员名与选项名是这套 API 最常见的错——它不像普通 C 那样拼错就编译报错而是运行时静默走到else抛异常再靠 library/src/options.cxx 里getClosestOption走 Levenshtein 距离给你你是不是想写 xxx的兜底提示。逐个方法拆解叶子判定、类型映射与默认值、三种 domain叶子判定type 字段是分水岭_parse_json_option对每个键先看它是不是叶子string(JSON _option_type ERROR_VARIABLE _option_type_error GET ${_cur_json} type) string(JSON _option_type_type ERROR_VARIABLE _option_type_type_error TYPE ${_cur_json} type) if (_option_type_error STREQUAL NOTFOUND AND ${_option_type_type} STREQUAL STRING) # 有 type 且 type 是字符串 → 叶子选项走下面的生成逻辑 else() # 没有 type是个对象→ 分组压栈后递归 _parse_json_option(${_cur_json}) endif()为什么这样判断分组节点如grid本身没有type而叶子节点必有type且其值是字符串。这一条 if/else 把结构与数据彻底分开后面所有生成都只对叶子发生。类型映射JSON 类型名到 C 类型叶子拿到type后映射成真正的 C 类型节选自脚本中段的一长串elseifif(_option_type STREQUAL int_vector) set(_option_actual_type std::vectorint) elseif(_option_type STREQUAL string) set(_option_actual_type std::string) elseif(_option_type STREQUAL path) set(_option_actual_type std::filesystem::path) set(_option_variant_type std::string) set(_option_variant_convert .string()) # get 时把 path 转回 string 塞进 variant elseif(_option_type STREQUAL color) set(_option_actual_type f3d::color_t) set(_option_variant_type std::vectordouble) # ... ratio / direction / colormap / transform2d 同理注意_option_actual_type结构体里真实存储类型如f3d::color_t和_option_variant_type进option_variant_t的类型如std::vectordouble是两个东西——前者保证类型安全后者是为了让所有选项都能装进那 6 种std::variant。path的.string()就是在两者之间做换算。默认值还是 std::optionalif (_default_value_error STREQUAL NOTFOUND) # 有 default_value → 普通成员直接带初值 string(APPEND _options_struct … ${_option_actual_type} ${_member_name} ${_init};\n) list(APPEND _options_is_optional if (name \${_option_name}\) return false) else() # 无 default_value → std::optionalTget 时靠 .value() 取 string(APPEND _options_struct … std::optional${_option_actual_type} ${_member_name};\n) set(_optional_getter .value()) endif()这里的后果链很关键optional 选项在从未 set时调用get.value()会抛std::bad_optional_access被 library/private/options_generated.h.in 的外层try捕获后转成no_value_exception——这正是options.h里每个get都声明可能抛no_value_exception的原因。而普通成员永远有值isOptional对它们恒返回false。三种 domainrange / enum / indexif(_option_domain_style STREQUAL range) # 校验 min/max/increment 齐全否则 FATAL_ERROR string(APPEND _options_domains_struct … DomainRange${_type} ${_member_name} {min, max, inc};\n) list(APPEND _options_increase_decrease … options_tools::increaseDecreaseUp(opt.${name}, opt.domains.${name})) elseif(_option_domain_style STREQUAL enum) # 逐个取出 enum 值拼成 DomainEnum elseif(_option_domain_style STREQUAL index) # 仅允许 int拼 DomainIndexdomain决定了一个选项能怎么被调节range支持increase/decrease带上下限钳制enum支持cycle在列表里轮转index两者皆可。三种 domain 的取值互斥——对range选项调用getEnumDomain会在生成好的分支里直接抛incompatible_exception这是靠错配即抛异常保证语义清晰而非返回空。推演状态一段嵌套 JSON 递归解析后生成了什么拿下面这段真实子集走一遍递归观察_option_basename与两份输出如何同步增长{ scene: { camera: { index: { type: int, domain: {style: index} } } }, render: { line_width: { type: double } } }步骤_option_basename生成的_option_name追加到_options_struct追加到_options_setter进入scene→scene.—分组struct scene {—进入camerascene.→scene.camera.—分组struct camera {—叶子indexscene.camera.scene.camera.indexstd::optionalint index;if (name scene.camera.index) opt.scene.camera.index std::getint(value)弹出两层—} camera; } scene;—进入render→render.—分组struct render {—叶子line_widthrender.render.line_widthstd::optionaldouble line_width;if (name render.line_width) opt.render.line_width std::getdouble(value)两个要点其一成员名永远是最内层键名index而选项名是缩进路径的快照scene.camera.index二者只在顶层不冲突时才偶然相同其二struct嵌套与if分支逐条同步生成所以结构和分派永远对得上不会漏配一个选项。整轮是单次线性遍历每个叶子只做常数次list(APPEND)总开销 $O(N)$$N$ 为选项数。选型对照JSON 驱动生成 vs 手写注册表把这套JSON 驱动生成和常见的手写注册表放在一起看维度f3dJSON CMake 生成手写注册表如std::mapname, lambda加一个选项改 library/options.json重配置即可手写成员 每个方法的分支 文档多处同步set/get运行时开销if/else链线性扫描最坏 $O(N)$$N≈96$哈希 $O(1)$ 平均类型安全编译期成员 std::getT校验多靠void*/variant运行时断言domain/默认值/可选性数据里声明自动落到 domain 结构需逐字段手工维护拼错名字落到else抛异常getClosestOption给建议取决于实现一句话选型选项数量大、类型杂、还要对外暴露 SDK 文档时数据即代码的生成式更省维护成本——加选项只碰一个 JSON而选项极少、且要求 $O(1)$ 查找的场景手写注册表更直接。进阶方向可以看仓库怎么给生成式补强getClosestOption用 library/src/levenshtein.cxx 的 Levenshtein 距离把线性扫描的短板拼错名转成了最接近建议这正是生成式 API 常见的体验补丁。解析规则本身则被 library/testing/TestSDKOptionsIO.cxx 锁死改 JSON 语义时它会立刻报错。收尾要点核心机制选项 API 不是手写的——f3d_generate_options()在 configure 期递归读options.json把每个选项追加成if (name …)分支再configure_file注入options.h.in/options_generated.h.in两个模板槽位产出public/options.h与private/options_generated.h。关键约定选项名 点分路径由_option_basename递归拼接成员名 最内层键有default_value是普通成员、无则是std::optionalget未 set 时经.value()抛no_value_exception。复杂度画像生成期单次线性 $O(N)$运行时set/get是if/else链最坏 $O(N)$非哈希拼错名靠 Levenshtein 兜底。适用场景选项多、类型杂、需要统一 SDK 文档的 C 库。想继续深挖cmake/f3dOptions.cmake生成逻辑本体、library/private/options_generated.h.in生成的分派模板、library/src/options.cxx薄封装层与异常语义。【免费下载链接】f3dFast and minimalist 3D viewer.项目地址: https://gitcode.com/GitHub_Trending/f3/f3d创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考