简介:面向股票软件二次开发与公式接口扩展的开发者,这份大智慧新一代C语言接口模板用于在VC环境中生成WIN32动态链接库,为外部扩展函数提供可编译的基础骨架。压缩包共6个文件,体积约6KB,包含2个cpp源文件与2个h头文件,分别承载扩展函数实现、接口声明及预编译头配置,另有dsw工作区与dsp工程文件,便于直接导入旧版Visual C++查看项目结构和编译参数。已有1892人学习下载,说明该接口骨架在同类资料中具有一定参考价值。读者可据此理解DLL导出函数与公式调用之间的衔接方式,参考工程配置、源文件组织与头文件声明,快速搭建自己的扩展函数项目,减少从零配置WIN32工程和排查编译链接问题的时间,适合需要对接大智慧公式接口的初中级开发者。
1. 公式语言算不动的时候,C 接口就该上场了
行情软件自带的公式语言写个 MA、MACD 很顺手,拖个指标就能画。上个月我遇到一个需求,要在一批标的上下跑滑窗波动率加权,还得带递归滤波,内置解释器跑到几千根 K 线就肉眼可见地卡,改参数重算一遍要等好几秒。这种时候就得把重活从脚本层挪到编译层——公式接口的价值就在这儿:行情终端负责取数、绘制、调度,具体算法用 C/C++ 写成动态库,注册进公式列表,界面上看起来还是一个普通指标。
这份资源是一套面向行情分析终端的 C 语言接口定义和配套示例,适合两类人:一类是写指标写到手不够用、想上原生代码做过复杂计算的量化开发;另一类是接手了历史遗留指标的维护岗,需要看懂那些 dll 到底跟宿主怎么交互。往下我把接口怎么摆、dll 怎么编、数据怎么进怎么出、哪里会翻车,按我拆包的顺序说一遍。
2. 接口全景:头文件、导出约定和数据视图的三件套
拿到这套接口包,别急着写代码,先把目录翻一遍,找出三个东西:头文件、示例工程、宿主侧的加载说明。接口本身其实很薄,核心就三件事——一个不透明句柄、一组只读数据视图、几个约定好的导出函数。真正折磨人的不是函数个数,是这些函数跑在什么上下文里。
2.1 头文件里到底有什么:句柄、视图和回调
宿主和插件之间最好保持"最小知情"原则。宿主不告诉你它的内部结构体长什么样,只给一个fl_context*这样的不透明指针,你原样传回去就行;数据也不让你直接持有,而是给一段连续的double数组,你只读,生命周期由宿主控制,这批算完就回收。
下面是我按这类接口常见形态整理的一份头文件骨架,函数名和字段名是示例,实际以包里那份*.h为准,但结构基本是这一套:
/* formulalink_export.h —— 宿主与插件之间的契约,只放 ABI 相关内容 */ #ifndef FORMULALINK_EXPORT_H #define FORMULALINK_EXPORT_H #ifdef __cplusplus extern "C" { /* C++ 编译器也要按 C 的方式生成符号名 */ #endif #if defined(_WIN32) # define FL_API __declspec(dllexport) # define FL_CALL __stdcall #else # define FL_API __attribute__((visibility("default"))) # define FL_CALL #endif /* 上下文:宿主内部结构,插件永远不要解引用它的成员 */ typedef struct fl_context fl_context; /* K 线视图:宿主填好,插件只读。数组长度均为 count */ typedef struct { const double* open; const double* high; const double* low; const double* close; const double* volume; int count; /* 本批要算多少根 */ int start; /* 本批第一根在整个序列里的下标,增量计算靠它 */ } fl_series; /* 日志回调:level 0=info 1=warn 2=error,msg 由插件传入 */ typedef int (FL_CALL *fl_log_fn)(int level, const char* msg); /* 三个必导出的入口 */ FL_API int FL_CALL fl_plugin_init(fl_context* ctx, fl_log_fn logger); FL_API int FL_CALL fl_plugin_calc(fl_context* ctx, const fl_series* in, double* out); FL_API void FL_CALL fl_plugin_release(fl_context* ctx); #ifdef __cplusplus } #endif #endif这三个函数的角色分工很明确。fl_plugin_init在加载时调一次,用来拿日志回调、清空状态、做一次参数校验;fl_plugin_calc是热路径,可能每根新 K 线进来就被调一次,必须够快;fl_plugin_release在卸载或切换公式时调,负责把 malloc 出来的东西还回去。你在 init 里申请、在 release 里释放,这条对应关系一旦打破,宿主反复切公式就会缓慢吃内存。
有个细节值得多说一句:extern "C"不是可选项。C++ 编译器默认会对函数名做名字修饰(mangling),fl_plugin_calc编出来可能变成_Z14fl_plugin_calcP10fl_contextPK9fl_seriesPd这种,宿主用GetProcAddress按纯文本名字去找,必然找不到。很多人第一次加载失败就是栽在这,日志里只写"入口点缺失",看半天代码没毛病。
2.2 导出约定:__stdcall、extern "C" 和返回码
32 位 Windows 下调用约定不是小事。__cdecl由调用方清栈,__stdcall由被调方清栈,两边不一致的直接后果是栈指针错位——轻则返回后宿主读到垃圾值,重则整个进程当场退出,而且崩的地方往往已经跑到宿主代码里去了,堆栈看着跟你的 dll 毫无关系,非常像玄学问题。
所以头文件里我把FL_CALL定义成__stdcall,导出函数声明和实现必须保持一致。工程里最稳妥的做法是不要手写这个宏,直接包含头文件,让声明带着宏走。我在示例工程里见过有人图省事,.c文件里手抄了一遍函数签名,结果抄漏了FL_CALL,x64 下一切正常(x64 只有一种调用约定,宏是空的),x86 下直接崩,测试环境是 64 位就完全发现不了。
返回值统一用int:0 表示成功,负数表示失败,宿主拿到负数就跳过这次绘制并记一条日志。别用bool,别用指针当返回值,更不要把 C++ 异常抛出去。
提示:报错信息别只写
return -1;,先把原因通过日志回调写出去再返回。宿主那边的错误提示通常只有一行"公式计算失败",指望它定位问题不现实。
2.3 为什么是"数据视图"而不是"数据拷贝"
新手容易问:为什么不干脆把整段 K 线结构体数组传给我,我想怎么改怎么改?因为拷贝有成本。一只标的几千根 K 线,五个字段就是几万个 double,每算一次 memcpy 一遍,加上宿主要同时算几十个公式,这个开销就不可忽略了。视图方案下插件拿到的是指针,读写零拷贝,代价是你必须遵守两条纪律:不改写、不持有。
不改写好理解,const double*编译器会帮你拦。不持有是更隐蔽的一条——有人图省事,在 init 里把in->close存到全局变量,想着下次计算还能用。结果宿主换了个周期的数据源,那块内存在算完这批之后就被复用了,你手里是个悬空指针,读出来的数字看着像行情又完全对不上,这种 bug 排查起来极其费劲。要跨批次保留数据,自己 malloc 一份拷出来。
start和count这两个字段是这套接口里最容易被误读的。count是本批数量,start是这批在整个序列中的起点。如果宿主做增量计算,第二批进来时start可能等于上一批的count,out数组只对应新那几根。你要是每次都从out[0]开始写到count,而宿主按start偏移去取结果,画面上就会出现指标整体左移的错位。第 4 章我会专门拆这段。
3. 从零跑通:编一个 DLL 挂进公式列表
接口看懂了,接下来就是让它真的被加载。这一步的坑集中在编译配置上,代码本身反而简单。我按"先出最小可加载的壳,再往里填算法"的顺序走,别一上来就写完整指标。
3.1 编译配置:位数、运行库、输出目录
第一位的是位数。宿主要是 32 位进程,你的 dll 必须是 32 位,反之亦然。64 位进程加载 32 位 dll 会直接报"不是有效的 Win32 应用程序",这个错误信息挺误导人的,其实跟程序有效无效没关系,就是位数不匹配。判断方法很简单:任务管理器里看宿主进程有没有带(32 位)后缀,或者用dumpbin /headers看 dll 的 machine 字段。
MinGW 下的编译命令大致是这样:
# 生成 32 位 dll;--kill-at 去掉 __stdcall 给符号加的 @N 后缀 gcc -shared -m32 -O2 -Wall -Wextra \ -o formula_ma.dll formula_ma.c \ -Wl,--kill-at # 检查导出的符号名对不对,必须是裸露的 fl_plugin_calc objdump -p formula_ma.dll | grep -A 20 "Export Address Table"--kill-at这个参数是血泪经验。__stdcall在 32 位下会把函数名修饰成_fl_plugin_calc@12,而宿主是拿"fl_plugin_calc"这个字符串去找的,对不上。MSVC 的__declspec(dllexport)走的是另一套修饰规则,通常需要用.def文件显式指定导出名,或者在工程设置里关掉名称修饰。用 MSVC 的话大致是:
:: /LD 出 dll,/MT 静态链接运行库,避免目标机缺 vcruntime cl /nologo /LD /O2 /MT /W3 formula_ma.c /Fe:formula_ma.dll /link /DEF:formulalink.def运行库这一项也别忽略。用/MD动态链接 CRT,在没装对应版本运行库的机器上加载会失败,报错还是那种"模块找不到"的模糊提示。指标 dll 这种分发场景,我一般直接/MT静态链进去,文件大几百 KB 无所谓,省掉一类环境问题。用 MinGW 的话默认就是静态的,不用额外处理。
输出目录也要注意。不少宿主会扫描固定目录(比如安装目录下的formula或plugin子目录),你把 dll 编到Debug/里,然后去界面上找半天找不到公式,其实根本没被扫到。编完先确认 dll 落在了宿主认的路径下,再谈加载。
3.2 第一次加载:宿主怎么找到你的函数
宿主的加载流程通常是:遍历目录 →LoadLibrary→GetProcAddress拿fl_plugin_init→ 调用 → 拿fl_plugin_calc→ 登记成一个可用公式。所以第一次跑通,最省事的验证方式是先只导出 init,让它打一行日志出来。
#include <string.h> #include "formulalink_export.h" static fl_log_fn g_log = 0; FL_API int FL_CALL fl_plugin_init(fl_context* ctx, fl_log_fn logger) { (void)ctx; g_log = logger; if (g_log) { g_log(0, "formulalink: init reached"); /* 能看到这行,说明加载链通了 */ } return 0; } FL_API int FL_CALL fl_plugin_calc(fl_context* ctx, const fl_series* in, double* out) { int i; (void)ctx; if (!in || !out || in->count <= 0) return -1; for (i = 0; i < in->count; ++i) out[i] = 0.0; /* 先输出一条零线占位 */ return 0; } FL_API void FL_CALL fl_plugin_release(fl_context* ctx) { (void)ctx; g_log = 0; }这段代码唯一的目的就是"证明链路通"。它输出一条平的零线,界面上难看,但只要公式列表里出现了你的指标、计算不报错,就说明三件事成立:dll 被扫到了、符号名对得上、调用约定没错。后面所有问题都可以在这三条之外找。
如果这一版就加载不上,别改代码,按这个顺序查:宿主进程位数 →objdump或 Dependency Walker 看导出表里有没有裸名函数 → 用LoadLibrary单独写个十几行的测试 exe 试着加载一次,把GetLastError打出来。最后这一招最直接,宿主不给错误码,测试 exe 会给。
3.3 最小可验证结果:先让一根均线画出来
零线能画出来之后,换成真算法。拿最简单的均线做第一次对照,因为它有唯一正确答案,肉眼就能判断对错。
/* 计算第 i 根 K 线上、周期为 period 的简单均线 */ static void ma_at(const fl_series* s, int i, int period, double* dst) { int j; double sum = 0.0; if (i < period - 1) { /* 前面不足一个周期,给 0,宿主会跳过不画 */ dst[i] = 0.0; return; } for (j = i - period + 1; j <= i; ++j) { sum += s->close[j]; } dst[i] = sum / (double)period; } FL_API int FL_CALL fl_plugin_calc(fl_context* ctx, const fl_series* in, double* out) { int i; (void)ctx; if (!in || !out || !in->close || in->count <= 0) return -1; for (i = 0; i < in->count; ++i) { ma_at(in, i, 20, out); } return 0; }ma_at里那个i < period - 1的判断是必须的。指标序列的前 19 根没有足够的样本,此时输出 0 或者干脆不写,宿主的绘图器会把 0 当作无效值跳过,曲线就从第 20 根开始。如果你贪图省事,对每个下标都老老实实求平均(不够就除以实际个数),画出来的线前面会有一个奇怪的"斜坡",看起来像是指标本身的性质,实际是你把边界条件处理错了。
这一版跑通后,把它和宿主自带的同名均线叠在一张图上。两条线严丝合缝,说明数据视图、索引对齐、增量逻辑全对;如果差了几根或者整体平移,问题基本就锁定在start的用法上,直接跳到第 4 章。
4. 核心链路拆解:一次计算请求在插件里的完整走位
到这一步接口能加载、能出图了,接下来才是真正决定这套东西能不能用在生产的地方:一次calc调用从进入到返回,数据在插件内部到底怎么流动。增量计算、状态管理、错误处理这三块,任何一块想当然,指标在长时间运行下都会出问题。
4.1 增量计算:start 和 count 到底谁说了算
先看宿主的两类调用模式。全量模式下,宿主一次性给你start=0、count=全长,out数组长度也是全长;增量模式下,实盘每来一根新 K 线,宿主只把新那根给你,start=已算过的根数、count=1,out只有一格。
如果你的算法是纯逐根计算的(比如简单均线,第 i 根只依赖第 i 根往前 period 根),这两种模式对你没区别,因为你的输出永远是从out[0]开始写count个。但如果你用了滑动窗口复用、或者要引用"上一根的结果"(EMA、RSI 这类递归指标),就必须知道当前这批在全局序列里的位置。
/* 带状态的 EMA:中间态跨批次保留,增量模式下不能从头算 */ typedef struct { double ema; int filled; /* 已处理过的样本数 */ int seen; /* 已推进到全局第几根,对应 start + count */ } ema_state; static void ema_step(ema_state* st, const double* close, int n, int period, double* out) { double k = 2.0 / (period + 1.0); int i; for (i = 0; i < n; ++i) { if (!st->filled) { st->ema = close[i]; /* 首根用收盘价初始化,避免前段蹿升 */ st->filled = 1; } else { st->ema = close[i] * k + st->ema * (1.0 - k); } out[i] = st->ema; } st->seen += n; }关键在st->seen。每次调用结束把它加上n,下次调用时你就会知道自己的状态对应到全局的第几根。有些接口会在fl_series之外再给一个total字段告诉你总长度,有的话直接用;没有的话就自己维护seen,但要记得在fl_plugin_init里清零,否则切一次公式就带着上个公式的尾巴。
还有一个容易踩的点:宿主可能做"回补"。历史数据补下来之后,它会从某个较早的下标开始重算一段,此时start会往回退。纯增量状态在这种场景下就崩了——你的 EMA 已经走到第 500 根,人家要你从第 300 根重算,状态没法回滚。稳妥做法是在calc开头判断in->start < st->seen,一旦发现回补就整体重置状态,从这批的第一根重新初始化。性能损失一点,正确性有保证。
4.2 状态管理:把全局变量收进上下文
示例代码里图省事用了static全局变量,这在只有一个公式实例时没问题,但宿主很可能同时算多只标的、多个周期的同一个公式。这时候全局变量就是灾难现场:A 标的的 EMA 状态被 B 标的覆盖,两条曲线互相串味,画出来的东西既不像 A 也不像 B。
接口给了fl_context*就是干这个的。但注意,它是不透明指针,你没法往里面塞自己的字段。所以常规做法是在插件内部维护一张"上下文 → 私有状态"的映射表,用ctx指针做 key。
#include <stdlib.h> typedef struct node { fl_context* key; ema_state state; /* 每个上下文一份状态 */ struct node* next; } node; static node* g_table = 0; static ema_state* state_for(fl_context* ctx) { node* p; for (p = g_table; p; p = p->next) { if (p->key == ctx) return &p->state; } /* 没找到就新建一条,init 与 calc 之间可能被多次调用 */ p = (node*)calloc(1, sizeof(node)); if (!p) return 0; p->key = ctx; p->next = g_table; g_table = p; return &p->state; } static void table_release(void) { node* p = g_table; while (p) { node* nx = p->next; free(p); p = nx; } g_table = 0; }链表在小规模下够用,上下文数量一般不会超过几十个。如果要更稳,可以换成固定大小的开放寻址哈希表,避免malloc失败后返回空指针这种处理分支。但别在这里用 C++ 的std::map加锁——calc是热路径,一把互斥锁能把性能吃掉一大半,而且宿主是否多线程调用这个函数,接口文档里通常写得很含糊,按"可能会"设计最省心。
calloc出来的节点要保证在fl_plugin_release里全部释放。注意宿主可能只调release不调init(比如加载失败后的清理),所以release里对空表也要能安全执行。
4.3 出错时怎么写:日志、返回码和降级值
热路径上的错误处理要克制。别在每个数组下标上都写assert,也别用printf——控制台输出在 GUI 宿主里压根看不见,还会拖慢速度。接口给的日志回调就是干这个的,但它是同步调用,写多了同样影响性能,所以我一般只在初始化、状态切换、以及连续失败超过阈值时打日志。
FL_API int FL_CALL fl_plugin_calc(fl_context* ctx, const fl_series* in, double* out) { ema_state* st; int i; if (!in || !out) return -1; /* 参数非法 */ if (!in->close || in->count <= 0) return -2; /* 本批没有数据 */ if (in->start < 0) return -3; /* start 异常 */ st = state_for(ctx); if (!st) { if (g_log) g_log(2, "formulalink: state alloc failed"); return -4; /* 内存不足 */ } /* 检测回补:宿主从更早的位置重算,状态必须重置 */ if (in->start < st->seen && st->seen > 0) { st->filled = 0; st->seen = in->start; if (g_log) g_log(1, "formulalink: backfill detected, state reset"); } ema_step(st, in->close, in->count, 20, out); (void)i; return 0; }返回负数的时候,out里最好已经填过一遍"安全值"(比如全部填 0),而不是留着上一次调用的残值。宿主拿到负数只会跳过绘制,但如果它内部还缓存了这块 buffer,残值可能被当成有效数据显示出来,那就成了看起来正常但数字莫名其妙的脏数据。
异常这块必须单独强调:fl_plugin_calc是从宿主代码调进来的,C++ 的throw一旦穿过 dll 边界,行为是未定义的。宿主可能是用另一种编译器、另一套 CRT 编的,栈展开机制不兼容,结果通常是进程直接没。所以整套接口的实现文件建议一律当 C 写,.c后缀,编译器不给你生成异常表,想抛也抛不出去。非要用 C++ 的特性(比如 STL 容器),就在calc外面包一层try/catch(...),把一切异常就地变成返回码。
5. 避坑与排查:DLL 加载失败和结果错位的五种常见坑
这套接口的问题有个共同特征:症状出现在宿主侧,根因在插件侧。所以排查顺序永远是先证明"我的 dll 自己能跑",再去怀疑宿主。下面五条是我在实际项目里反复遇到的,按出现频率排。
5.1 坑一:加载报"找不到入口点"
现象是宿主日志里一行"加载失败",或者公式列表里干脆看不到你的指标,用 Dependency Walker 打开 dll,导出表里有一堆名字,但没有一个是宿主找的那个。
原因基本就两种:一是 C++ 名字修饰没关,导出的是_Z14fl_plugin_calc...这种;二是__stdcall在 32 位下加了@12后缀,跟宿主按纯名查找对不上。还有一种小概率情况,编译时把函数声明成了static,__declspec(dllexport)直接被忽略,符号压根没导出去。
解决方式是先看导出表。用objdump -p xxx.dll(MinGW)或者dumpbin /exports xxx.dll(MSVC)确认导出名是裸露的fl_plugin_init。名字不对就回去检查头文件有没有被正确包含、extern "C"有没有生效、链接选项里有没有.def文件。MSVC 下最省事的做法是加一个.def:
LIBRARY formula_ma EXPORTS fl_plugin_init fl_plugin_calc fl_plugin_release这样不管什么调用约定、什么编译器,导出的名字就是你要的那三个。
5.2 坑二:x64 正常,x86 一加载就崩
这个特别容易误判成"逻辑有 bug",因为在开发机上跑得好好的。换到 32 位宿主,宿主一调calc进程就没了,或者返回后宿主读到一条完全错误的曲线。
原因还是调用约定。x64 只有一种约定,__stdcall宏展开成空,代码怎么写都对;x86 下如果.c实现文件里的函数签名和头文件声明不一致(比如手抄的时候漏了FL_CALL),声明是__stdcall、定义是__cdecl,编译器不会报错,但调用方按__stdcall清栈,被调方按__cdecl也清一次,栈指针直接错位。
解决方式是杜绝手抄。所有导出函数的定义都必须通过包含头文件拿到宏,代码里不出现裸的int fl_plugin_calc(...)。另外 CI 里至少要有一个 32 位构建和一次 32 位宿主的冒烟测试,别只在开发机的 x64 上点几下就发版。
5.3 坑三:指标整体左移或前后两段接不上
现象是曲线前半截对、后半截错,或者跟内置公式比整体偏移了几十根 K 线,数据本身看着是行情的量级,就是位置不对。
原因是start的语义理解错了。有人把start当成"本批从哪一根开始画",于是在循环里写out[in->start + i] = ...,但宿主的out数组长度只有count,下标一过界就是踩内存;另一种写法是out只写count个但忘了状态推进,下一批进来接着从state的旧值算,两段之间出现跳变。
解决方式是在calc开头把start和count打到日志里,连跑一次全量、一次增量,看宿主到底怎么调的。然后统一规则:out永远从[0]写到[count-1],全局位置只用来维护状态,不用来索引输出数组。这条规则定下来,增量与否都不会错位。
5.4 坑四:中文公式名变成乱码
现象是公式列表里名字显示成一串问号或者方格,函数本身算得没错。
原因是字符串编码不一致。宿主可能是 GBK 系的老编码,而你用 UTF-8 源码编出来的字面量传给宿主,两边对不上。这类接口里但凡涉及字符串,头文件注释通常会写明编码;没写的话,用最笨的办法试——同一段中文分别用 GBK 和 UTF-8 编两版,看哪版显示正常。
解决方式是在插件内部做一次转换,只保留一处转换点,别让编码问题散落在各个字符串里。更好的做法是公式名用纯 ASCII,中文名在宿主的配置里另外填,插件只认 ID。这样你的 dll 拿到哪台机器上都不会因为代码页不同而翻车。
5.5 坑五:切几次公式之后内存一路涨
现象是用任务管理器看宿主进程内存,反复切换指标,内存不回落,跑一晚上能涨几百 MB。
原因是init里malloc、release里没释放,或者只在release里释放了主结构体,忘了链表节点内部的二次分配。还有一种隐蔽情况:宿主在重新加载 dll 时可能只调FreeLibrary不调release,你申请的东西就彻底丢了。
解决方式是给每个malloc配一条固定的释放路径,release里按申请的逆序释放,并且用调试版的 CRT 跑一遍泄漏检测(MSVC 的_CrtDumpMemoryLeaks)。另外,如果宿主可能不调release就直接卸载,那就在 dll 的DllMain的PROCESS_DETACH分支里补一次清理,但注意DllMain里能做的事很有限,别在里面调LoadLibrary或者等锁。
6. 二次开发:把一次性 Demo 改成能长期维护的指标插件
一个能画出线的 dll 和一个能维护三年的 dll,差别不在算法,在结构。我现在的习惯是把每个公式拆成"描述"和"计算"两段:描述部分用一张静态表说清公式名、参数个数、参数默认值、输出线数;计算部分只认下标和数组,不碰任何宿主相关的东西。这样新增一个指标就是往表里加一行、写一个纯函数,不用改框架代码。
typedef struct { const char* id; /* 宿主侧唯一标识,纯 ASCII */ const char* name; /* 展示名 */ int param_cnt; /* 参数个数,宿主按这个数量提供输入 */ double param_def[4]; int out_lines; /* 输出几条线 */ int (*calc)(const fl_series*, const double* params, double* out, int line); } fl_formula; static const fl_formula g_formulas[] = { { "ma20", "均线20", 1, { 20.0 }, 1, calc_ma }, { "ema12", "指数均线", 1, { 12.0 }, 1, calc_ema }, };这套表驱动的好处是扩展成本极低,加指标不用动入口函数。代价是每个calc都必须是无状态的纯函数,跨批次的状态得另外挂到上下文里去,两层结构要分清楚。
验证方法我一般用"三点对照":挑一段有明显趋势反转的历史区间,把自写指标和宿主内置的同参数指标叠一张图,看拐点是否重合;再把周期参数改成 5 和 60 各跑一遍,确认参数确实生效而不是被写死;最后手动拨快系统时间或者连跑一整天,看内存曲线是不是平的。三关都过,才敢挂到实盘界面上。
从那以后我每次接这类 C 接口,第一件事都是先编一个只有init打日志的空壳,确认加载链通了再写一行算法。听起来慢,但比在几十行代码里猜是符号名的问题还是调用约定的问题快得多。希望帮到你。
本文还有配套的精品资源,点击获取