mcpField
S
stata-c-plugins

stata-c-plugins

github.com/dylantmoore开发29569 次安装31 周前更新
代码仓库

关于 stata-c-plugins

面向开发者的 Stata C/C++ 插件开发技能,适用于构建或迁移计算功能。内容涵盖 SDK、安全、测试及从 macOS 开发环境进行跨平台打包。

主要功能

  1. 01Stata 插件 SDK 指引
  2. 02C 和 C++ 实现模式
  3. 03内存安全与测试建议
  4. 04跨平台编译和打包
  5. 05Stata .ado 封装模式

适用场景

  1. 01基于现有 C++ 库构建 Stata 插件
  2. 02将统计软件包功能迁移到 Stata
  3. 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.md
packaging_and_help.md
performance_patterns.md
testing_strategy.md
translation_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 部分。

如何处理每项任务

编写任何代码之前,先进入计划模式。 一份好的计划应涵盖:

  1. 完整清单 — 要构建的每项功能、选项和组件(对于翻译:源软件包 API 的完整目录)
  2. 架构决策 — 封装 C++ 后端、从头编写 C,还是仅使用 Stata
  3. 相关参考文件 — 提前确定本技能的哪些参考文件包含所需信息,并在计划步骤中明确引用,以便在恰当时机加载:
    • 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"、异常安全、编译
  4. 按阶段划分的步骤,以及步骤之间的依赖关系
  5. 每个步骤: 构建内容、编写的测试,以及继续之前运行审查流程
  6. 对于翻译项目: 将最终保真度审查作为最后一步(参见 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)_macosxgcc -arch arm64-DSYSTEM=APPLEMAC-bundle-pthread
Linux (x86_64)_unixgcc-DSYSTEM=OPUNIX-shared-pthread
Windows (x86_64)_windowsx86_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,其中包含详细的代码示例:

  1. 预排序特征索引 — 对特征值排序一次,然后在每个树节点处线性扫描。每次分割的复杂度为 O(n),而不是 O(n log n)。
  2. 预计算距离范数 — 对 KNN 使用 ||a-b||^2 = ||a||^2 + ||b||^2 - 2*a'b。
  3. 快速选择 — 使用 O(n) 部分排序查找第 k 个最近邻。
  4. 并行训练集成模型(pthreads) — 并发训练多个模型。每个线程都有自己的数据副本和 RNG 状态。切勿从工作线程调用 Stata SDK 函数(SF_vdata、SF_vstore、SF_display) — 先在主线程读取所有数据,再将计算分派给工作线程,等待线程结束后在主线程写回结果。
  5. XorShift RNG — C 插件无法访问 Stata 的内部 RNG(runiform())。XorShift128+ 速度快、统计性质可靠且线程安全(每个线程都有自己的状态)。从 argv[] 获取种子以确保结果可复现。
  6. 树使用稠密数组 — 使用扁平节点数组代替链表,以提高缓存局部性。

调试

由于无法将调试器附加到 Stata 的插件宿主进程上,调试很困难。

调试策略
  1. 通过 SF_display() 输出:

    char buf[256];
    snprintf(buf, sizeof(buf), "Debug: n=%d, p=%d\n", n, p);
    SF_display(buf);
    
  2. 写入诊断文件:

    FILE *f = fopen("plugin_debug.log", "w");
    fprintf(f, "value at [%d][%d] = %f\n", i, j, val);
    fclose(f);
    
  3. 先单独测试。 编写一个读取 CSV 并调用算法的 main()。使用常规工具(gdb、valgrind、sanitizers)调试,然后再适配插件接口。

  4. 开发期间使用 sanitizers 构建: -g -fsanitize=address

  5. 检查 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 格式说明。

常见陷阱

  1. 排序会破坏合并键。 如果在 preserve/restore 中排序,merge_id 的关联就会断开。务必在 preserve 之前创建 merge_id。

  2. 所有索引都从 1 开始。 SF_vdata(var, obs, &val) — var 和 obs 都从 1 开始。索引偏移错误不会有提示。

  3. marksample 默认会排除缺失值。 对于插补(缺失的因变量正是要处理的对象),请使用 marksample touse, novarlist。

  4. macOS 的 c(os) 返回 "MacOSX"。 使用 gtools 模式检测 Mac:inlist("c(os)'", "MacOSX") | strpos("c(machine_type)'", "Mac")。对于其他平台,lower(c(os)) 会返回 "windows" 或 "unix"。

  5. argv[] 不会进行边界检查。 当 argc == 2 时访问 argv[3] 会导致段错误。务必先检查 argc。

  6. clear all 会清除插件。 在测试脚本中执行 clear all 后,请重新加载插件定义。

  7. .ado 文件中只有第一个 program define 会被自动发现。 子程序需要放在各自的 .ado 文件中,或通过显式 run 加载。

  8. 当算法需要时,请对输入进行归一化(神经网络、基于梯度的方法、KNN 等基于距离的方法)。在 .ado 包装器中缩放至均值 = 0、标准差 = 1,并在预测后反归一化。插件应接收干净且已归一化的数据 — 缩放工作交由 .ado 处理。

  9. Windows 上的 pthreads 需要 -lwinpthread。 请使用条件链接器标志。

  10. 内存错误会导致 Stata 崩溃,且无法恢复。 预先分配所有内存,检查每次分配,并在开发期间使用消毒器进行构建。

  11. glibc 版本不匹配。 在较新的发行版上构建 Linux 插件,会生成无法在较旧系统上加载的二进制文件。请在 Docker 中使用 Ubuntu 18.04,以获得最大的兼容性。

  12. SF_nvar() 返回数据集变量总数。 它会统计数据集中的所有变量,而不仅仅是 plugin call 变量列表中的变量。如果 .ado 创建了临时变量(touse、merge_id、排序键),计数就会高于预期。切勿使用 SF_nvar() 验证参数数量 — 请通过 argv 传递预期数量。

  13. 在 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 中。

评论

支持 Markdown