Skip to content

Commit 8d58fce

Browse files
[mypyc] Add new type for expressing header dependencies (#21223)
Follow-up to #21183 to address a review comment. The external dependency of a capsule added there is really only needed to get the path of a lib-rt header that can be placed in the external header of a C extension so it doesn't make sense to use `SourceDep` that references a .c file. Instead add a new `HeaderDep` type that expresses only a dependency on a header file. The header has to be included in the external C extension header because the exports table might reference types defined in lib-rt headers, like `VecT` in `lib-rt/vecs/librt_vecs.h`. However, the external C extension header cannot include the `_api.h` header added in that PR because that would leak a dependency on the associated `_api.c` file which defines extern objects declared in `_api.h`. That would result in undefined objects when a C extension that doesn't depend on a given lib-rt module (including indirectly) imports from a C extension that does. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent cd71671 commit 8d58fce

File tree

5 files changed

+83
-26
lines changed

5 files changed

+83
-26
lines changed

mypyc/codegen/emitmodule.py

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@
5656
short_id_from_name,
5757
)
5858
from mypyc.errors import Errors
59-
from mypyc.ir.deps import LIBRT_BASE64, LIBRT_STRINGS, LIBRT_TIME, LIBRT_VECS, SourceDep
59+
from mypyc.ir.deps import (
60+
LIBRT_BASE64,
61+
LIBRT_STRINGS,
62+
LIBRT_TIME,
63+
LIBRT_VECS,
64+
Capsule,
65+
HeaderDep,
66+
SourceDep,
67+
)
6068
from mypyc.ir.func_ir import FuncIR
6169
from mypyc.ir.module_ir import ModuleIR, ModuleIRs, deserialize_modules
6270
from mypyc.ir.ops import DeserMaps, LoadLiteral
@@ -436,24 +444,31 @@ def load_scc_from_cache(
436444
return modules
437445

438446

439-
def collect_source_dependencies(
440-
modules: dict[str, ModuleIR], *, internal: bool = True
441-
) -> set[SourceDep]:
442-
"""Collect all SourceDep dependencies from all modules.
443-
444-
If internal is set to False, returns only the dependencies that can be exported to C extensions
445-
dependent on the one currently being compiled.
446-
"""
447+
def collect_source_dependencies(modules: dict[str, ModuleIR]) -> set[SourceDep]:
448+
"""Collect all SourceDep dependencies from all modules."""
447449
source_deps: set[SourceDep] = set()
448450
for module in modules.values():
449451
for dep in module.dependencies:
450452
if isinstance(dep, SourceDep):
451-
if internal == dep.internal:
453+
if dep.internal:
452454
source_deps.add(dep)
455+
elif isinstance(dep, Capsule):
456+
source_deps.add(dep.internal_dep())
457+
return source_deps
458+
459+
460+
def collect_header_dependencies(modules: dict[str, ModuleIR], *, internal: bool) -> set[str]:
461+
"""Collect all header dependencies from all modules."""
462+
header_deps: set[str] = set()
463+
for module in modules.values():
464+
for dep in module.dependencies:
465+
if isinstance(dep, (SourceDep, HeaderDep)):
466+
if dep.internal == internal:
467+
header_deps.add(dep.get_header())
453468
else:
454469
capsule_dep = dep.internal_dep() if internal else dep.external_dep()
455-
source_deps.add(capsule_dep)
456-
return source_deps
470+
header_deps.add(capsule_dep.get_header())
471+
return header_deps
457472

458473

459474
def compile_modules_to_c(
@@ -652,9 +667,9 @@ def emit_dep_headers(decls: Emitter, internal: bool) -> None:
652667
if self.compiler_options.depends_on_librt_internal:
653668
decls.emit_line(f'#include "internal/librt_internal{suffix}.h"')
654669
# Include headers for conditional source files
655-
source_deps = collect_source_dependencies(self.modules, internal=internal)
656-
for source_dep in sorted(source_deps, key=lambda d: d.path):
657-
decls.emit_line(f'#include "{source_dep.get_header()}"')
670+
header_deps = collect_header_dependencies(self.modules, internal=internal)
671+
for header_dep in sorted(header_deps):
672+
decls.emit_line(f'#include "{header_dep}"')
658673

659674
emit_dep_headers(ext_declarations, False)
660675

mypyc/common.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
BITMAP_BITS: Final = 32
6969

7070
# Runtime C library files that are always included (some ops may bring
71-
# extra dependencies via mypyc.ir.SourceDep)
71+
# extra dependencies via mypyc.ir.deps.SourceDep or mypyc.ir.deps.HeaderDep)
7272
RUNTIME_C_FILES: Final = [
7373
"init.c",
7474
"getargs.c",

mypyc/ir/deps.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,8 @@ def internal_dep(self) -> SourceDep:
2626
module = self.name.split(".")[-1]
2727
return SourceDep(f"{module}/librt_{module}_api.c", include_dirs=[module])
2828

29-
# TODO: This SourceDep is really only used for its associated header so it would make more sense
30-
# to add a separate type. Alternatively, see if this can be removed altogether if we move the
31-
# definitions that depend on this header from the external header of the C extension.
32-
def external_dep(self) -> SourceDep:
33-
"""External source dependency of the capsule that may be included in external headers of C
29+
def external_dep(self) -> HeaderDep:
30+
"""External header dependency of the capsule that may be included in external headers of C
3431
extensions that depend on the capsule.
3532
3633
The external headers of the C extensions are included by other C extensions that don't
@@ -42,7 +39,7 @@ def external_dep(self) -> SourceDep:
4239
including the internal header would result in undefined symbols.
4340
"""
4441
module = self.name.split(".")[-1]
45-
return SourceDep(f"{module}/librt_{module}.c", include_dirs=[module], internal=False)
42+
return HeaderDep(f"{module}/librt_{module}.h", include_dirs=[module], internal=False)
4643

4744

4845
class SourceDep:
@@ -76,7 +73,36 @@ def get_header(self) -> str:
7673
return self.path.replace(".c", ".h")
7774

7875

79-
Dependency = Capsule | SourceDep
76+
class HeaderDep:
77+
"""Defines a C header file that a primitive may require.
78+
79+
The header gets explicitly #included if the dependency is used.
80+
include_dirs are passed to the C compiler when the generated extension
81+
is compiled separately and needs to include the header.
82+
"""
83+
84+
def __init__(
85+
self, path: str, *, include_dirs: list[str] | None = None, internal: bool = True
86+
) -> None:
87+
# Relative path from mypyc/lib-rt, e.g. 'strings/librt_strings.h'
88+
self.path: Final = path
89+
self.include_dirs: Final = include_dirs or []
90+
self.internal: Final = internal
91+
92+
def __repr__(self) -> str:
93+
return f"HeaderDep(path={self.path!r})"
94+
95+
def __eq__(self, other: object) -> bool:
96+
return isinstance(other, HeaderDep) and self.path == other.path
97+
98+
def __hash__(self) -> int:
99+
return hash(("HeaderDep", self.path))
100+
101+
def get_header(self) -> str:
102+
return self.path
103+
104+
105+
Dependency = Capsule | SourceDep | HeaderDep
80106

81107

82108
LIBRT_STRINGS: Final = Capsule("librt.strings")

mypyc/ir/module_ir.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from mypyc.common import JsonDict
66
from mypyc.ir.class_ir import ClassIR
7-
from mypyc.ir.deps import Capsule, Dependency, SourceDep
7+
from mypyc.ir.deps import Capsule, Dependency, HeaderDep, SourceDep
88
from mypyc.ir.func_ir import FuncDecl, FuncIR
99
from mypyc.ir.ops import DeserMaps
1010
from mypyc.ir.rtypes import RType, deserialize_type
@@ -48,6 +48,14 @@ def serialize(self) -> JsonDict:
4848
"internal": dep.internal,
4949
}
5050
serialized_deps.append(source_dep)
51+
elif isinstance(dep, HeaderDep):
52+
header_dep: JsonDict = {
53+
"type": "HeaderDep",
54+
"path": dep.path,
55+
"include_dirs": dep.include_dirs,
56+
"internal": dep.internal,
57+
}
58+
serialized_deps.append(header_dep)
5159

5260
return {
5361
"fullname": self.fullname,
@@ -82,6 +90,14 @@ def deserialize(cls, data: JsonDict, ctx: DeserMaps) -> ModuleIR:
8290
internal=dep_dict["internal"],
8391
)
8492
)
93+
elif dep_dict["type"] == "HeaderDep":
94+
deps.add(
95+
HeaderDep(
96+
dep_dict["path"],
97+
include_dirs=dep_dict["include_dirs"],
98+
internal=dep_dict["internal"],
99+
)
100+
)
85101
module.dependencies = deps
86102

87103
return module

mypyc/test/test_cheader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import re
88
import unittest
99

10-
from mypyc.ir.deps import SourceDep
10+
from mypyc.ir.deps import HeaderDep, SourceDep
1111
from mypyc.ir.ops import PrimitiveDescription
1212
from mypyc.primitives import (
1313
bytearray_ops,
@@ -79,7 +79,7 @@ def check_name(name: str) -> None:
7979
for op in all_ops:
8080
if op.dependencies:
8181
for dep in op.dependencies:
82-
if isinstance(dep, SourceDep):
82+
if isinstance(dep, (SourceDep, HeaderDep)):
8383
header_fnam = os.path.join(base_dir, dep.get_header())
8484
if os.path.isfile(header_fnam):
8585
with open(os.path.join(base_dir, header_fnam)) as f:

0 commit comments

Comments
 (0)