关于 stata-c-plugins
面向开发者的 Stata C/C++ 插件开发技能,适用于构建或迁移计算功能。内容涵盖 SDK、安全、测试及从 macOS 开发环境进行跨平台打包。
主要功能
- 01Stata 插件 SDK 指引
- 02C 和 C++ 实现模式
- 03内存安全与测试建议
- 04跨平台编译和打包
- 05Stata .ado 封装模式
适用场景
- 01基于现有 C++ 库构建 Stata 插件
- 02将统计软件包功能迁移到 Stata
- 03为 macOS、Linux 和 Windows 打包插件
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dylantmoore/ stata-skill --skill stata-c-plugins安装后在 Claude Code 中输入/stata-c-plugins确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/cpp_plugins.mdpackaging_and_help.mdperformance_patterns.mdtesting_strategy.mdtranslation_workflow.md详细介绍
Stata C/C++ 插件开发
为 Stata 构建高性能的 C/C++ 插件。本技能涵盖完整的开发生命周期,从 SDK 配置到跨平台分发,基于为统计插补、随机森林、字符串匹配和因果推断构建生产级 Stata 插件的真实经验。
本技能假设开发平台为 macOS(Apple Silicon 或 Intel)。 构建命令、交叉编译工作流和 Docker 说明均面向 Mac。插件本身面向所有四个平台(macOS ARM64、macOS x86_64、Linux x86_64、Windows x86_64),但开发环境是 macOS。如果需要在 Linux 或 Windows 上原生开发,请相应调整编译和 Docker 部分。
如何处理每项任务
编写任何代码之前,先进入计划模式。 一份好的计划应涵盖:
- 完整清单 — 要构建的每项功能、选项和组件(对于翻译:源软件包 API 的完整目录)
- 架构决策 — 封装 C++ 后端、从头编写 C,还是仅使用 Stata
- 相关参考文件 — 提前确定本技能的哪些参考文件包含所需信息,并在计划步骤中明确引用,以便在恰当时机加载:
references/translation_workflow.md— 完整翻译工作流、测试改造、保真度审查references/testing_strategy.md— 测试层级、参考数据生成、第 0 层(改造原始测试)references/performance_patterns.md— pthreads、XorShift RNG、快速选择、预排序索引references/packaging_and_help.md— .toc/.pkg/.sthlp 模板、构建脚本references/cpp_plugins.md— C++ 封装、extern "C"、异常安全、编译
- 按阶段划分的步骤,以及步骤之间的依赖关系
- 每个步骤: 构建内容、编写的测试,以及继续之前运行审查流程
- 对于翻译项目: 将最终保真度审查作为最后一步(参见
translation_workflow.md)
按组件顺序实施,在每个组件内并行开展。 定义接口后,将相互独立的子任务分派给并行子 Agent(例如,C 插件实现、.ado 封装器和测试套件可以同时进行)。合并其工作,运行完整测试套件,然后在进入下一个组件之前执行审查流程。
每完成一个组件后运行审查流程:
- 默认做法:并行分派 2-3 个审查 Agent,最好来自不同模型(例如 Claude + GPT + Gemini),以获得多样化视角。使用环境中可用的多模型工具。
- 如果只有一个模型可用:分派 2-3 个 Agent,分别从不同角度审查(正确性、完整性、架构)。使用不同提示词,尽量模拟不同模型带来的多样性。
- 每个 Agent 审查差异、测试结果和需求,并遵循以下指示:“列出任何遗漏、错误或问题。如果一切看起来正确,请说 LGTM。”
- 修复提出的所有问题后,重新分派审查并循环进行,直到所有 Agent 都说 LGTM。然后继续。
优先封装,其次才从头编写
翻译一个程序包时,务必先检查是否已有 C/C++ 后端,再编写任何算法代码。 许多 R 程序包在 src/ 中包含 C++ 代码。许多 Python 程序包包含 Cython 或随附的 C/C++ 库。也有许多独立的 C++ 库可用于字符串匹配、线性代数、树算法等。
如果存在 C++ 实现,就封装它。 不要用 C 重新实现算法。封装可确保输出一致(使用相同代码路径)、达到生产级性能,并大幅减少代码量。插件只是 Stata SDK 与库 API 之间的一层轻量 extern "C" 粘合代码。二进制文件大小无关紧要——静态链接所有内容(-static-libstdc++ -static-libgcc),然后发布编译出的二进制文件,大小无论是多少都可以,哪怕在 Windows 上达到 10-15 MB。用户不在乎插件文件有多大,只在乎结果是否正确。
完整模式请参见 references/cpp_plugins.md,工作流请参见 references/translation_workflow.md。项目 CLAUDE.md 中「示例应用」部分列出的仓库包含采用这种方法的有效示例(封装 C++ 后端、多插件分发,以及用于在新数据上评分的保存/加载功能)。
对于翻译项目,还应:沿用原始程序包的测试套件和数据(参见 references/testing_strategy.md 中的第 0 层),编写额外的 Stata 专属测试,并以多 Agent 忠实度审查作为计划的最后一步。完整工作流请参见 references/translation_workflow.md。
插件 SDK
从以下网址下载 stplugin.h 和 stplugin.c:https://www.stata.com/plugins/
这两个文件定义了 C 代码与 Stata 之间的接口:
| 函数/宏 | 用途 |
|---|---|
SF_vdata(var, obs, &val) | 读取变量值(从 1 开始计数!) |
SF_vstore(var, obs, val) | 写入变量值(从 1 开始计数!) |
SF_nobs() | 当前数据集中的观测数 |
SF_nvar() | 整个数据集中的变量数(而不只是插件调用中的变量数) |
SF_is_missing(val) | 检查 Stata 缺失值(.) |
SV_missval | 缺失值常量 |
SF_display(msg) | 在 Stata 中打印提示信息 |
SF_error(msg) | 在 Stata 中打印红色错误信息 |
索引从 1 开始。 变量索引和观测索引都从 1 开始,而不是 0。这里的差一错误不会发出警告,却会导致程序悄无声息地读取错误变量的数据,后果严重。
内存安全
插件崩溃会导致整个 Stata 会话终止。 不会提示保存,也无法恢复。用户所有未保存的工作都会丢失。这是最重要、必须牢记的一点。
- 检查每次
malloc()/calloc()调用是否返回NULL - 在访问
argv[]前验证argc - 开发期间使用
-fsanitize=address构建 - 先用小数据测试,再逐步扩大规模
- 在
stata_call()中预先分配所有内存,并在结束时释放
stata_call() 入口点
每个插件都实现一个函数。插件也可以用 C++ 编写——入口点只需使用 extern "C" 链接,以便 Stata 能够找到它;其余部分都可以使用完整的 C++。如果有现成的 C++ 代码可供封装(例如 R 程序包的 src/ 目录),显然就适合使用 C++。当你需要复杂的数据结构,或通过 std::thread 实现线程处理时,C++ 也很有帮助。关于实用的 C++ 指南——extern "C" 模式、异常安全、编译命令和库封装——请参见 references/cpp_plugins.md。本文件其余部分将重点介绍 C,因为它是更简单的默认选择。
#include "stplugin.h"
// For C++ plugins, wrap the entry point with extern "C":
// extern "C" {
// STDLL stata_call(int argc, char *argv[]) { ... }
// }
STDLL stata_call(int argc, char *argv[]) {
// 0. Validate arguments BEFORE accessing argv[]
if (argc < 3) {
SF_error("myplugin requires 3 arguments: n_train n_test seed\n");
return 198; // Stata's "syntax error" code
}
// 1. Parse arguments (all strings — use atoi/atof)
int n_train = atoi(argv[0]);
int n_test = atoi(argv[1]);
int seed = atoi(argv[2]);
// 2. Get dimensions
ST_int nobs = SF_nobs();
// CAUTION: SF_nvar() returns ALL variables in the dataset, not just
// the ones passed to `plugin call`. If the .ado creates tempvars
// (touse, merge_id, etc.) the count will be higher than expected.
// Pass the variable count via argv instead of relying on SF_nvar().
int p = atoi(argv[3]); // safer: pass feature count explicitly
// 3. Allocate memory
double *X = calloc(nobs * p, sizeof(double));
double *y = calloc(nobs, sizeof(double));
double *pred = calloc(nobs, sizeof(double));
if (!X || !y || !pred) {
SF_error("myplugin: out of memory\n");
if (X) free(X); if (y) free(y); if (pred) free(pred);
return 909;
}
// 4. Read data from Stata (1-indexed!)
ST_double val;
for (ST_int obs = 1; obs <= nobs; obs++) {
SF_vdata(1, obs, &val); // var 1 = depvar
y[obs-1] = val;
for (int j = 0; j < p; j++) {
SF_vdata(j + 2, obs, &val); // vars 2..nvars-1 = features
X[(obs-1) * p + j] = val;
}
}
// 5. Run your algorithm
int rc = my_algorithm(X, y, pred, n_train, n_test, p, seed);
if (rc != 0) {
SF_error("myplugin: algorithm failed\n");
free(X); free(y); free(pred);
return 909;
}
// 6. Write results back to Stata
for (ST_int obs = 1; obs <= nobs; obs++) {
SF_vstore(nvars, obs, pred[obs-1]); // last var = output
}
free(X); free(y); free(pred);
return 0; // 0 = success
}
返回代码
0— 成功198— 语法错误(参数无效)909— 内存不足601— 找不到文件- 任何非零值都会触发 Stata 错误
.ado 封装模式
用户不会直接调用 plugin call。.ado 文件会提供 Stata 原生接口。
保留/合并模式
这是针对部分数据进行操作的插件所采用的核心模式:
program define mycommand, rclass
syntax varlist(min=2) [if] [in], GENerate(name) [SEED(integer 12345) REPlace]
gettoken depvar indepvars : varlist
if "`replace'" != "" {
capture drop `generate'
}
confirm new variable `generate'
// Mark sample: novarlist ALLOWS missing depvar (critical for imputation)
marksample touse, novarlist
markout `touse' `indepvars' // but DO exclude missing predictors
// Stable merge key — create BEFORE any sorting or subsetting
tempvar merge_id
quietly gen long `merge_id' = _n
// Count subsets
quietly count if `touse' & !missing(`depvar')
local n_train = r(N)
quietly count if `touse' & missing(`depvar')
local n_test = r(N)
// Create output variable (all missing initially)
quietly gen double `generate' = .
// Preserve, subset, call plugin
preserve
quietly keep if `touse'
// Sort if plugin requires it (donors first, test second)
tempvar sort_order
quietly gen `sort_order' = missing(`depvar')
quietly sort `sort_order'
// Call plugin
plugin call myplugin `depvar' `indepvars' `generate', ///
`n_train' `n_test' `seed'
// Save results and restore
tempfile results
quietly keep `merge_id' `generate'
quietly save `results'
restore
// Merge predictions back (update replaces missing with non-missing)
quietly merge 1:1 `merge_id' using `results', nogenerate update
end
**update 为什么有效:**执行 preserve 之前,generate 变量全为缺失值。restore 之后,它仍然全为缺失值。update 选项会用合并文件中的非缺失值替换缺失值。replace 选项通过 capture drop 提前处理,因此到合并时,该变量总是刚刚创建的。
插件排序约定
**重要:**有些插件要求数据按特定顺序排序(训练行在前,测试行在后)。另一些插件会在内部处理缺失数据。排序不匹配是最危险的错误之一——插件会悄悄读取错误的数据,生成垃圾输出,而且不会报错。排序顺序不匹配可能会大幅降低预测质量(例如,相关系数从 0.99 降到 0.38),因为插件会把测试观测当作训练数据,反之亦然。
- 如果插件内部检查
SF_is_missing():不要在 .ado 包装器中排序 - 如果插件要求先有连续的
n_train行,再有n_test行:调用前按missing(depvar)排序
请记录你的插件采用哪种模式。
插件加载(跨平台)
使用 gtools 风格的操作系统检测模式。该模式通过 c(os) 检测操作系统,并构造一个不带路径的文件名。Stata 的 adopath 会解析此文件名,在所有平台上都可靠。
/* ---- Load plugin (gtools-style: detect OS, bare filename) ---- */
if ( inlist("`c(os)'", "MacOSX") | strpos("`c(machine_type)'", "Mac") ) local c_os_ macosx
else local c_os_: di lower("`c(os)'")
cap program drop myplugin
program myplugin, plugin using("myplugin_`c_os_'.plugin")
根据平台,这会解析为 myplugin_macosx.plugin、myplugin_windows.plugin 或 myplugin_unix.plugin。
**警告——不要使用 findfile 和绝对路径。**以下模式在 Windows 上无法正常工作,绝不可使用:
* BROKEN — DO NOT USE
capture findfile myplugin.plugin
capture program myplugin, plugin using("`r(fn)'")
findfile 会返回绝对路径(例如 C:\ado\plus\m\myplugin.plugin)。在 Windows 上,通过 using() 传入某些绝对路径时,Stata 的 LoadLibrary 调用会失败。gtools 风格的模式通过传入不带路径的文件名避免了这个问题,Stata 会通过 adopath 解析该文件名——这正是 gtools、ftools 和其他主流软件包的工作方式。
同样,不要使用嵌套的 if/else 级联来逐一尝试每种 platform-arch 后缀。这是若干软件包曾经采用的旧模式;如果涉及 findfile,也会因同样的原因失效,而且脆弱冗长。
插件文件命名:pluginname_os.plugin,其中 os 为 macosx、unix 或 windows。示例:qrf_plugin_macosx.plugin、grf_plugin_windows.plugin。
注意:clear all 会清除已加载的插件定义。如果测试脚本以 clear all 开头,所有 program ... plugin 定义都会消失。请重新加载。
跨平台编译
为三个平台构建(ARM Mac 可通过 Rosetta 运行 x86_64,因此一个 macOS 二进制文件即可)。请先安装 Windows 交叉编译器:brew install mingw-w64。
| 目标操作系统 | 输出名称后缀 | 编译器 | -D 标志 | 链接标志 | pthreads |
|---|---|---|---|---|---|
| macOS (ARM64) | _macosx | gcc -arch arm64 | -DSYSTEM=APPLEMAC | -bundle | -pthread |
| Linux (x86_64) | _unix | gcc | -DSYSTEM=OPUNIX | -shared | -pthread |
| Windows (x86_64) | _windows | x86_64-w64-mingw32-gcc | -DSYSTEM=STWIN32 | -shared | -lwinpthread |
所有平台:发布版本使用 -O3 -fPIC,开发版本则添加 -g -fsanitize=address。
**对于 C++ 插件:**使用 g++ 代替 gcc。根据库的要求添加相应版本的 -std=c++(请查阅其文档——C++11、C++14 和 C++17 都很常见)。仅包含头文件的 C++ 库可以放入 c_source/,并通过 -I. 引入。在 Windows 和 Linux 上始终使用 -static-libstdc++ -static-libgcc。
命名约定:pluginname_os.plugin(例如 qrf_plugin_macosx.plugin、grf_plugin_windows.plugin)。os 后缀必须与 gtools 风格加载器生成的后缀一致:macosx、unix 或 windows。
macOS 注意事项:使用 -bundle,不要使用 -shared。这是一个常见错误。
从 macOS 构建 Linux 版本(需要 Docker)
macOS 上没有原生的 Linux 交叉编译器。使用 Colima 通过 Docker 构建(运行 brew install colima docker,然后运行 colima start)。使用一行命令构建:
docker run --rm --platform linux/amd64 -v "$(pwd):/build" -w /build ubuntu:18.04 \
bash -c "apt-get update -qq && apt-get install -y -qq g++ gcc make > /dev/null 2>&1 && make linux"
**glibc 兼容性:**在 Ubuntu 18.04 上构建以获得最大兼容性(仅需 GLIBC 2.14,可在大约 2012 年以来的所有 Linux 系统上运行)。在 Ubuntu 22.04 及更高版本上构建需要 GLIBC 2.34,因此不兼容 RHEL 8、Ubuntu 20.04 和许多 HPC 环境。
性能优化
请参阅 references/performance_patterns.md,其中包含详细的代码示例:
- 预排序特征索引 — 对特征值排序一次,然后在每个树节点处线性扫描。每次分割的复杂度为 O(n),而不是 O(n log n)。
- 预计算距离范数 — 对 KNN 使用 ||a-b||^2 = ||a||^2 + ||b||^2 - 2*a'b。
- 快速选择 — 使用 O(n) 部分排序查找第 k 个最近邻。
- 并行训练集成模型(pthreads) — 并发训练多个模型。每个线程都有自己的数据副本和 RNG 状态。切勿从工作线程调用 Stata SDK 函数(
SF_vdata、SF_vstore、SF_display) — 先在主线程读取所有数据,再将计算分派给工作线程,等待线程结束后在主线程写回结果。 - XorShift RNG — C 插件无法访问 Stata 的内部 RNG(
runiform())。XorShift128+ 速度快、统计性质可靠且线程安全(每个线程都有自己的状态)。从argv[]获取种子以确保结果可复现。 - 树使用稠密数组 — 使用扁平节点数组代替链表,以提高缓存局部性。
调试
由于无法将调试器附加到 Stata 的插件宿主进程上,调试很困难。
调试策略
-
通过 SF_display() 输出:
char buf[256]; snprintf(buf, sizeof(buf), "Debug: n=%d, p=%d\n", n, p); SF_display(buf); -
写入诊断文件:
FILE *f = fopen("plugin_debug.log", "w"); fprintf(f, "value at [%d][%d] = %f\n", i, j, val); fclose(f); -
先单独测试。 编写一个读取 CSV 并调用算法的
main()。使用常规工具(gdb、valgrind、sanitizers)调试,然后再适配插件接口。 -
开发期间使用 sanitizers 构建:
-g -fsanitize=address -
检查 SF_vdata() 的返回值。 它返回
RC(0=成功)。非零表示 obs/var 索引无效。
常见故障模式
| 症状 | 可能原因 |
|---|---|
| Stata 无提示地崩溃 | 段错误:缓冲区溢出、argv 访问错误、空指针解引用 |
| 插件返回的值全都缺失 | 变量数量错误、obs 索引错误、插件未加载 |
| 结果是乱码 | 排序不匹配、0 与 1 的索引错误、输入未归一化 |
| “plugin not found” | 文件名错误、clear all 清除了定义、平台错误 |
| 在 Mac 上可运行,在 Linux 上失败 | 整数大小差异;使用 <stdint.h> 中的 int32_t/int64_t |
打包和分发
使用特定于平台的 .pkg 文件,这样用户只需下载适用于其操作系统的二进制文件。Stata 的 net install 不支持条件逻辑,因此避免向每位用户分发全部 4 个二进制文件的方法是按平台提供单独的软件包。所有软件包都安装相同的 .ado 和 .sthlp 文件,只有 .plugin 二进制文件不同。
mypackage/
├── stata.toc # lists all package variants
├── mypackage.pkg # all platforms (for users who don't care)
├── mypackage_mac.pkg # macOS only
├── mypackage_linux.pkg # Linux only
├── mypackage_win.pkg # Windows only
├── mycommand.sthlp # overview help file (short name!)
├── mycommand.ado # user-facing command
├── myplugin_macosx.plugin
├── myplugin_unix.plugin
├── myplugin_windows.plugin
└── c_source/ # NOT distributed, for building
├── build.py
├── stplugin.c
├── stplugin.h
└── algorithm.c
用户安装其平台对应的软件包:
* macOS
net install mypackage_mac, from("https://raw.githubusercontent.com/user/repo/main") replace
* Linux
net install mypackage_linux, from("https://raw.githubusercontent.com/user/repo/main") replace
* Windows
net install mypackage_win, from("https://raw.githubusercontent.com/user/repo/main") replace
所有平台的二进制文件都通过全平台 .pkg 分发,用户也可以安装特定于平台的软件包。Stata 在运行时通过类似 gtools 的操作系统检测,仅加载匹配的插件。Windows C++ 二进制文件可能因静态链接而达到 10–15 MB,这属于正常情况。
请参阅 references/packaging_and_help.md,其中包含 .toc、.pkg、.sthlp 模板和 SMCL 格式说明。
常见陷阱
-
排序会破坏合并键。 如果在
preserve/restore中排序,merge_id 的关联就会断开。务必在 preserve 之前创建 merge_id。 -
所有索引都从 1 开始。
SF_vdata(var, obs, &val)— var 和 obs 都从 1 开始。索引偏移错误不会有提示。 -
marksample默认会排除缺失值。 对于插补(缺失的因变量正是要处理的对象),请使用marksample touse, novarlist。 -
macOS 的
c(os)返回 "MacOSX"。 使用 gtools 模式检测 Mac:inlist("c(os)'", "MacOSX") | strpos("c(machine_type)'", "Mac")。对于其他平台,lower(c(os))会返回"windows"或"unix"。 -
argv[] 不会进行边界检查。 当
argc == 2时访问argv[3]会导致段错误。务必先检查argc。 -
clear all会清除插件。 在测试脚本中执行clear all后,请重新加载插件定义。 -
.ado 文件中只有第一个
program define会被自动发现。 子程序需要放在各自的 .ado 文件中,或通过显式run加载。 -
当算法需要时,请对输入进行归一化(神经网络、基于梯度的方法、KNN 等基于距离的方法)。在 .ado 包装器中缩放至均值 = 0、标准差 = 1,并在预测后反归一化。插件应接收干净且已归一化的数据 — 缩放工作交由 .ado 处理。
-
Windows 上的 pthreads 需要
-lwinpthread。 请使用条件链接器标志。 -
内存错误会导致 Stata 崩溃,且无法恢复。 预先分配所有内存,检查每次分配,并在开发期间使用消毒器进行构建。
-
glibc 版本不匹配。 在较新的发行版上构建 Linux 插件,会生成无法在较旧系统上加载的二进制文件。请在 Docker 中使用 Ubuntu 18.04,以获得最大的兼容性。
-
SF_nvar()返回数据集变量总数。 它会统计数据集中的所有变量,而不仅仅是plugin call变量列表中的变量。如果 .ado 创建了临时变量(touse、merge_id、排序键),计数就会高于预期。切勿使用SF_nvar()验证参数数量 — 请通过argv传递预期数量。 -
在 Windows 上,
findfile+ 绝对路径会出问题。findfile返回的绝对路径无法被 Stata 的LoadLibrary在 Windows 上解析。请改用 gtools 风格的操作系统检测模式(见上文的 Plugin Loading 部分)— 它会构造一个不带路径的文件名,由 Stata 通过 adopath 解析。
命名约定
- 方法选择选项使用
method(),而非model() - 输出变量命名使用
generate()(缩写为gen()) - 将
replace用作标志选项,而非replace() - 插件文件:
algorithm_plugin_os.plugin,其中 os 为macosx、unix或windows - .ado 文件:使用小写字母,多词名称用下划线连接
- Stata 选项约定:选项名使用小写字母,缩写使用大写字母(
GENerate、MAXDepth) - 插件支持的目标版本为 Stata 14.0+(
version 14.0) - 帮助文件使用简短的命令名称,而非仓库名称。 如果仓库名为
mypackage_stata,概览帮助文件仍应命名为mypackage.sthlp(这样help mypackage才能正常使用)。帮助文件或命令名称中不要追加 "stata" — 用户已经在 Stata 中。