Skip to content

Commit 5e43683

Browse files
committed
feat(pkgs): add Eigen blas feature (reference BLAS, no Fortran)
Correcting the earlier "BLAS needs Fortran, too heavy → no feature" call: it was wrong. Eigen's `eigen_blas` library (blas/CMakeLists.txt EigenBlas_SRCS) builds from blas/*.cpp (5 files) + blas/f2c/*.c (18 f2c-translated C files); the only .f files live under blas/testing/ (the test suite) and are NOT part of the library. So it compiles with a plain C/C++ toolchain and fits mcpp's sources-only feature gate exactly like compat.cjson's `utils`. - compat.eigen: add `features = { blas = { sources = {"*/blas/*.cpp", "*/blas/f2c/*.c"} } }`. Off by default; `features = ["blas"]` compiles Eigen's reference BLAS into the eigen lib, exposing the standard Fortran-ABI symbols (sgemm_/dgemm_/ddot_/…). - example now opts into `blas` and calls dgemm_ to exercise the feature in CI alongside the header-only core path. Verified on mcpp 0.0.68: - with feature: `eigen ok=1 core=1 blas(dgemm)=1 C=[1 3 2 4]`. - without feature (negative): `undefined reference to 'dgemm_'` — gate works. Descriptor comment + design doc updated with the corrected analysis.
1 parent 90e8b01 commit 5e43683

5 files changed

Lines changed: 109 additions & 51 deletions

File tree

.agents/docs/2026-06-28-add-eigen-plan.md

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,32 @@ Eigen 无可编译源(纯模板头),故:
3232
- 用一个 trivial anchor TU(`mcpp_generated/eigen_anchor.c`)给 mcpp 一个可构建的 `lib` 目标(同 opengl/khrplatform)。
3333
- `language=c++23``c_standard=c11`(anchor 是 C)、`deps={}`
3434

35-
## 3. feature 机制评估 —— 当前不适用(已记录原因)
36-
37-
用户要求"如果 Eigen 有类似 feature 的东西,可走 mcpp feature 机制"。结论:**当前(mcpp 0.0.68)不适用**,原因经源码核实:
38-
39-
- mcpp 包描述符的 `features`**只能门控 `sources`**(feature 贡献额外源码 glob,默认排除、请求时编入 ——
40-
`compat.cjson``utils``compat.gtest``main`;源码 `manifest.cppm` 中 feature 子字段仅识别 `sources`,
41-
其余被 skip)。
42-
- Eigen 是 header-only,**没有可门控的可选源码**。其天然的"可选轴"是 `unsupported/` 实验模块,但它与 `Eigen/`
43-
**同处 tarball 根** —— 任何暴露稳定核(`*`)的 include path 都不可避免地一并暴露 `unsupported/`(已实测:
44-
`<unsupported/Eigen/MatrixFunctions>` / `<AutoDiff>` 直接可编)。**既无源可门控,也无法用 sources-only 的
45-
feature 把头藏起来**
46-
- 其它 Eigen 开关(`EIGEN_MPL2_ONLY``EIGEN_USE_BLAS/LAPACKE`…)是编译 **define**,feature 表同样不能携带。
47-
- `blas/` 是唯一可门控的真实源,但需 Fortran(`.f`)+ 独立 lib 目标,feature 仅能往既有目标 *追加* 源 → 不匹配,过重。
35+
## 3. feature 机制 —— `blas`(已落地)
36+
37+
用户要求"如果 Eigen 有类似 feature 的东西,可走 mcpp feature 机制"。结论:**有,且已实现 `blas` feature**
38+
39+
> ⚠️ **修正**:初稿曾错判"`blas/` 需 Fortran、过重、不做"。复核 `blas/CMakeLists.txt``EigenBlas_SRCS`:
40+
> 库源是 `blas/*.cpp`(5 个:single/double/complex_single/complex_double/xerbla)+ `blas/f2c/*.c`(18 个,
41+
> **f2c = Fortran 已转 C**)。**唯一的 `.f` 全在 `blas/testing/`(测试套件),不参与库构建** → 纯 C++/C 即可编。
42+
43+
- **机制**:mcpp 包描述符的 `features` 表门控 `sources`(默认排除、请求时编入既有 lib 目标 —— 同 `compat.cjson`
44+
`utils``compat.gtest``main`)。Eigen 的 `eigen_blas` 正好匹配:
45+
```lua
46+
features = { ["blas"] = { sources = { "*/blas/*.cpp", "*/blas/f2c/*.c" } } }
47+
```
48+
默认不编;`eigen = { version = "5.0.1", features = ["blas"] }` 时编入 `eigen` lib,暴露标准 BLAS 符号
49+
(`sgemm_`/`dgemm_`/`ddot_`…,Fortran ABI)。`blas/common.h` 用相对 `../Eigen/Core` 取头,源码须就地编译,
50+
`*/blas/*.cpp` glob 保证它们留在解包树内 → OK。
51+
- **实测(mcpp 0.0.68)**:
52+
- 开 feature:example `eigen ok=1 core=1 blas(dgemm)=1 C=[1 3 2 4]`
53+
- 关 feature(负向):同样调用 `dgemm_``undefined reference to 'dgemm_'`(证明默认确实排除、门控生效)。
54+
55+
### 3.1 其余"非 feature"项(原因仍成立)
56+
- `unsupported/` 实验模块是 header-only,与 `Eigen/` 同处 tarball 根 —— 核 include path(`*`)已一并暴露它
57+
(实测 `<unsupported/Eigen/MatrixFunctions>` / `<AutoDiff>` 直接可编);sources-only 门控无法"藏头",故无可门控、
58+
直接可用。
59+
- Eigen 的编译 **define** 开关(`EIGEN_MPL2_ONLY``EIGEN_USE_BLAS/LAPACKE`…)feature 表(0.0.68)不能携带;
60+
若将来 feature 支持 define/cflags,`mpl2only`(→ `-DEIGEN_MPL2_ONLY`)是干净接入点。
4861

