diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5721ea..ba260c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,13 @@ jobs: - name: Tests (buffers, streams, filter chains, .lzma, checks, errors, the constants, every function linked, header mixing) run: mcpp test $TARGET + # The release profile compiles a function to a section of its own and the link + # collects the unreferenced ones. 5.8.3 passed every test in the dev profile on + # Windows and faulted here: the functions were reached through import slots the + # linker made, and collected. + - name: Tests, where the link collects unreferenced sections (the release profile) + run: mcpp test --profile release $TARGET + - name: Export audit (the headers as clang reads them on this host) if: startsWith(matrix.toolchain, 'llvm') run: python3 tools/generate.py --check diff --git a/README.md b/README.md index 66edd6c..63c4904 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ upstream's own test files with the same library. ## Getting started ```sh -mcpp new pack --template mcpplibs.mwrap.xz@5.8.3 +mcpp new pack --template mcpplibs.mwrap.xz@5.8.3.1 cd pack mcpp run ``` @@ -37,7 +37,7 @@ it, and prints the sizes and the checksum. ```toml [dependencies] -"mcpplibs.mwrap.xz" = "5.8.3" +"mcpplibs.mwrap.xz" = "5.8.3.1" ``` ```cpp diff --git a/README.zh.md b/README.zh.md index 7a040ff..804a7df 100644 --- a/README.zh.md +++ b/README.zh.md @@ -24,7 +24,7 @@ C++ 常量。每次 CI 都用同一份库解码上游自己的测试文件。 ## 快速开始 ```sh -mcpp new pack --template mcpplibs.mwrap.xz@5.8.3 +mcpp new pack --template mcpplibs.mwrap.xz@5.8.3.1 cd pack mcpp run ``` @@ -33,7 +33,7 @@ mcpp run ```toml [dependencies] -"mcpplibs.mwrap.xz" = "5.8.3" +"mcpplibs.mwrap.xz" = "5.8.3.1" ``` ```cpp diff --git a/docs/usage.md b/docs/usage.md index 6183729..dfa8b01 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -54,3 +54,12 @@ values under other names and `#undef` the macros before naming the constants The package depends on `compat.xz` 5.8.3, which builds liblzma from the release sources with the target's toolchain. A program that imports the module links that library; it does not look for a system liblzma. + +The library is static, and the module reads `lzma.h` with `LZMA_API_STATIC` +defined (5.8.3.1). On Windows the header otherwise declares every function +imported from a DLL, and a unit that read it so calls through `__imp_lzma_*`. +A unit that includes `lzma.h` itself beside the module defines +`LZMA_API_STATIC` before it, or includes `` first, which +defines it. 5.8.3 did not: its tests passed in the dev profile, and a program +built in the release profile, where the link collects unreferenced sections, +faulted at its first call into the library on Windows. diff --git a/include/mwrap/xz/macros.hpp b/include/mwrap/xz/macros.hpp index c91e3a8..b472f56 100644 --- a/include/mwrap/xz/macros.hpp +++ b/include/mwrap/xz/macros.hpp @@ -9,6 +9,12 @@ #ifndef MWRAP_XZ_MACROS_HPP #define MWRAP_XZ_MACROS_HPP +// The configuration the compat package compiles with: a unit that includes the +// upstream header itself shares it by including this header first. +#ifndef LZMA_API_STATIC +#define LZMA_API_STATIC +#endif + #ifndef LZMA_STREAM_INIT #define LZMA_STREAM_INIT { nullptr, 0, 0, nullptr, 0, 0, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, 0, 0, 0, 0, LZMA_RESERVED_ENUM, LZMA_RESERVED_ENUM } #endif diff --git a/mcpp.toml b/mcpp.toml index 5b4f981..c8fceea 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -2,7 +2,7 @@ [package] namespace = "mcpplibs.mwrap" name = "xz" -version = "5.8.3" +version = "5.8.3.1" description = "XZ Utils 5.8.3 liblzma C++ modules for mcpp (import mcpplibs.mwrap.xz;): .xz and .lzma streams, filters, blocks, indexes and checks over compat.xz, with the constants of lzma.h as C++ constants" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/macros.cppm b/src/macros.cppm index 8915253..0973314 100644 --- a/src/macros.cppm +++ b/src/macros.cppm @@ -3,6 +3,9 @@ // the macro where the module is built (a named module cannot export a macro). The // other macros are in . Written by tools/generate.py. module; +#ifndef LZMA_API_STATIC +#define LZMA_API_STATIC +#endif #include // The values, taken under other names while the macros are defined. diff --git a/src/xz.cppm b/src/xz.cppm index 1b01175..4375f92 100644 --- a/src/xz.cppm +++ b/src/xz.cppm @@ -3,6 +3,9 @@ // upstream declares it; the root also re-exports the macro mapping. The implementation // is compat.xz 5.8.3. Written by tools/generate.py from the headers. module; +#ifndef LZMA_API_STATIC +#define LZMA_API_STATIC +#endif #include export module mcpplibs.mwrap.xz; diff --git a/tests/header_mix_test.cpp b/tests/header_mix_test.cpp index 67a3329..791ea83 100644 --- a/tests/header_mix_test.cpp +++ b/tests/header_mix_test.cpp @@ -1,5 +1,11 @@ // The header and the module in one translation unit: the same entities, so a // stream set up through one spelling is ended through the other. +// +// A unit that includes the upstream header itself shares the configuration compat.xz +// is compiled with ( states it too): the library is static. +#ifndef LZMA_API_STATIC +#define LZMA_API_STATIC +#endif #include import std; import mcpplibs.mwrap.xz; diff --git a/tests/linked.cpp b/tests/linked.cpp index bb1c4ff..4361320 100644 --- a/tests/linked.cpp +++ b/tests/linked.cpp @@ -120,6 +120,17 @@ int main() }; int missing = 0; for (const void* f : functions) missing += f == nullptr; - std::println("110 functions linked, {} null", missing); - return missing != 0; + // The functions are not at one address. A link that resolved them through slots it + // made for them and then dropped the functions leaves every slot the same address, + // which is not null. (Two functions of identical code may share one where the + // linker folds them, so only a run of more than half is read as that.) + std::vector sorted(std::begin(functions), std::end(functions)); + std::ranges::sort(sorted, std::less{}); + std::size_t run = 1, longest = 1; + for (std::size_t i = 1; i < sorted.size(); ++i) { + run = sorted[i] == sorted[i - 1] ? run + 1 : 1; + longest = std::max(longest, run); + } + std::println("110 functions linked, {} null, at most {} at one address", missing, longest); + return missing != 0 || longest > sorted.size() / 2; } diff --git a/tests/macros.cpp b/tests/macros.cpp index fd669bf..419cc43 100644 --- a/tests/macros.cpp +++ b/tests/macros.cpp @@ -2,6 +2,9 @@ // expands even in a qualified name, so the values of the headers are taken under other // names and the macros undefined before the mapped names are used. Written by // tools/generate.py. +#ifndef LZMA_API_STATIC +#define LZMA_API_STATIC +#endif #include namespace header { diff --git a/tools/generate.py b/tools/generate.py index bc63b7a..df4e566 100644 --- a/tools/generate.py +++ b/tools/generate.py @@ -131,6 +131,10 @@ def render(cfg, surface, constants, types, kept): '// The upstream header included after this one redefines these macros.', '// Written by tools/generate.py.', f'#ifndef {guard}', f'#define {guard}'] + if cfg.get('defines'): + header += ['', '// The configuration the compat package compiles with: a unit that includes the', + '// upstream header itself shares it by including this header first.'] + header += [f'#ifndef {d}\n#define {d}\n#endif' for d in cfg['defines']] for n in sorted(kept): # A module-only unit has no NULL macro; in C++ nullptr is its meaning. body = re.sub(r'\bNULL\b', 'nullptr', qualify(kept[n]['body'], set(mapped), f'{scope}::macros')) @@ -165,8 +169,19 @@ def render(cfg, surface, constants, types, kept): ' const void* volatile functions[] = {'] linked += [f' reinterpret_cast(&{n}),' for n in functions] linked += [' };', ' int missing = 0;', ' for (const void* f : functions) missing += f == nullptr;', - f' std::println("{len(functions)} functions linked, {{}} null", missing);', - ' return missing != 0;', '}'] + ' // The functions are not at one address. A link that resolved them through slots it', + ' // made for them and then dropped the functions leaves every slot the same address,', + ' // which is not null. (Two functions of identical code may share one where the', + ' // linker folds them, so only a run of more than half is read as that.)', + ' std::vector sorted(std::begin(functions), std::end(functions));', + ' std::ranges::sort(sorted, std::less{});', + ' std::size_t run = 1, longest = 1;', + ' for (std::size_t i = 1; i < sorted.size(); ++i) {', + ' run = sorted[i] == sorted[i - 1] ? run + 1 : 1;', + ' longest = std::max(longest, run);', + ' }', + f' std::println("{len(functions)} functions linked, {{}} null, at most {{}} at one address", missing, longest);', + ' return missing != 0 || longest > sorted.size() / 2;', '}'] join = lambda lines: '\n'.join(lines) + '\n' return {f'src/{name}.cppm': join(module), 'src/macros.cppm': join(macros), f'include/mwrap/{name}/macros.hpp': join(header), 'tests/macros.cpp': join(test), diff --git a/tools/surface.toml b/tools/surface.toml index 279052c..b84bf02 100644 --- a/tools/surface.toml +++ b/tools/surface.toml @@ -6,6 +6,13 @@ version = "5.8.3" license = "0BSD" generated-on = "linux" headers = ["lzma.h"] +# compat.xz is a static library. On Windows lzma.h declares every function imported +# from a DLL unless this is defined (LZMA_API_IMPORT): a unit that read it so calls +# through `__imp_lzma_*`, which no DLL provides. lld-link then makes a slot of its own +# for each and does not count the slot as a reference, so in a link that collects +# unreferenced sections (-ffunction-sections, the release profile) the functions are +# dropped and each slot holds the image's first byte: the call faults (5.8.3). +defines = ["LZMA_API_STATIC"] owned = ["lzma.h", "lzma/base.h", "lzma/bcj.h", "lzma/block.h", "lzma/check.h", "lzma/container.h", "lzma/delta.h", "lzma/filter.h", "lzma/hardware.h", "lzma/index.h", "lzma/index_hash.h", "lzma/lzma12.h", "lzma/stream_flags.h", "lzma/version.h", "lzma/vli.h"]