- 科研
- 科学计算
- 高性能计算
【免费下载链接】lammps
Public development project of the LAMMPS MD software package
导读
本文是 LAMMPS 开发者指南中“Writing plugins”章节的深入讲解,面向希望在不重编译、不重新链接整个可执行文件的前提下为 LAMMPS 增加新样式(pair、bond、fix、compute、command 等)的开发者。读完本文你将掌握插件 DSO 的结构约定、lammpsplugin_init初始化函数与lammpsplugin_t注册结构体的用法、pair/fix/command 三类工厂函数的编写差异,以及如何用 CMake 或 GNU make 编译插件并借助LAMMPS_PLUGIN_PATH实现启动时自动加载。
插件机制概述:为 LAMMPS 二进制动态注入新样式
插件(plugin)是 LAMMPS 提供的一种在不重新编译 LAMMPS 可执行文件的前提下添加新功能的机制。其实现位于 PLUGIN 包中(源码见 src/PLUGIN/plugin.cpp 与 src/PLUGIN/plugin.h),因此要使用插件功能,LAMMPS 必须编译安装了该包。
插件本质上是利用操作系统加载动态共享对象(DSO,Dynamic Shared Object)文件的能力:DSO 在运行时被加载进进程,其中特定的导出符号可以被解析并调用。任何包含插件的 DSO 文件都必须导出一个固定名称的初始化函数lammpsplugin_init,它遵循下文描述的特定规则。当通过plugin命令加载该 DSO 时,LAMMPS 会查找并调用这个函数,由它把 DSO 中封装的插件注册进 LAMMPS。
从实现角度看,这一机制之所以可行,源于 LAMMPS 的面向对象设计:所有 pair 样式类都继承自Pair基类,所有 fix 样式类都继承自Fix基类,其余样式(bond、angle、dihedral、improper、kspace、compute、region、command、run、min 等)同样如此,调用方通常只直接调用这些基类中存在的成员函数。当执行pair_style、fix等命令时,LAMMPS 通过一个与样式名映射的工厂函数创建派生类实例。例如处理pair_style lj/cut 2.5时,LAMMPS 会查找创建PairLJCut类的工厂函数并执行,返回值是一个Pair *指针,被赋给当前激活的 pair 样式位置。
因此,插件 DSO 的核心任务就是:实现这样一个工厂函数,并通过注册把它挂到 LAMMPS 对应样式类别的全局注册表中。加载插件后,该样式对 LAMMPS 的行为与把对应代码静态编译进二进制中的包完全一致。
安装 PLUGIN 包与 plugin 命令基础
在使用插件前,需要先编译带 PLUGIN 包的 LAMMPS,然后才能使用 plugin 命令 管理插件。其语法为:
plugin command args支持五个子命令:
| 子命令 | 作用 |
|---|---|
load file | 从指定 DSO 文件加载其中包含的所有插件;单个 DSO 可包含多个插件,已加载的插件会被跳过 |
unload style name | 卸载指定样式(pair、bond、angle、dihedral、improper、kspace、compute、fix、region、command、run、min)下指定名称的插件;若该样式当前正在使用,其实例会先被删除并替换为该类样式的默认设置 |
list | 打印当前已加载插件及其样式与名称 |
clear | 卸载所有已加载插件 |
restore | 兼容性保留命令,在新版中不再需要、不做任何事(见下文"进程全局注册表") |
典型用法示例(见 plugin 命令文档):
plugin load morse2plugin.so plugin unload pair morse2/omp plugin unload command hello plugin list plugin clear加载单个 DSO 的完整调用链可以在 src/PLUGIN/plugin.cpp 的plugin_load()中看到:platform::dlopen()打开 DSO,platform::dlsym()按名称查找lammpsplugin_init符号,随后以三个参数调用它——指向当前 LAMMPS 实例的指针、DSO 句柄、注册函数指针。
lammpsplugin_init:插件 DSO 的入口约定
所有插件 DSO 都必须实现并导出如下签名的初始化函数(C 绑定,因此没有 C++ 名称修饰):
void lammpsplugin_init(void *lmp, void *handle, void *regfunc);三个void *参数分别是:
lmp:当前调用它的 LAMMPS 实例指针,需要原样传给注册函数;handle:DSO 文件的内部句柄,必须写入插件信息结构体,以便在 DSO 中所有插件都被卸载后由 LAMMPS 关闭并释放该 DSO;regfunc:注册函数的函数指针,需要先转换为lammpsplugin_regfunc类型,再以lammpsplugin_t *结构体指针和 LAMMPS 实例指针为参数调用,完成单个插件的注册。
同一个初始化函数内部可以多次调用注册函数,从而在一个 DSO 中注册多个插件。类型定义见 src/lammpsplugin.h:
typedef void *(lammpsplugin_factory1) (void *); typedef void *(lammpsplugin_factory2) (void *, int, char **); typedef struct { const char *version; const char *style; const char *name; const char *info; const char *author; union { lammpsplugin_factory1 *v1; lammpsplugin_factory2 *v2; } creator; void *handle; } lammpsplugin_t; typedef void (*lammpsplugin_regfunc)(lammpsplugin_t *, void *); typedef void (*lammpsplugin_initfunc)(void *, void *, void *);lammpsplugin_t 结构体成员详解
注册插件时,需要填充一个lammpsplugin_t结构体,各成员含义如下(与原文档成员表一致):
| 成员 | 含义 |
|---|---|
version | 插件编译时对应的 LAMMPS 版本字符串 |
style | 插件的样式类别(pair、bond、fix、command 等) |
name | 插件样式名 |
info | 描述插件的信息字符串 |
author | 作者姓名与邮箱字符串 |
creator.v1 | 指向 pair、bond、angle、dihedral、improper、kspace、command 或 minimize 样式的工厂函数 |
creator.v2 | 指向 compute、fix、region 或 run 样式的工厂函数 |
handle | 打开的 DSO 文件句柄 |
注册时,lammpsplugin_t被放入 LAMMPS 内部维护的全局插件列表中,对应 DSO 句柄的引用计数加一(见 src/PLUGIN/plugin.cpp 中的pluginlist与dso_refcounter);卸载时引用计数归零才会真正dlclose()释放 DSO。注册函数还会把工厂函数以给定的名字加入对应样式类别的注册表。
creator.v1 与 creator.v2:两类工厂函数的区分
creator是联合体,同一时刻只能使用其中一个成员,由插件style决定:
creator.v1:适用于计算受力的样式(pair、bond、angle、dihedral、improper)、command 样式和 minimize 样式。工厂函数只接收一个参数——LAMMPS 实例指针,赋值前需将函数指针转换为lammpsplugin_factory1类型:typedef void *(lammpsplugin_factory1) (void *);creator.v2:适用于 fix、compute、region 和 run 样式。工厂函数接收三个参数——LAMMPS 实例指针、参数列表长度int、char **参数列表指针,赋值前需转换为lammpsplugin_factory2类型:typedef void *(lammpsplugin_factory2) (void *, int, char **);
这种区分与 LAMMPS 内部创建各类样式的调用方式一一对应:pair/fix 等类的构造函数签名不同,fix、compute这类命令创建实例时需要透传命令行参数。
示例一:Pair 样式插件(factory1)
原文档以假想的morse2pair 样式为例,其实现位于pair_morse2.h/pair_morse2.cpp(类名为PairMorse2)。工厂函数与初始化函数如下:
#include "lammpsplugin.h" #include "version.h" #include "pair_morse2.h" using namespace LAMMPS_NS; static Pair *morse2creator(LAMMPS *lmp) { return new PairMorse2(lmp); } extern "C" void lammpsplugin_init(void *lmp, void *handle, void *regfunc) { lammpsplugin_regfunc register_plugin = (lammpsplugin_regfunc) regfunc; lammpsplugin_t plugin; plugin.version = LAMMPS_VERSION; plugin.style = "pair"; plugin.name = "morse2"; plugin.info = "Morse2 variant pair style v1.0"; plugin.author = "Axel Kohlmeyer (akohlmey@gmail.com)"; plugin.creator.v1 = (lammpsplugin_factory1 *) &morse2creator; plugin.handle = handle; (*register_plugin)(&plugin,lmp); }要点:
- 工厂函数
morse2creator()只接收一个 LAMMPS 指针,因此必须赋给creator.v1并转换为lammpsplugin_factory1类型;返回值是Pair派生类实例的指针; - 工厂函数可以声明为
static,避免与其他插件发生符号冲突; - 派生类的类名
PairMorse2在整个 LAMMPS 可执行文件中必须唯一(插件之间不能重名注册,见下文)。
仓库中 examples/plugins/morse2plugin.cpp 给出了真实的同款示例,并且它在同一个lammpsplugin_init中连续注册了morse2与morse2/omp两个样式——注册完第一个后只更新name、info、creator.v1再调用一次注册函数,这正好演示了"单个 DSO 可注册多个插件"的用法。
示例二:Fix 样式插件(factory2)
若工厂函数针对 fix 或 compute(接收 LAMMPS 指针、参数个数与参数串列表三个参数),指针类型必须是lammpsplugin_factory2,赋给creator.v2成员:
#include "lammpsplugin.h" #include "version.h" #include "fix_nve2.h" using namespace LAMMPS_NS; static Fix *nve2creator(LAMMPS *lmp, int argc, char **argv) { return new FixNVE2(lmp,argc,argv); } extern "C" void lammpsplugin_init(void *lmp, void *handle, void *regfunc) { lammpsplugin_regfunc register_plugin = (lammpsplugin_regfunc) regfunc; lammpsplugin_t plugin; plugin.version = LAMMPS_VERSION; plugin.style = "fix"; plugin.name = "nve2"; plugin.info = "NVE2 variant fix style v1.0"; plugin.author = "Axel Kohlmeyer (akohlmey@gmail.com)"; plugin.creator.v2 = (lammpsplugin_factory2 *) &nve2creator; plugin.handle = handle; (*register_plugin)(&plugin,lmp); }示例三:Command 样式插件(factory1 + 单文件实现)
Command 样式同样使用第一类工厂函数。下面的示例还展示了插件类实现可以与插件接口代码放在同一个源文件中:
#include "lammpsplugin.h" #include "comm.h" #include "error.h" #include "command.h" #include "version.h" #include <cstring> namespace LAMMPS_NS { class Hello : public Command { public: Hello(class LAMMPS *lmp) : Command(lmp) {}; void command(int, char **); }; } using namespace LAMMPS_NS; void Hello::command(int argc, char **argv) { if (argc != 1) error->all(FLERR,"Illegal hello command"); if (comm->me == 0) utils::logmesg(lmp,fmt::format("Hello, {}!\n",argv[0])); } static void hellocreator(LAMMPS *lmp) { return new Hello(lmp); } extern "C" void lammpsplugin_init(void *lmp, void *handle, void *regfunc) { lammpsplugin_t plugin; lammpsplugin_regfunc register_plugin = (lammpsplugin_regfunc) regfunc; plugin.version = LAMMPS_VERSION; plugin.style = "command"; plugin.name = "hello"; plugin.info = "Hello world command v1.1"; plugin.author = "Axel Kohlmeyer (akohlmey@gmail.com)"; plugin.creator.v1 = (lammpsplugin_factory1 *) &hellocreator; plugin.handle = handle; (*register_plugin)(&plugin,lmp); }加载该插件后,即可在输入脚本中直接使用hello <name>命令。真实实现见 examples/plugins/helloplugin.cpp。
附加细节:注册规则、样式覆盖与多插件注册
原文档强调了几条必须遵守的规则:
- 初始化函数名必须叫
lammpsplugin_init,必须有 C 绑定,且只接收三个void *参数; - 第一个参数(LAMMPS 实例指针)必须传给注册函数;第二个参数(DSO 句柄)必须写入插件结构体,供引用计数与卸载使用;第三个参数(注册函数指针)必须先存入
lammpsplugin_regfunc类型变量再调用; - 同一个初始化函数可以多次调用注册函数,注册多个插件;
- 插件 DSO 中的样式类本身(如
PairMorse2)可以像普通 LAMMPS 内置样式一样编写,只是不再需要#ifdef PAIR_CLASS保护的PairStyle宏——类名到样式名的映射由插件注册函数根据lammpsplugin_t中的信息完成。当然,如果希望新代码将来并入 LAMMPS 主分支,仍然可以保留该宏; - 插件可以用已有样式名注册,此时插件会覆盖已有代码。这可以用来修改既有样式的行为,或在不必重编译、重装整个 LAMMPS 的情况下调试新版本样式。在 src/PLUGIN/plugin.cpp 的
plugin_register()中可以看到,覆盖内置样式时会在根进程打印Overriding built-in ... style ... from plugin警告。
进程全局注册表:插件生命周期与版本行为变化
关于插件生命周期,原文档记录了两个重要版本变化:
- 12Jun2025 起:执行
clear命令时,插件不再被卸载; - 2Sep2026 起:样式工厂函数从"每个 LAMMPS 实例独立的样式映射"迁移为进程全局注册表。其直接后果是:
- 已加载插件跨越
clear命令保持有效,无需重新加载; - 同一进程中任何 LAMMPS 实例(包括通过 C 库接口、Python 或 Fortran 模块创建的多实例)加载的插件,对所有其他实例立即可用,不再需要显式的 restore 步骤(
plugin restore命令仅为向后兼容保留、什么都不做); plugin load可以重复发出,但已加载的插件会被跳过(src/PLUGIN/plugin.cpp 的plugin_register()中通过plugin_find()检查并打印 "Ignoring load of ... must unload existing ... plugin first");替换插件必须先显式plugin unload;- 插件只会在被显式卸载、或 LAMMPS 库接口被 finalize(如 C 库接口中的
lammps_plugin_finalize(),见 unittest/c-library/test_main.cpp)时移除; - 多个实例并发的插件加载/卸载操作在内部通过互斥锁(
plugin_mutex)串行化,保证共享的插件注册表不会被破坏; info styles命令(见 info 命令文档)和-help命令行输出会对当前由插件提供的样式名附加一个尾部星号*标记。
- 已加载插件跨越
编译插件:环境一致性与构建方式
插件必须使用与 LAMMPS 可执行文件和库相同的编译器、相同的库(如 MPI)和相同的编译设置(MPI 开关、OpenMP、整数位宽等)来编译。否则插件很可能无法加载——LAMMPS 是 C++,函数签名的作用域、类型和参数个数都被编码进符号名(name mangling),任何不一致都会导致plugin load失败。对此 plugin 命令文档 还提醒:插件依赖 LAMMPS 的二进制接口(ABI),尤其是 MPI 库,用不同 MPI 库或不同编译设置编译出的插件不能保证工作,且 LAMMPS 不做一致性检查,不匹配时可能加载失败,甚至可能造成数据损坏或崩溃。
插件的编译有两种方式:
方式一:CMake
examples/plugins/CMakeLists.txt 是一份可以直接作为模板使用的 CMake 构建脚本,其中几个值得注意的技术点:
- 要求 CMake 3.20+ 与 C++17 标准(与当前 LAMMPS 的 C++ 标准一致);
- 独立构建时通过
LAMMPS_HEADER_DIR指定 LAMMPS 头文件目录,默认安装前缀设为$HOME/.local(避免需要 root 权限); - 每个插件用
add_library(... MODULE ...)声明,链接到 LAMMPS 库,并通过set_target_properties(... PROPERTIES PREFIX "" SUFFIX ".so")去掉 CMake 默认的前缀/后缀,使产物形如helloplugin.so; - 平台差异处理:macOS 使用
-Wl,-undefined,dynamic_lookup,Windows 使用WINDOWS_EXPORT_ALL_SYMBOLS导出 DLL 符号,Linux/BSD 等 ELF 平台使用-rdynamic; - 当在静态 LAMMPS 库构建中内嵌编译插件时,需要 CMake 3.27+ 并使用
$<COMPILE_ONLY:lammps>生成器表达式只导入编译期设置,避免把整份 LAMMPS 副本链接进插件导致符号二次构造/析构、进程退出时崩溃(Windows 例外:DLL 不允许未定义符号,必须完整链接库)。
方式二:GNU make
examples/plugins/Makefile 展示了传统的 make 方式:CXX=mpicxx,编译标志包含-fPIC(生成位置无关代码,DSO 必需),链接时用-shared -rdynamic生成.so,具体目标规则在 Makefile.common 中。-fopenmp与 OPENMP 头文件路径是为了编译morse2/omp这类 OpenMP 变体样式。
更复杂的插件实例:从既有包构建插件
原文档给出了两个"把整个包变成插件"的进阶实例:
- examples/kim/plugin(KIM 包插件):把 KIM 包转换为插件,KIM 包源码本身无需任何改动,只需额外添加插件接口与加载器代码。该示例仅支持 CMake 构建,需要以
-DLAMMPS_SOURCE_DIR=<path/to/lammps/src/folder>运行 CMake,其余配置与编译 LAMMPS 相同; - examples/PACKAGES/pace/plugin(ML-PACE 包插件):从 ML-PACE 包创建插件。主体代码位于先下载并编译的静态外部库中,再与 pair 样式包装器和插件加载器组合。由于外部库与 LAMMPS 的许可证在分发二进制时存在冲突(ML-PACE 不能静态链接),而构建插件所需的 LAMMPS 头文件以更宽松的许可证提供,因此该示例还包含一个 NSIS 脚本用于生成 Windows 安装包,安装时会自动设置所需环境变量,之后启动兼容的 LAMMPS 二进制即可自动加载并注册该插件,ML-PACE 包用法与直接静态链接进 LAMMPS 时完全一致。
启动时自动加载:LAMMPS_PLUGIN_PATH
设置环境变量LAMMPS_PLUGIN_PATH后,LAMMPS 会在启动时自动搜索该路径所列目录(可多个,路径分隔符与系统 PATH 一致)中所有以plugin.so结尾的文件(例如helloplugin.so),并尝试自动加载其中包含的插件。实现位于 src/PLUGIN/plugin.cpp 的plugin_auto_load():它遍历目录、用正则匹配\plugin.so$文件名并逐个plugin_load()。按此方式加载的插件,其行为与对应代码被静态编译进二进制完全一致。
测试与验证
仓库的单测覆盖了插件的主要行为,可作为开发插件时的验证参考。在 unittest/commands/test_simple_commands.cpp 中可以看到:
- 加载
helloplugin.so后检查日志中出现 "Loading plugin: Hello world command"; - 加载
nve2plugin.so后检查 "NVE2 variant fix style"; plugin list输出1: command style plugin hello、2: fix style plugin nve2等条目;- 重复加载同一插件会被拒绝并提示必须先卸载已有插件;
- 卸载不存在对应插件的样式(如
plugin unload pair nve2)会提示 "Ignoring unload ... not from a plugin"。
此外 unittest/c-library/CMakeLists.txt 展示了把 examples/COUPLE/plugin 目录作为插件构建并与 C 库接口联调的测试组织方式,而静态库构建场景下插件必须在不嵌入 LAMMPS 副本的前提下构建(见 unittest/commands/CMakeLists.txt 中的说明)。
小结
LAMMPS 的插件机制把"新增样式"从"重编译整个 LAMMPS"中解放出来:只需按约定编写实现类 + 工厂函数 +lammpsplugin_init注册函数,编译成*plugin.soDSO,即可在任意兼容的 LAMMPS 二进制中通过plugin load或LAMMPS_PLUGIN_PATH自动加载使用。编写时的核心要点可归纳为:工厂函数签名必须与样式类别匹配(force/command/min 用 factory1,fix/compute/region/run 用 factory2)、初始化函数必须为 C 绑定、编译环境必须与宿主 LAMMPS 一致、样式名在进程内全局唯一。以 examples/plugins 目录中的六个插件为模板(pair、fix、command、zero 系列、kspace、run/min),再参考 KIM 与 ML-PACE 的包级插件实例,即可快速上手自己的插件开发。
- 科研
- 科学计算
- 高性能计算
【免费下载链接】lammps
Public development project of the LAMMPS MD software package
相关推荐
Ponytail全面概览:一个让AI平均少写54%代码的免费开源神器
Ponytail全面概览:一个让AI平均少写54%代码的免费开源神器 🐴 Ponytail 是一款免费开源的 AI Agent 技能包 ,它让 Claude
人工智能AI 技能AI 插件提示工程AI 评测ASP.NET Boilerplate 插件系统开发指南:动态加载与功能扩展终极教程
ASP.NET Boilerplate 插件系统开发指南:动态加载与功能扩展终极教程 ASP.NET Boilerplate 是一个强大的企业级应用程序框架,提
后端Web框架依赖注入认证鉴权如何在5分钟内快速上手MAVLink:新手入门完整教程
如何在5分钟内快速上手MAVLink:新手入门完整教程 MAVLink(Micro Air Vehicle Message Marshalling Librar
通信序列化嵌入式
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考