4962
→ 故本期 **暴露完整头集(core + unsupported)、不加 feature**,并在 descriptor 注释中完整记录该分析。
5063
若 mcpp 未来允许 feature 携带 define/cflags,`mpl2only`(→ `-DEIGEN_MPL2_ONLY`)是干净的接入点。
@@ -62,16 +75,18 @@ CN url:`https://gitcode.com/mcpp-res/eigen/releases/download/5.0.1/eigen-5.0.1.t
6275

6376
## 5. 最小工程 + CI
6477

65-
- `tests/examples/eigen/{mcpp.toml, src/main.cpp}` —— `#include <Eigen/Dense>`,2x2 线代往返断言
66-
(`A*x`、determinant、dot、QR solve)。纯文本 include,不混 `import std;`
78+
- `tests/examples/eigen/{mcpp.toml, src/main.cpp}` —— 既测 header-only 核(`#include <Eigen/Dense>`,2x2 线代往返:
79+
`A*x` / determinant / QR solve),又取 `features = ["blas"]``dgemm_` 验证 BLAS feature 端到端。纯文本 include,
80+
不混 `import std;`
6781
- `validate.yml` 的 detect 把 `compat.eigen.lua``eigen` → 命中 `tests/examples/eigen``smoke-examples (eigen)` 单跑。
6882
- `mirror-cn-reachable` 覆盖新 CN url。
6983

7084
## 6. 落地记录(2026-06-28)
7185

