diff --git a/.agents/docs/2026-10-18-add-safeint-plan.md b/.agents/docs/2026-10-18-add-safeint-plan.md new file mode 100644 index 00000000..f47885ed --- /dev/null +++ b/.agents/docs/2026-10-18-add-safeint-plan.md @@ -0,0 +1,63 @@ +# Add SafeInt 3.0.28a as compat.safeint + +## Source and shape + +- Upstream: . +- Version: `3.0.28a`, the newest upstream tag. The local checkout is 25 + commits past that tag, so the package pins the release rather than an + untagged development snapshot. +- License: MIT. +- Shape: Form B, header-only. `SafeInt.hpp`, `safe_math.h`, and + `safe_math_impl.h` are at the archive root, so `include_dirs = { "*" }` + matches upstream's CMake INTERFACE include directory. A trivial anchor C TU + supplies mcpp's buildable `safeint` lib target. +- No features: the archive has no optional compilable library component. + Upstream's tests are intentionally excluded. + +Upstream provides two public APIs. `SafeInt.hpp` implements the C++ +`SafeInt` class. `safe_math.h` provides checked C arithmetic for 32- and +64-bit integers. `safe_math.h` raises `#error` when `SafeInt.hpp` has already +been included, so the test project exercises them from separate translation +units. + +## Download and mirror + +- GLOBAL: + `https://github.com/dcleblanc/SafeInt/archive/refs/tags/3.0.28a.tar.gz` +- SHA-256: + `9e652d065a3cef80623287d5dc61edcf6a95ddab38a9dfeb34f155261fc9cef7` +- The archive was downloaded twice and the bytes compared; both downloads had + the same digest. +- No verified `mcpp-res/safeint` release is available, so the descriptor uses + the repository's plain-string upstream URL fallback. It does not invent a CN + mirror. A maintainer can add a byte-identical GitCode release later. + +## Test member + +Two members are registered in the root workspace because mcpp builds every +`tests/*.cpp` as an independent executable (they are tests, not object files +linked into one binary): + +- `tests/examples/safeint/`: SafeInt addition, subtraction, multiplication and + division; successful and rejected `SafeCast`; and overflow and + divide-by-zero exception codes. +- `tests/examples/safeint-c-api/`: checked add/subtract/multiply/divide through + `safe_math.h`, including rejection of `INT32_MAX * 2`. The header is wrapped + in `extern "C"` here because upstream's own C++ linkage spelling is + inconsistent, while the intended consumer is C. + +Every assertion affects the process exit status (`return ok ? 0 : 1`). + +## Verification + +- Pinned mcpp `2026.10.1.2`, the version in `validate.yml`: + `mcpp xpkg parse --all-os pkgs/c/compat.safeint.lua` reports `parse OK` for + linux, macosx, and windows. +- Pinned cold test: + `mcpp test -p safeint -p safeint-c-api --cache off` reports + `workspace result ok. 2 member(s); 2 passed; 0 failed`. +- Full relevant lint passed: Lua syntax, mirror URL policy, package identity, + reserved namespaces, cross-package references, platform parity, and + duplicate versions. +- A direct GCC 16.2.1 compile against the local checkout also ran both tests: + `safeint OK` and `safeint C API OK`. diff --git a/README.md b/README.md index 45888654..32a700ad 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ One descriptor per common shape: | Native module library (Form A) | [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | | C sources + `features` | [`compat.cjson`](pkgs/c/compat.cjson.lua) | | C++ sources, several versions | [`compat.yaml-cpp`](pkgs/c/compat.yaml-cpp.lua) | -| Header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | +| Header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) · [`compat.safeint`](pkgs/c/compat.safeint.lua) | | Generated config header | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | | C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | | C++23 module shipped by upstream | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | diff --git a/README.zh-CN.md b/README.zh-CN.md index cc334fa8..e39065ee 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -37,7 +37,7 @@ mcpp self config --mirror CN # 切换至国内镜像,默认使用 GLOBAL 上 | 原生模块库(Form A) | [`mcpplibs.tinyhttps`](pkgs/t/tinyhttps.lua) · [`gzj-creator.galay`](pkgs/g/gzj-creator.galay.lua) | | C 源码 + `features` | [`compat.cjson`](pkgs/c/compat.cjson.lua) | | C++ 源码,多版本 | [`compat.yaml-cpp`](pkgs/c/compat.yaml-cpp.lua) | -| header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) | +| header-only | [`compat.gtl`](pkgs/c/compat.gtl.lua) · [`compat.safeint`](pkgs/c/compat.safeint.lua) | | 生成 config 头 | [`compat.c-ares`](pkgs/c/compat.c-ares.lua) | | C++23 module wrapper | [`nlohmann.json`](pkgs/n/nlohmann.json.lua) | | 上游自带 C++23 module | [`khronos.vulkan-hpp`](pkgs/k/khronos.vulkan-hpp.lua) | diff --git a/mcpp.toml b/mcpp.toml index 206c958d..ff8a0cdf 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -103,6 +103,8 @@ members = [ "tests/examples/redis-plus-plus", "tests/examples/redis-plus-plus-v133", "tests/examples/redis-plus-plus-async", + "tests/examples/safeint", + "tests/examples/safeint-c-api", "tests/examples/sdl2", "tests/examples/sdl3", "tests/examples/spdlog", diff --git a/pkgs/c/compat.safeint.lua b/pkgs/c/compat.safeint.lua new file mode 100644 index 00000000..a49108dc --- /dev/null +++ b/pkgs/c/compat.safeint.lua @@ -0,0 +1,63 @@ +-- Form B inline descriptor for SafeInt — header-only checked integer arithmetic. +-- +-- Upstream ships two independent public APIs: +-- * SafeInt.hpp — the C++ SafeInt class; +-- * safe_math.h — checked C arithmetic over 32- and 64-bit integers. +-- +-- Both headers, and safe_math.h's safe_math_impl.h, sit at the archive root, +-- exactly like upstream's INTERFACE include directory. A trivial anchor TU +-- gives mcpp a buildable lib target, following the compat.gtl / compat.eigen +-- shape. There are no optional compilable components to expose as features. +-- +-- safe_math.h deliberately rejects a translation unit that has already +-- included SafeInt.hpp, so these APIs must be consumed from separate TUs. +-- A C++ TU using only the C API should wrap #include in +-- extern "C"; this keeps upstream's prototype and inline definitions on the +-- same linkage. +package = { + spec = "1", + namespace = "compat", + name = "safeint", + description = "Header-only checked integer arithmetic for C++ and C", + licenses = {"MIT"}, + repo = "https://github.com/dcleblanc/SafeInt", + type = "package", + + xpm = { + linux = { + ["3.0.28a"] = { + -- No verified mcpp-res release is available: use the upstream + -- archive directly rather than fabricating a CN mirror table. + url = "https://github.com/dcleblanc/SafeInt/archive/refs/tags/3.0.28a.tar.gz", + sha256 = "9e652d065a3cef80623287d5dc61edcf6a95ddab38a9dfeb34f155261fc9cef7", + }, + }, + macosx = { + ["3.0.28a"] = { + url = "https://github.com/dcleblanc/SafeInt/archive/refs/tags/3.0.28a.tar.gz", + sha256 = "9e652d065a3cef80623287d5dc61edcf6a95ddab38a9dfeb34f155261fc9cef7", + }, + }, + windows = { + ["3.0.28a"] = { + url = "https://github.com/dcleblanc/SafeInt/archive/refs/tags/3.0.28a.tar.gz", + sha256 = "9e652d065a3cef80623287d5dc61edcf6a95ddab38a9dfeb34f155261fc9cef7", + }, + }, + }, + + mcpp = { + language = "c++23", + import_std = false, + c_standard = "c11", + include_dirs = { "*" }, + generated_files = { + ["mcpp_generated/safeint_anchor.c"] = [==[ +int mcpp_compat_safeint_headers_anchor(void) { return 0; } +]==], + }, + sources = { "mcpp_generated/safeint_anchor.c" }, + targets = { ["safeint"] = { kind = "lib" } }, + deps = { }, + }, +} diff --git a/tests/examples/safeint-c-api/mcpp.toml b/tests/examples/safeint-c-api/mcpp.toml new file mode 100644 index 00000000..413efab7 --- /dev/null +++ b/tests/examples/safeint-c-api/mcpp.toml @@ -0,0 +1,8 @@ +# The C API cannot share a translation unit with SafeInt.hpp, and mcpp builds +# every tests/*.cpp as its own executable, so it gets a second member. +[package] +name = "safeint-c-api-tests" +version = "0.1.0" + +[dependencies.compat] +safeint = "3.0.28a" diff --git a/tests/examples/safeint-c-api/tests/safe_math.cpp b/tests/examples/safeint-c-api/tests/safe_math.cpp new file mode 100644 index 00000000..d6bd3de6 --- /dev/null +++ b/tests/examples/safeint-c-api/tests/safe_math.cpp @@ -0,0 +1,37 @@ +// safe_math.h is the C API. Include it inside extern "C" so its prototype and +// inline definitions share C linkage; it also rejects a translation unit that +// included SafeInt.hpp. +#include + +extern "C" { +#include +} + +int main() { + bool ok = true; + auto check = [&](bool condition, const char* what) { + if (!condition) { + std::printf("FAIL: %s\n", what); + ok = false; + } + }; + + int result = 0; + + check(check_add_int_int(40, 2, &result) && result == 42, + "checked C addition"); + check(check_sub_int_int(44, 2, &result) && result == 42, + "checked C subtraction"); + check(check_mul_int_int(21, 2, &result) && result == 42, + "checked C multiplication"); + check(check_div_int_int(84, 2, &result) && result == 42, + "checked C division"); + check(!check_mul_int_int(INT32_MAX, 2, &result), + "checked C multiplication rejects overflow"); + + if (ok) { + std::puts("safeint C API OK"); + } + + return ok ? 0 : 1; +} diff --git a/tests/examples/safeint/mcpp.toml b/tests/examples/safeint/mcpp.toml new file mode 100644 index 00000000..99e88cd7 --- /dev/null +++ b/tests/examples/safeint/mcpp.toml @@ -0,0 +1,9 @@ +# SafeInt test project: one TU covers SafeInt.hpp and another covers the +# independent safe_math.h API. Upstream forbids including both headers in the +# same translation unit. +[package] +name = "safeint-tests" +version = "0.1.0" + +[dependencies.compat] +safeint = "3.0.28a" diff --git a/tests/examples/safeint/tests/safeint.cpp b/tests/examples/safeint/tests/safeint.cpp new file mode 100644 index 00000000..324f85aa --- /dev/null +++ b/tests/examples/safeint/tests/safeint.cpp @@ -0,0 +1,56 @@ +// compat.safeint — assert the C++ class's normal arithmetic, non-throwing cast +// check, and both overflow and divide-by-zero exception paths. +#include + +#include +#include +#include + +int main() { + bool ok = true; + auto check = [&](bool condition, const char* what) { + if (!condition) { + std::printf("FAIL: %s\n", what); + ok = false; + } + }; + + SafeInt lhs = 40; + SafeInt rhs = 2; + check(static_cast(lhs + rhs) == 42, + "SafeInt addition"); + check(static_cast((lhs + rhs) * 2) == 84, + "SafeInt multiplication"); + check(static_cast((lhs + rhs) / rhs) == 21, + "SafeInt division"); + + std::int32_t narrowed = 0; + check(SafeCast(std::uint64_t{42}, narrowed) && narrowed == 42, + "SafeCast accepts an in-range value"); + check(!SafeCast(std::numeric_limits::max(), narrowed), + "SafeCast rejects an out-of-range value"); + + bool overflow_reported = false; + try { + SafeInt maximum = + std::numeric_limits::max(); + maximum += 1; + } catch (const SafeIntException& error) { + overflow_reported = error.m_code == SafeIntArithmeticOverflow; + } + check(overflow_reported, "overflow throws SafeIntArithmeticOverflow"); + + bool div_zero_reported = false; + try { + SafeInt value = 42; + value /= 0; + } catch (const SafeIntException& error) { + div_zero_reported = error.m_code == SafeIntDivideByZero; + } + check(div_zero_reported, "divide by zero throws SafeIntDivideByZero"); + + if (ok) { + std::puts("safeint OK"); + } + return ok ? 0 : 1; +}