Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
generate_iop_static.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# This file is part of Ansel,
4# Copyright (C) 2026 Aurélien PIERRE.
5#
6# Ansel is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# Ansel is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Ansel. If not, see <http://www.gnu.org/licenses/>.
18
19"""Generate the glue that binds statically-linked IOP modules into the application.
20
21IOP modules used to be one shared object each, discovered by scanning a directory and
22bound with g_module_symbol(). That indirection bought nothing -- the module set is fixed
23at build time, and src/develop/iop_order.c and src/develop/geometry/geometry.c both carry
24a hardcoded census of module names that a module on disk cannot join -- while costing a
25dlopen per module at every startup and turning a stale .so left over from an experimental
26branch into a refusal to start (see dt_ioppr_check_so_iop_order).
27
28Two things have to be generated to replace it:
29
30 * <module>_present.h -- DT_MODULE_HAS_<fn>, 0 or 1, for EVERY name in the API. This is
31 the question g_module_symbol() used to answer at runtime: does this module define this
32 entry point at all? It is answered here by running the real compiler's preprocessor
33 over the module's real sources with the module's real flags. That is not
34 over-engineering: a regex over the raw source gets 90 of 91 modules right and then
35 trips over iop/censorize.c, which hides four functions behind `#if FALSE', and writes
36 name() with its return type on the previous line. Only the preprocessor knows what
37 HAVE_OPENCL and `#if FALSE' resolve to, and it costs one -E pass per source.
38
39 * <module>_static.c -- the function that fills this module's dt_iop_module_so_t, expanded
40 from the same X-macro list in iop/iop_api.h that used to drive the g_module_symbol()
41 calls, inside the module's own translation unit so the plain API names resolve to THIS
42 module's asm-labelled symbols.
43
44plus one registry naming every module, compiled into lib_ansel.
45"""
46
47import argparse
48import os
49import re
50import subprocess
51import sys
52
53# Emitted into every module's translation unit by DT_MODULE(), not declared through the
54# X-macro list, but colliding across modules exactly like the API does.
55VERSION_FUNCTIONS = ("dt_module_dt_version", "dt_module_mod_version")
56
57
58def _usable_flags(flags):
59 """Drop what the preprocessor cannot use, keep the rest verbatim.
60
61 CMake's COMPILE_DEFINITIONS carries at least one entry that already includes its own
62 `-D' (LIBSOUP_VERSION_MAJOR), which reaches us as `-D-DLIBSOUP_VERSION_MAJOR=2' -- not a
63 macro name. Imported targets also leak into INCLUDE_DIRECTORIES as `-I<dir>/LCMS2::LCMS2'.
64 Neither changes which functions a module defines, so neither is worth failing over.
65 """
66 usable = []
67 for flag in flags:
68 if not flag:
69 continue
70 if flag.startswith("-D"):
71 name = flag[2:].split("=", 1)[0]
72 if not name or not (name[0].isalpha() or name[0] == "_"):
73 continue
74 usable.append(flag)
75 return usable
76
77
78def parse_api(path):
79 """Return [(kind, name)] for every entry point declared in an X-macro API header."""
80 entries = []
81 seen = set()
82 pattern = re.compile(r"^\s*(OPTIONAL|REQUIRED|DEFAULT)\‍(\s*[^,]+,\s*([A-Za-z_]\w*)")
83 with open(path, encoding="utf-8") as handle:
84 for line in handle:
85 match = pattern.match(line)
86 if match is None:
87 continue
88 kind, name = match.group(1), match.group(2)
89 if name in seen:
90 continue
91 seen.add(name)
92 entries.append((kind, name))
93 if not entries:
94 sys.exit(f"generate_iop_static: no API entries found in {path}")
95 return entries
96
97
98def preprocess(compiler, flags, source):
99 """Run the compiler's preprocessor over one source, keeping its line markers."""
100 command = [compiler, "-E"] + flags + [source]
101 result = subprocess.run(command, capture_output=True, text=True, errors="replace")
102 if result.returncode != 0:
103 sys.stderr.write(result.stderr[-4000:])
104 sys.exit(f"generate_iop_static: preprocessing failed for {source}")
105 return result.stdout
106
107
108def own_code(preprocessed, module_dir):
109 """Keep only the regions the line markers attribute to the module's own files.
110
111 Without this, a definition in an included header counts as the module's own: two C++
112 modules (iop/bilateral.cc, iop/tonemap.cc) pick up an unrelated `init(...)' from a
113 header and would claim an init() they do not have.
114 """
115 kept = []
116 keeping = False
117 marker = re.compile(r'^#\s+\d+\s+"([^"]*)"')
118 module_dir = os.path.realpath(module_dir)
119 for line in preprocessed.splitlines():
120 match = marker.match(line)
121 if match is not None:
122 name = match.group(1)
123 path = os.path.realpath(name) if os.path.isabs(name) else None
124 keeping = (path is not None and path.startswith(module_dir + os.sep)) \
125 or os.path.basename(name).startswith("introspection_")
126 continue
127 if keeping:
128 kept.append(line)
129 return "\n".join(kept)
130
131
132def defined_entry_points(body, names):
133 """Names among `names' that `body' defines at file scope with external linkage."""
134 if not names:
135 return set()
136 definition = re.compile(
137 r"(?<![\w.>])(%s)\s*\‍([^;{]*\‍)\s*\{" % "|".join(sorted(names, key=len, reverse=True))
138 )
139 found = {match.group(1) for match in definition.finditer(body)}
140 # A static definition is the module's own business and never crosses the link.
141 return {
142 name for name in found
143 if re.search(r"\bstatic\b[^;{}]*\b%s\s*\‍(" % re.escape(name), body) is None
144 }
145
146
147def generate_present(args, api_names):
148 flags = [flag for flag in args.flags if flag]
149 found = set()
150 for source in args.sources:
151 # A module's scanned sources are not uniformly one language (e.g. demosaic.c
152 # with demosaic/amaze.cc as an extra source), and -E must run through the
153 # compiler matching each file's own language -- see the comment on --cxx above.
154 compiler = args.cxx if source.endswith((".cc", ".cpp", ".cxx")) else args.cc
155 body = own_code(preprocess(compiler, flags, source), args.module_dir)
156 found |= defined_entry_points(body, api_names)
157
158 missing_required = [name for kind, name in args.api if kind == "REQUIRED" and name not in found]
159 if missing_required:
160 sys.exit(
161 f"generate_iop_static: module '{args.module}' defines no "
162 f"{', '.join(missing_required)} -- every module must."
163 )
164
165 lines = [
166 "/* Auto-generated by tools/generate_iop_static.py -- do not edit.",
167 " *",
168 f" * Which entry points iop/{args.module} actually defines, as the compiler's own",
169 " * preprocessor sees them. Consumed by DT_MODULE_PICK() in common/module_api.h;",
170 " * every name in the API appears here, so a name the generator did not consider",
171 " * is a compile error rather than a silently NULL function pointer. */",
172 "",
173 f"#ifndef DT_IOP_{args.module.upper()}_PRESENT_H",
174 f"#define DT_IOP_{args.module.upper()}_PRESENT_H",
175 "",
176 ]
177 for _kind, name in args.api:
178 lines.append(f"#define DT_MODULE_HAS_{name} {1 if name in found else 0}")
179 lines += ["", f"#endif // DT_IOP_{args.module.upper()}_PRESENT_H", ""]
180 write_if_changed(args.present, "\n".join(lines))
181
182
184 prefix = f"dt_iop_{args.module}__"
185 content = f"""/* Auto-generated by tools/generate_iop_static.py -- do not edit.
186 *
187 * Binds iop/{args.module}'s entry points into its dt_iop_module_so_t. This is compiled
188 * as part of the {args.module} object library, so DT_MODULE_SYMBOL_PREFIX is set and the
189 * plain API names below carry this module's asm label: `module->process = process'
190 * stores the address of {prefix}process and nothing else.
191 *
192 * The DEFAULT fallbacks are NOT applied here -- default_<fn> is static to
193 * develop/imageop.c, which applies them right after calling this. */
194
195#include "develop/imageop.h"
196#include "{args.module}_present.h"
197
198void {prefix}fill_so(dt_iop_module_so_t *module)
199{{
200#define INCLUDE_API_FROM_MODULE_STATIC
201#include "iop/iop_api.h"
202
203 /* Not an X-macro entry: DT_MODULE() defines this in every module unconditionally. */
204 module->version = dt_module_mod_version;
205}}
206"""
207 write_if_changed(args.fill, content)
208
209
211 modules = sorted(args.modules)
212 lines = [
213 "/* Auto-generated by tools/generate_iop_static.py -- do not edit.",
214 " *",
215 " * Every IOP module built into this binary. This replaces the directory scan in",
216 " * dt_module_load_modules(): the set of modules is decided by src/iop/CMakeLists.txt",
217 " * at build time, which is also what develop/iop_order.c's order tables and",
218 " * develop/geometry/geometry.c's roster are written against, so discovering it again",
219 " * at runtime could only ever disagree with them.",
220 " *",
221 " * Referencing every fill function from lib_ansel's own sources is also what pulls",
222 " * each module's objects into the link. */",
223 "",
224 '#include "develop/imageop.h"',
225 "",
226 ]
227 for module in modules:
228 lines.append(f"extern void dt_iop_{module}__fill_so(dt_iop_module_so_t *module);")
229 lines += ["", "const dt_iop_module_static_entry_t dt_iop_static_modules[] = {"]
230 for module in modules:
231 lines.append(f' {{ "{module}", dt_iop_{module}__fill_so }},')
232 lines += [
233 "};",
234 "",
235 "const int dt_iop_static_modules_count "
236 "= (int)(sizeof(dt_iop_static_modules) / sizeof(dt_iop_static_modules[0]));",
237 "",
238 ]
239 write_if_changed(args.out, "\n".join(lines))
240
241
242def write_if_changed(path, content):
243 """Avoid touching an unchanged output, so ninja does not rebuild the world."""
244 if os.path.exists(path):
245 with open(path, encoding="utf-8") as handle:
246 if handle.read() == content:
247 return
248 os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
249 with open(path, "w", encoding="utf-8") as handle:
250 handle.write(content)
251
252
253def main():
254 parser = argparse.ArgumentParser(description=__doc__)
255 sub = parser.add_subparsers(dest="command", required=True)
256
257 module_parser = sub.add_parser("module", help="presence header + so-fill source")
258 module_parser.add_argument("--module", required=True)
259 module_parser.add_argument("--module-dir", required=True)
260 module_parser.add_argument("--api", required=True)
261 module_parser.add_argument("--present", required=True)
262 module_parser.add_argument("--fill", required=True)
263 # Both compilers, not just one: a module's sources aren't uniformly one language
264 # (e.g. demosaic.c with demosaic/amaze.cc as an extra source), and preprocessing a
265 # file with the compiler for the other language can silently resolve its standard
266 # library headers differently than the real build will -- see generate_present().
267 module_parser.add_argument("--cc", required=True)
268 module_parser.add_argument("--cxx", required=True)
269 module_parser.add_argument("--sources", nargs="+", required=True)
270
271 registry_parser = sub.add_parser("registry", help="the table of every built module")
272 registry_parser.add_argument("--out", required=True)
273 registry_parser.add_argument("--modules", nargs="+", required=True)
274
275 # Compiler flags come after a `--' sentinel: they are full of tokens starting with a
276 # dash, which argparse would read as options of its own.
277 argv = sys.argv[1:]
278 flags = []
279 if "--" in argv:
280 cut = argv.index("--")
281 argv, flags = argv[:cut], argv[cut + 1:]
282
283 args = parser.parse_args(argv)
284 args.flags = _usable_flags(flags)
285
286 if args.command == "registry":
288 return
289
290 args.api = parse_api(args.api)
291 api_names = {name for _kind, name in args.api} | set(VERSION_FUNCTIONS)
292 generate_present(args, api_names)
293 generate_fill(args)
294
295
296if __name__ == "__main__":
297 main()
preprocess(compiler, flags, source)
write_if_changed(path, content)
generate_present(args, api_names)
own_code(preprocessed, module_dir)
defined_entry_points(body, names)