7286
- CN 镜像:`mcpp-res/eigen@5.0.1` 已建 + 发布,CN 与 GLOBAL byte-identical(sha `e9c326dc…`),http 200。
7387
- 本地实测(mcpp **0.0.68**,与 CI 同版本,`MCPP_INDEX_MIRROR=GLOBAL`):
74-
`tests/run_example.sh eigen``eigen ok=1 y=[3 7] det=-2 dot=5`,`OK: eigen`
88+
`tests/run_example.sh eigen`(开 `blas` feature)→ `eigen ok=1 core=1 blas(dgemm)=1 C=[1 3 2 4]`,`OK: eigen`
89+
- `blas` feature 门控负向实测:不带 feature 调 `dgemm_` → 链接 `undefined reference to 'dgemm_'`(默认排除生效)。
7590
- `unsupported/` 可达性单独实测:`g++ -std=c++23 -I<root>` 编 `<Eigen/Dense> + <unsupported/Eigen/MatrixFunctions> +
7691
<AutoDiff>` → rc=0。
7792
- 全量 lint 本地模拟通过(语法 / 必填字段 / 无前导 v / mirror url 检查)。

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ mcpp build # 自动拉取源码 + 构建
3939
| `glfw` | 3.4 | GLFW 窗口与输入库(X11/null 后端源码构建) |
4040
| `gtest` | 1.15.2 | Google Test 测试框架 |
4141
| `cjson` | 1.7.19 | 超轻量 ANSI C JSON 解析库(`#include <cJSON.h>`,`compat` 源码构建) |
42-
| `eigen` | 5.0.1 | C++ 模板线性代数库(header-only,`#include <Eigen/Dense>`;`unsupported/` 实验模块亦可用) |
42+
| `eigen` | 5.0.1 | C++ 模板线性代数库(header-only,`#include <Eigen/Dense>`;`unsupported/` 实验模块亦可用;`features = ["blas"]` 编入 Eigen 参考 BLAS) |
4343
| `imgui` | 1.92.8 | Dear ImGui immediate-mode GUI 核心源码 |
4444
| `opengl` | 2026.05.31 | Khronos OpenGL API 头文件 |
4545
| `glx-runtime` | 2026.06.03 | Linux host GLVND/GLX/OpenGL runtime adapter |

pkgs/c/compat.eigen.lua

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,42 @@
11
-- Form B inline descriptor for Eigen — a C++ template library for linear
2-
-- algebra (matrices, vectors, numerical solvers, related algorithms). Eigen
3-
-- is HEADER-ONLY: there is nothing to compile, so the package just exposes
4-
-- the source tree's root on the include path (`#include <Eigen/Dense>` etc.)
5-
-- and carries a tiny anchor translation unit so mcpp still has a buildable
6-
-- `lib` target (same shape as compat.opengl / compat.khrplatform).
2+
-- algebra (matrices, vectors, numerical solvers, related algorithms). The
3+
-- Eigen *core* is HEADER-ONLY: there is nothing to compile for normal use, so
4+
-- the package exposes the source tree's root on the include path
5+
-- (`#include <Eigen/Dense>` etc.) and carries a tiny anchor translation unit
6+
-- so mcpp always has a buildable `lib` target (same shape as compat.opengl /
7+
-- compat.khrplatform). The optional `blas` feature additionally compiles
8+
-- Eigen's reference BLAS into that lib (see below).
79
--
810
-- `include_dirs = {"*"}` points at the tarball root (`eigen-<ver>/`), which is
911
-- exactly the directory upstream tells you to put on the include path. That
1012
-- makes BOTH the stable modules under `Eigen/` and the experimental modules
1113
-- under `unsupported/Eigen/` (Tensor, AutoDiff, Splines, MatrixFunctions, …)
12-
-- resolvable — see the note on features below for why `unsupported` is not a
13-
-- separate opt-in here.
14+
-- resolvable out of the box.
1415
--
15-
-- On the `feature` mechanism (asked for, deliberately omitted — analysis):
16-
-- mcpp's package-descriptor `features` table gates **sources only** (a
17-
-- feature contributes extra source globs that are excluded by default and
18-
-- compiled in when the feature is requested — see compat.cjson's `utils`
19-
-- and compat.gtest's `main`). Eigen has no such optional *source*: it is
20-
-- header-only. Its natural opt-in axis would be the `unsupported/` modules,
21-
-- but those are headers that live BESIDE `Eigen/` under the same tarball
22-
-- root, so any include path that exposes the stable core (`*`) inevitably
23-
-- exposes `unsupported/` too — there is no source to gate and no way to
24-
-- hide the headers behind a feature with the current (sources-only) gate.
25-
-- Other Eigen knobs (EIGEN_MPL2_ONLY, EIGEN_USE_BLAS/LAPACKE, …) are
26-
-- compile *defines*, which the feature table also cannot carry on mcpp
27-
-- 0.0.68. So a feature here would be cosmetic/misleading; we expose the
28-
-- full header set instead and document it. If mcpp later lets a feature
29-
-- contribute defines/cflags, an `mpl2only` (-> -DEIGEN_MPL2_ONLY) feature
30-
-- would be the clean fit.
16+
-- Feature: `blas` — Eigen's reference BLAS implementation.
17+
-- Eigen ships a full BLAS library under `blas/` (the `eigen_blas` target in
18+
-- upstream CMake). Despite the historical name, it builds from pure C++
19+
-- (`blas/*.cpp`) + f2c-translated C (`blas/f2c/*.c`) — NO Fortran compiler
20+
-- is needed (the only `.f` files live under `blas/testing/`, the test
21+
-- suite, and are not part of the library). So it fits mcpp's sources-only
22+
-- feature gate cleanly, exactly like compat.cjson's `utils`: excluded by
23+
-- default, and compiled into the `eigen` lib when the dependency requests
24+
-- `features = ["blas"]`. The result exposes the standard BLAS symbols
25+
-- (sgemm_/dgemm_/ddot_/… , Fortran ABI) for code that wants to link a BLAS.
26+
-- Common.h pulls Eigen via a path RELATIVE to blas/ (`../Eigen/Core`), so
27+
-- the sources must compile in place — they do, since `*/blas/*.cpp` keeps
28+
-- them under the unpacked tree. Verified locally on mcpp 0.0.68.
29+
--
30+
-- What is NOT a feature here (and why):
31+
-- * `unsupported/` modules are header-only and live BESIDE `Eigen/` under
32+
-- the same tarball root, so the core include path (`*`) already exposes
33+
-- them; a sources-only gate cannot hide headers, so there is nothing to
34+
-- gate — they are simply available.
35+
-- * Eigen's compile-define knobs (EIGEN_MPL2_ONLY, EIGEN_USE_BLAS/LAPACKE,
36+
-- …) are preprocessor defines; the feature table carries only `sources`
37+
-- on mcpp 0.0.68, so they cannot be feature-gated yet. If mcpp later lets
38+
-- a feature contribute defines/cflags, `mpl2only` (-> -DEIGEN_MPL2_ONLY)
39+
-- would be the clean fit.
3140
--
3241
-- All `mcpp` paths are GLOBS relative to the verdir; the leading `*` absorbs
3342
-- the GitLab archive's `eigen-<tag>/` wrap layer.
@@ -83,6 +92,19 @@ package = {
8392
},
8493
sources = { "mcpp_generated/eigen_anchor.c" },
8594
targets = { ["eigen"] = { kind = "lib" } },
95+
-- Optional: compile Eigen's reference BLAS (`eigen_blas`) into the lib.
96+
-- C++ + f2c-C only, no Fortran. Off by default; pulled in with
97+
-- `features = ["blas"]`. blas/ has exactly the 5 library .cpp and
98+
-- blas/f2c/ exactly the 18 library .c (the upstream eigen_blas source
99+
-- set); the `*.cpp`/`*.c` globs match them and nothing else.
100+
features = {
101+
["blas"] = {
102+
sources = {
103+
"*/blas/*.cpp",
104+
"*/blas/f2c/*.c",
105+
},
106+
},
107+
},
86108
deps = { },
87109
},
88110
}

tests/examples/eigen/mcpp.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ default = "gcc@16.1.0"
1010
[indices]
1111
compat = { path = "../../.." }
1212

13+
# `features = ["blas"]` opts into Eigen's reference BLAS (eigen_blas) so this
14+
# example also exercises the feature-gated source build. Without it, the core
15+
# header-only `#include <Eigen/...>` path still works on its own.
1316
[dependencies.compat]
14-
eigen = "5.0.1"
17+
eigen = { version = "5.0.1", features = ["blas"] }
1518

1619
[targets.eigen-example]
1720
kind = "bin"

tests/examples/eigen/src/main.cpp

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,47 @@
11
// Eigen is header-only; the compat package puts the source tree root on the
22
// include path so `#include <Eigen/...>` just works. Plain textual includes
33
// here (Eigen is not a module), so we do NOT mix in `import std;`.
4+
//
5+
// This example also opts into the `blas` feature (see mcpp.toml), which
6+
// compiles Eigen's reference BLAS into the lib — so we additionally link and
7+
// call dgemm_ (Fortran BLAS ABI) to prove the feature-gated build works.
48
#include <Eigen/Dense>
59
#include <cstdio>
610
#include <cmath>
711

12+
// Standard Fortran BLAS ABI, provided by Eigen's eigen_blas (feature "blas").
13+
extern "C" void dgemm_(const char* transa, const char* transb,
14+
const int* m, const int* n, const int* k,
15+
const double* alpha, const double* a, const int* lda,
16+
const double* b, const int* ldb,
17+
const double* beta, double* c, const int* ldc);
18+
819
int main() {
20+
// --- core Eigen (header-only) ---
921
// A * x for a 2x2 system: [[1,2],[3,4]] * [1,1]^T = [3,7]^T
1022
Eigen::Matrix2d A;
1123
A << 1, 2,
1224
3, 4;
1325
Eigen::Vector2d x(1.0, 1.0);
1426
Eigen::Vector2d y = A * x;
27+
double det = A.determinant(); // -2
28+
Eigen::Vector2d z = A.colPivHouseholderQr().solve(y); // back to [1,1]
1529

16-
double det = A.determinant(); // 1*4 - 2*3 = -2
17-
double dot = x.dot(Eigen::Vector2d(2.0, 3.0)); // 1*2 + 1*3 = 5
18-
19-
// Solve A z = y, expect z back to [1,1].
20-
Eigen::Vector2d z = A.colPivHouseholderQr().solve(y);
30+
bool core_ok = y(0) == 3.0 && y(1) == 7.0 && det == -2.0
31+
&& std::abs(z(0) - 1.0) < 1e-9 && std::abs(z(1) - 1.0) < 1e-9;
2132

22-
bool ok = y(0) == 3.0 && y(1) == 7.0
23-
&& det == -2.0 && dot == 5.0
24-
&& std::abs(z(0) - 1.0) < 1e-9 && std::abs(z(1) - 1.0) < 1e-9;
33+
// --- BLAS feature: C = A * B via dgemm_ (column-major) ---
34+
// A = [[1,2],[3,4]] col-major = {1,3,2,4}; B = identity. Expect C == A.
35+
const double a[4] = {1, 3, 2, 4};
36+
const double b[4] = {1, 0, 0, 1};
37+
double c[4] = {0, 0, 0, 0};
38+
const int n = 2;
39+
const double one = 1.0, zero = 0.0;
40+
dgemm_("N", "N", &n, &n, &n, &one, a, &n, b, &n, &zero, c, &n);
41+
bool blas_ok = c[0] == 1 && c[1] == 3 && c[2] == 2 && c[3] == 4;
2542

26-
std::printf("eigen ok=%d y=[%g %g] det=%g dot=%g\n",
27-
ok, y(0), y(1), det, dot);
43+
bool ok = core_ok && blas_ok;
44+
std::printf("eigen ok=%d core=%d blas(dgemm)=%d y=[%g %g] C=[%g %g %g %g]\n",
45+
ok, core_ok, blas_ok, y(0), y(1), c[0], c[1], c[2], c[3]);
2846
return ok ? 0 : 1;
2947
}

0 commit comments

Comments
 (0)