diff --git a/.github/workflows/Build_VIPM_Library.yml b/.github/workflows/Build_VIPM_Library.yml index 37fccc4..b20551e 100644 --- a/.github/workflows/Build_VIPM_Library.yml +++ b/.github/workflows/Build_VIPM_Library.yml @@ -17,6 +17,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'c/**' push: paths-ignore: @@ -27,6 +28,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'c/**' # Allows you to run this workflow manually from the Actions tab workflow_dispatch: diff --git a/.github/workflows/Check_Broken_VIs.yml b/.github/workflows/Check_Broken_VIs.yml index 1ea181e..d618cc5 100644 --- a/.github/workflows/Check_Broken_VIs.yml +++ b/.github/workflows/Check_Broken_VIs.yml @@ -15,6 +15,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'c/**' pull_request: branches: @@ -29,6 +30,7 @@ on: - '**.svg' - '**.json' - '**.yml' + - 'c/**' # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: diff --git a/.gitignore b/.gitignore index dccf551..a963972 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,18 @@ *.aliases *.lvlps -*.vip \ No newline at end of file +*.vip + +# C / Visual Studio build artefacts (under c/) +c/**/Debug/ +c/**/Release/ +c/**/x64/ +c/**/x86/ +c/**/.vs/ +c/**/*.user +c/**/*.suo +c/**/*.obj +c/**/*.exe +c/**/*.pdb +c/**/*.ilk +c/**/*.idb +c/**/*.tlog \ No newline at end of file diff --git a/c/README.md b/c/README.md new file mode 100644 index 0000000..41e1b64 --- /dev/null +++ b/c/README.md @@ -0,0 +1,85 @@ +# CSM MassData Parameter Support —— C 语言移植 + +本目录提供 CSM MassData Parameter Support 插件的 C 语言实现, +接口与 [`addons/MassData-Parameter`](../addons/MassData-Parameter) +中的 LabVIEW VI 一一对应,可与 LabVIEW 端无缝互通。 + +## 目录结构 + +``` +c/ +├── include/csm_massdata.h # 公开 C 接口(中文 Doxygen 注释) +├── src/csm_massdata.c # 跨平台实现(Win32 / POSIX) +├── _test/ +│ └── vs/ +│ ├── csm_massdata_test.sln # Visual Studio 2026 解决方案 +│ ├── csm_massdata_test.vcxproj # 测试工程(按 C 编译) +│ ├── csm_massdata_test.vcxproj.filters +│ └── test_main.c # 独立测试程序 +└── README.md # 本文件 +``` + +## 接口对应关系 + +每个 C 函数都是对应 LabVIEW VI 的逐字翻译:名称相同、参数顺序相同、 +语义相同。 + +| LabVIEW VI | C 函数 | +| --------------------------------------------------- | ----------------------------------------------------- | +| `CSM - Config MassData Parameter Cache Size.vi` | `CSM_ConfigMassDataParameterCacheSize` | +| `CSM - Convert MassData to Argument.vim` | `CSM_ConvertMassDataToArgument` | +| `CSM - Convert MassData to Argument With DataType.vim` | `CSM_ConvertMassDataToArgumentWithDataType` | +| `CSM - Convert Argument to MassData.vim` | `CSM_ConvertArgumentToMassData` | +| `CSM - MassData Data Type String.vi` | `CSM_MassDataDataTypeString` | +| `CSM - MassData Parameter Status.vi` | `CSM_MassDataParameterStatus` | + +参考字符串格式与 LabVIEW 端完全相同: + +``` +Start:;Size:[;DataType:] +``` + +## 构建与运行测试 + +### Visual Studio 2026(推荐) + +1. 用 Visual Studio 2026 打开 `c/_test/vs/csm_massdata_test.sln`。 +2. 选择任意配置(例如 `Debug|x64`),按 F5 启动。 +3. 控制台会逐条打印断言结果,并在最后输出 `N/N 个断言通过` 摘要。 + +工程使用 `v145` 平台工具集(Visual Studio 2026 默认)。 + +### 命令行(任意支持 C99 的编译器) + +```bash +cd c +cc -std=c99 -Wall -Wextra -Iinclude src/csm_massdata.c _test/vs/test_main.c -o csm_test -lpthread +./csm_test +``` + +Windows 下可用 `cl /I include src\csm_massdata.c _test\vs\test_main.c`, +不需要 `-lpthread`(实现会自动改用 `CRITICAL_SECTION`)。 + +## 在自己的工程中链接 + +1. 将 `c/include` 加入工程的头文件搜索路径。 +2. 编译并链接 `c/src/csm_massdata.c`(除标准 C 库与 POSIX + 平台上的 pthreads 之外没有其它依赖)。 +3. `#include "csm_massdata.h"` 后即可调用头文件中文档化的所有函数。 + +典型的编码 / 解码往返如下: + +```c +#include "csm_massdata.h" + +double samples[1024] = { /* ... */ }; +char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; +double restored[1024]; +size_t restored_size = 0; + +CSM_ConfigMassDataParameterCacheSize(64u * 1024u * 1024u); /* 64 MiB 缓冲区 */ +CSM_ConvertMassDataToArgumentWithDataType(samples, sizeof(samples), + "1D DBL", arg, sizeof(arg)); +/* ... 通过 CSM 总线传递 `arg` ... */ +CSM_ConvertArgumentToMassData(arg, restored, sizeof(restored), &restored_size); +``` diff --git a/c/_test/vs/csm_massdata_test.sln b/c/_test/vs/csm_massdata_test.sln new file mode 100644 index 0000000..e191f43 --- /dev/null +++ b/c/_test/vs/csm_massdata_test.sln @@ -0,0 +1,27 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.0.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "csm_massdata_test", "csm_massdata_test.vcxproj", "{A1B2C3D4-E5F6-4789-A012-3456789ABCDE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|x86.ActiveCfg = Debug|Win32 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Debug|x86.Build.0 = Debug|Win32 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|x86.ActiveCfg = Release|Win32 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE}.Release|x86.Build.0 = Release|Win32 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/c/_test/vs/csm_massdata_test.vcxproj b/c/_test/vs/csm_massdata_test.vcxproj new file mode 100644 index 0000000..47b864a --- /dev/null +++ b/c/_test/vs/csm_massdata_test.vcxproj @@ -0,0 +1,84 @@ + + + + + Debug + Win32 + + + Release + Win32 + + + Debug + x64 + + + Release + x64 + + + + 18.0 + {A1B2C3D4-E5F6-4789-A012-3456789ABCDE} + csm_massdata_test + Win32Proj + 10.0 + + + + Application + true + v145 + Unicode + + + Application + false + v145 + true + Unicode + + + + + $(ProjectDir)..\..\include;%(AdditionalIncludeDirectories) + Level4 + stdc11 + CompileAsC + true + true + _CRT_SECURE_NO_WARNINGS;%(PreprocessorDefinitions) + + + Console + true + + + + + Disabled + _DEBUG;%(PreprocessorDefinitions) + + + + + MaxSpeed + true + true + NDEBUG;%(PreprocessorDefinitions) + + + true + true + + + + + + + + + + + diff --git a/c/_test/vs/csm_massdata_test.vcxproj.filters b/c/_test/vs/csm_massdata_test.vcxproj.filters new file mode 100644 index 0000000..1c2b923 --- /dev/null +++ b/c/_test/vs/csm_massdata_test.vcxproj.filters @@ -0,0 +1,26 @@ + + + + + {1f3a9e8f-1234-4abc-9def-111111111111} + c;cpp + + + {1f3a9e8f-1234-4abc-9def-222222222222} + h;hpp + + + + + Source Files + + + Source Files + + + + + Header Files + + + diff --git a/c/_test/vs/test_main.c b/c/_test/vs/test_main.c new file mode 100644 index 0000000..cc12c70 --- /dev/null +++ b/c/_test/vs/test_main.c @@ -0,0 +1,225 @@ +/** + * @file test_main.c + * @brief CSM MassData C 接口的独立测试程序。 + * + * 覆盖 `csm_massdata.h` 中公开的所有 API: + * - 缓冲区配置与状态查询 + * - 不带数据类型的编码 / 解码往返 + * - 带数据类型的编码 / 解码往返 + * - 数据类型解析(CSM - MassData Data Type String) + * - 解析错误的处理 + * - 环形缓冲区覆盖检测 + * + * 任意断言失败时程序以非零状态退出,方便在交互模式与 CI 流水线中 + * 同时使用。 + */ + +#include "csm_massdata.h" + +#include +#include +#include +#include +#include + +static int g_failures = 0; +static int g_total = 0; + +#define CSM_TEST(cond) \ + do { \ + ++g_total; \ + if (!(cond)) { \ + ++g_failures; \ + fprintf(stderr, "[FAIL] %s:%d %s\n", \ + __FILE__, __LINE__, #cond); \ + } else { \ + printf("[ OK ] %s\n", #cond); \ + } \ + } while (0) + +/* 测试:缓冲区配置与状态查询 */ +static void test_config_and_status(void) +{ + csm_massdata_operation_t r, w; + size_t cache = 0; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(0) == CSM_MASSDATA_ERR_INVALID_ARG); + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(1024) == CSM_MASSDATA_OK); + CSM_TEST(CSM_MassDataParameterStatus(&r, &w, &cache) == CSM_MASSDATA_OK); + CSM_TEST(cache == 1024); + CSM_TEST(r.size == 0 && w.size == 0); +} + +/* 测试:不带数据类型的编码 / 解码往返 */ +static void test_roundtrip_plain(void) +{ + const int32_t source[8] = { 10, 20, 30, 40, 50, 60, 70, 80 }; + int32_t restored[8]; + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + size_t out_size = 0; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(4096) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(source, sizeof(source), + arg, sizeof(arg)) == CSM_MASSDATA_OK); + CSM_TEST(strncmp(arg, "Start:", 16) == 0); + + memset(restored, 0, sizeof(restored)); + CSM_TEST(CSM_ConvertArgumentToMassData(arg, restored, sizeof(restored), + &out_size) == CSM_MASSDATA_OK); + CSM_TEST(out_size == sizeof(source)); + CSM_TEST(memcmp(source, restored, sizeof(source)) == 0); +} + +/* 测试:带数据类型的编码 / 解码往返 */ +static void test_roundtrip_with_type(void) +{ + const double source[4] = { 1.5, -2.5, 3.5, -4.5 }; + double restored[4]; + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + char dup[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + char type[CSM_MASSDATA_MAX_DATATYPE_LEN]; + size_t out_size = 0; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(4096) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgumentWithDataType(source, sizeof(source), + "1D DBL", arg, + sizeof(arg)) + == CSM_MASSDATA_OK); + CSM_TEST(strstr(arg, ";DataType:1D DBL") != NULL); + + CSM_TEST(CSM_MassDataDataTypeString(arg, dup, sizeof(dup), + type, sizeof(type)) == CSM_MASSDATA_OK); + CSM_TEST(strcmp(type, "1D DBL") == 0); + CSM_TEST(strcmp(dup, arg) == 0); + + memset(restored, 0, sizeof(restored)); + CSM_TEST(CSM_ConvertArgumentToMassData(arg, restored, sizeof(restored), + &out_size) == CSM_MASSDATA_OK); + CSM_TEST(out_size == sizeof(source)); + CSM_TEST(memcmp(source, restored, sizeof(source)) == 0); +} + +/* 测试:参数中没有数据类型字段时,解析结果应为空字符串 */ +static void test_datatype_absent(void) +{ + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + char type[CSM_MASSDATA_MAX_DATATYPE_LEN]; + const uint8_t payload[3] = { 0xAA, 0xBB, 0xCC }; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(4096) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(payload, sizeof(payload), + arg, sizeof(arg)) == CSM_MASSDATA_OK); + type[0] = 'x'; + CSM_TEST(CSM_MassDataDataTypeString(arg, NULL, 0, type, sizeof(type)) + == CSM_MASSDATA_OK); + CSM_TEST(type[0] == '\0'); +} + +/* 测试:解析非法字符串时返回 PARSE 错误 */ +static void test_parse_errors(void) +{ + size_t out = 0; + uint8_t buf[16]; + + CSM_TEST(CSM_ConvertArgumentToMassData("garbage", buf, sizeof(buf), &out) + == CSM_MASSDATA_ERR_PARSE); + CSM_TEST(CSM_ConvertArgumentToMassData("Start:abc;Size:1", + buf, sizeof(buf), &out) + == CSM_MASSDATA_ERR_PARSE); + CSM_TEST(CSM_ConvertArgumentToMassData("Start:0;Size:1;wrong", + buf, sizeof(buf), &out) + == CSM_MASSDATA_ERR_PARSE); +} + +/* 测试:旧数据被环形缓冲区覆盖后,应报告 OVERWRITTEN */ +static void test_overwrite_detection(void) +{ + char first_arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + char filler_arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + uint8_t small[8] = { 1, 2, 3, 4, 5, 6, 7, 8 }; + uint8_t filler[32]; + uint8_t restored[8]; + size_t out = 0; + int i; + + /* 缓冲区故意设得很小,后续写入会把第一份数据挤掉。 */ + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(32) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(small, sizeof(small), + first_arg, sizeof(first_arg)) + == CSM_MASSDATA_OK); + for (i = 0; i < (int)sizeof(filler); ++i) filler[i] = (uint8_t)i; + CSM_TEST(CSM_ConvertMassDataToArgument(filler, sizeof(filler), + filler_arg, sizeof(filler_arg)) + == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertArgumentToMassData(first_arg, restored, + sizeof(restored), &out) + == CSM_MASSDATA_ERR_OVERWRITTEN); +} + +/* 测试:输出缓冲区太小时返回 BUFFER_TOO_SMALL,并报告所需大小 */ +static void test_buffer_too_small(void) +{ + const uint8_t payload[10] = { 0 }; + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + uint8_t small_out[2]; + size_t out = 0; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(4096) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(payload, sizeof(payload), + arg, sizeof(arg)) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertArgumentToMassData(arg, small_out, sizeof(small_out), + &out) == CSM_MASSDATA_ERR_BUFFER_TOO_SMALL); + CSM_TEST(out == sizeof(payload)); +} + +/* 测试:写入数据大于缓冲区容量时返回 CACHE_TOO_SMALL */ +static void test_cache_too_small(void) +{ + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + uint8_t big[64] = { 0 }; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(16) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(big, sizeof(big), arg, sizeof(arg)) + == CSM_MASSDATA_ERR_CACHE_TOO_SMALL); +} + +/* 测试:状态查询应当反映最近一次的读 / 写操作 */ +static void test_status_reflects_last_ops(void) +{ + csm_massdata_operation_t r, w; + size_t cache = 0; + char arg[CSM_MASSDATA_MAX_ARGUMENT_LEN]; + uint8_t payload[5] = { 9, 8, 7, 6, 5 }; + uint8_t restored[5]; + size_t out = 0; + + CSM_TEST(CSM_ConfigMassDataParameterCacheSize(1024) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertMassDataToArgument(payload, sizeof(payload), + arg, sizeof(arg)) == CSM_MASSDATA_OK); + CSM_TEST(CSM_ConvertArgumentToMassData(arg, restored, sizeof(restored), + &out) == CSM_MASSDATA_OK); + CSM_TEST(CSM_MassDataParameterStatus(&r, &w, &cache) == CSM_MASSDATA_OK); + CSM_TEST(w.size == sizeof(payload)); + CSM_TEST(r.size == sizeof(payload)); + CSM_TEST(cache == 1024); +} + +int main(void) +{ + printf("CSM MassData C API 测试套件\n"); + printf("===========================\n"); + + test_config_and_status(); + test_roundtrip_plain(); + test_roundtrip_with_type(); + test_datatype_absent(); + test_parse_errors(); + test_overwrite_detection(); + test_buffer_too_small(); + test_cache_too_small(); + test_status_reflects_last_ops(); + + printf("\n%d/%d 个断言通过,%d 个失败。\n", + g_total - g_failures, g_total, g_failures); + return (g_failures == 0) ? 0 : 1; +} diff --git a/c/include/csm_massdata.h b/c/include/csm_massdata.h new file mode 100644 index 0000000..51afd15 --- /dev/null +++ b/c/include/csm_massdata.h @@ -0,0 +1,219 @@ +/** + * @file csm_massdata.h + * @brief CSM MassData Parameter Support 插件的 C 语言移植版本。 + * + * 本头文件公开的 C 接口与 LabVIEW 端 + * `addons/MassData-Parameter/CSM MassData Parameter Support.lvlib` + * 中的 VI 在功能与命名上完全一致。 + * + * 函数名称、参数顺序与语义均与对应的 LabVIEW VI 严格保持一致, + * 因此使用 C 与 LabVIEW 两种语言编写的代码可以无需任何转换层 + * 直接互通 MassData 参数。 + * + * @par MassData 参数格式 + * MassData 参数是一段仅包含 ASCII 字符、可读的引用字符串,指向 + * 进程内一个全局环形缓冲区中的实际数据。支持以下两种形式: + * + * - 不带数据类型: `Start:;Size:` + * - 带 数据类型: `Start:;Size:;DataType:` + * + * 其中 `` 为非负十进制整数,`` 为自由格式的数据类型标签 + * (例如 `1D I32`、`Waveform` 等),由 CSM Data Type String VI 定义。 + * + * @par 数据生命周期 + * MassData 内部使用环形缓冲区。当缓冲区写满后,新写入的数据将 + * 从缓冲区起始位置覆盖最早的数据。被覆盖的数据无法恢复, + * 后续对其引用进行解码时会返回错误。同一进程内的所有调用者 + * 共享同一份 MassData 缓冲区。 + * + * @par 线程安全 + * 在内部状态完成初始化后,公开函数的并发调用会通过内部互斥量 + * 串行化执行。延迟初始化通过平台原语(Windows: @c InitOnceExecuteOnce, + * POSIX: @c pthread_once)保证仅执行一次,即使多个线程并发首次调用 + * 也不会产生竞态。建议在进入多线程阶段前主动调用 + * CSM_ConfigMassDataParameterCacheSize() 完成初始化,以获得最佳性能。 + * + * @copyright MIT 许可证 — 详见仓库根目录的 LICENSE 文件。 + */ + +#ifndef CSM_MASSDATA_H +#define CSM_MASSDATA_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* ------------------------------------------------------------------------- */ +/* 常量与类型定义 */ +/* ------------------------------------------------------------------------- */ + +/** MassData 缓冲区的默认大小(字节),与 LabVIEW VI 保持一致:50 MiB。 */ +#define CSM_MASSDATA_DEFAULT_CACHE_SIZE ((size_t)(50u * 1024u * 1024u)) + +/** 编码函数返回的 MassData 参数字符串的最大长度(包含末尾 NUL)。 */ +#define CSM_MASSDATA_MAX_ARGUMENT_LEN 256 + +/** 数据类型标签字符串的最大长度(包含末尾 NUL)。 */ +#define CSM_MASSDATA_MAX_DATATYPE_LEN 128 + +/** + * @brief MassData API 所有函数返回的状态码。 + */ +typedef enum csm_massdata_status_e { + CSM_MASSDATA_OK = 0, /**< 操作成功完成。 */ + CSM_MASSDATA_ERR_INVALID_ARG = -1, /**< 参数为 NULL 或无效。 */ + CSM_MASSDATA_ERR_BUFFER_TOO_SMALL = -2, /**< 调用方提供的输出缓冲区不足。 */ + CSM_MASSDATA_ERR_PARSE = -3, /**< MassData 参数字符串无法解析。 */ + CSM_MASSDATA_ERR_OVERWRITTEN = -4, /**< 引用的数据已被环形缓冲区覆盖。 */ + CSM_MASSDATA_ERR_CACHE_TOO_SMALL = -5, /**< 待写入的数据大于缓冲区容量。 */ + CSM_MASSDATA_ERR_NO_MEMORY = -6 /**< 内存分配失败。 */ +} csm_massdata_status_t; + +/** + * @brief 描述最近一次对 MassData 环形缓冲区的读或写操作。 + * + * 等价于 `CSM - MassData Parameter Status.vi` 返回的 + * @c Active Read Operation / @c Active Write Operation 簇。 + */ +typedef struct csm_massdata_operation_s { + uint64_t start; /**< 在缓冲区中的起始偏移量(字节)。 */ + uint64_t size; /**< 该次操作的字节数。 */ +} csm_massdata_operation_t; + +/* ------------------------------------------------------------------------- */ +/* API 函数 —— 每个函数对应一个同名的 LabVIEW VI */ +/* ------------------------------------------------------------------------- */ + +/** + * @brief 配置 MassData 后台缓冲区大小。 + * + * 对应 `CSM - Config MassData Parameter Cache Size.vi`。 + * + * 将内部环形缓冲区重新分配为 @p size 字节。与 LabVIEW VI 一致, + * 在程序运行过程中调用本函数会丢弃当前已缓存的数据;通常应在 + * 任何编码 / 解码调用之前、应用启动阶段调用一次。 + * + * @param[in] size 新的缓冲区大小(字节)。若从未调用过本函数, + * 则默认值为 @ref CSM_MASSDATA_DEFAULT_CACHE_SIZE。 + * + * @return 成功返回 @ref CSM_MASSDATA_OK; + * 若 @p size 为 0 返回 @ref CSM_MASSDATA_ERR_INVALID_ARG; + * 若分配失败返回 @ref CSM_MASSDATA_ERR_NO_MEMORY。 + */ +csm_massdata_status_t CSM_ConfigMassDataParameterCacheSize(size_t size); + +/** + * @brief 将原始数据转换为 MassData 参数(不嵌入数据类型)。 + * + * 对应 `CSM - Convert MassData to Argument.vim`。原始数据被复制到 + * 环形缓冲区,并向 @p argument 写入形如 + * `Start:;Size:` 的引用字符串。 + * + * @param[in] data 指向待保存的原始字节。仅当 @p data_size + * 为 0 时允许为 @c NULL。 + * @param[in] data_size @p data 的字节长度。 + * @param[out] argument 调用方分配的输出缓冲区,用于接收以 NUL 结尾的 + * MassData 参数字符串。 + * @param[in] argument_cap @p argument 的容量(字节),建议不小于 + * @ref CSM_MASSDATA_MAX_ARGUMENT_LEN。 + * + * @return @ref CSM_MASSDATA_OK 或对应的错误码。 + */ +csm_massdata_status_t CSM_ConvertMassDataToArgument(const void *data, + size_t data_size, + char *argument, + size_t argument_cap); + +/** + * @brief 将原始数据转换为带数据类型标签的 MassData 参数。 + * + * 对应 `CSM - Convert MassData to Argument With DataType.vim`。 + * 生成的参数形如 `Start:;Size:;DataType:`。 + * + * @param[in] data 指向待保存的原始字节。 + * @param[in] data_size @p data 的字节长度。 + * @param[in] data_type 以 NUL 结尾的数据类型字符串(例如 `"1D I32"`)。 + * 字符串中不允许出现 `';'` 或 `'<'` / `'>'`。 + * @param[out] argument 调用方分配的输出缓冲区,用于接收结果。 + * @param[in] argument_cap @p argument 的容量(字节)。 + * + * @return @ref CSM_MASSDATA_OK 或对应的错误码。 + */ +csm_massdata_status_t CSM_ConvertMassDataToArgumentWithDataType(const void *data, + size_t data_size, + const char *data_type, + char *argument, + size_t argument_cap); + +/** + * @brief 将 MassData 参数还原为原始数据。 + * + * 对应 `CSM - Convert Argument to MassData.vim`。本函数解析 + * @p argument 中的引用字符串,并将对应的数据复制到 @p data。 + * LabVIEW VI 中可选的 `Type` 输入在此处刻意省略:返回的就是 + * 此前写入的原始字节,不受嵌入的类型标签影响。 + * + * @param[in] argument 以 NUL 结尾的 MassData 参数字符串。 + * @param[out] data 调用方分配的接收缓冲区。 + * @param[in] data_cap @p data 的容量(字节)。 + * @param[out] data_size_out 返回实际写入 @p data 的字节数, + * 不允许为 @c NULL。 + * + * @return 成功返回 @ref CSM_MASSDATA_OK; + * @p argument 不合法时返回 @ref CSM_MASSDATA_ERR_PARSE; + * 缓存中的数据已被覆盖时返回 @ref CSM_MASSDATA_ERR_OVERWRITTEN; + * @p data_cap 小于实际数据大小时返回 + * @ref CSM_MASSDATA_ERR_BUFFER_TOO_SMALL(此时 + * @p data_size_out 仍会写入所需的字节数)。 + */ +csm_massdata_status_t CSM_ConvertArgumentToMassData(const char *argument, + void *data, + size_t data_cap, + size_t *data_size_out); + +/** + * @brief 从 MassData 参数中解析出数据类型字符串。 + * + * 对应 `CSM - MassData Data Type String.vi`。本函数不会消费输入: + * 当 @p argument_dup 非空时,会写入 @p argument 的副本,以模仿 + * LabVIEW VI 中“返回输入副本”的数据流行为。 + * + * @param[in] argument 以 NUL 结尾的 MassData 参数字符串。 + * @param[out] argument_dup 可选。若非空,则接收 @p argument 的副本。 + * @param[in] argument_dup_cap @p argument_dup 的容量(当 @p argument_dup + * 为空时忽略)。 + * @param[out] data_type 接收以 NUL 结尾的数据类型标签; + * 若不存在则为空字符串。 + * @param[in] data_type_cap @p data_type 的容量(字节)。 + * + * @return @ref CSM_MASSDATA_OK 或对应的错误码。 + */ +csm_massdata_status_t CSM_MassDataDataTypeString(const char *argument, + char *argument_dup, + size_t argument_dup_cap, + char *data_type, + size_t data_type_cap); + +/** + * @brief 读取 MassData 后台缓冲区的状态信息。 + * + * 对应 `CSM - MassData Parameter Status.vi`。 + * + * @param[out] active_read 接收最近一次的读操作信息,可为 @c NULL。 + * @param[out] active_write 接收最近一次的写操作信息,可为 @c NULL。 + * @param[out] cache_size 接收当前配置的缓冲区大小(字节),可为 @c NULL。 + * + * @return 始终返回 @ref CSM_MASSDATA_OK。 + */ +csm_massdata_status_t CSM_MassDataParameterStatus(csm_massdata_operation_t *active_read, + csm_massdata_operation_t *active_write, + size_t *cache_size); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* CSM_MASSDATA_H */ diff --git a/c/src/csm_massdata.c b/c/src/csm_massdata.c new file mode 100644 index 0000000..ed0495a --- /dev/null +++ b/c/src/csm_massdata.c @@ -0,0 +1,462 @@ +/** + * @file csm_massdata.c + * @brief CSM MassData Parameter Support C 接口的实现。 + * + * MassData 缓存是进程内唯一的环形缓冲区,由互斥量保护。每次成功的 + * 编码会推进一个单调递增的 64 位写游标 `write_total`,并将数据 + * 复制到环形缓冲区中 `write_total % capacity` 的位置(必要时回绕)。 + * 该游标值会被写入返回字符串的 `Start:` 字段,从而让解码端能够 + * 判断引用的数据是否仍驻留在缓存中、还是已经被后续写入覆盖。 + */ + +#include "csm_massdata.h" + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +# define WIN32_LEAN_AND_MEAN +# include +typedef CRITICAL_SECTION csm_mutex_t; +# define CSM_MUTEX_INIT(m) InitializeCriticalSection(m) +# define CSM_MUTEX_LOCK(m) EnterCriticalSection(m) +# define CSM_MUTEX_UNLOCK(m) LeaveCriticalSection(m) +static INIT_ONCE g_init_once = INIT_ONCE_STATIC_INIT; +#else +# include +typedef pthread_mutex_t csm_mutex_t; +# define CSM_MUTEX_INIT(m) pthread_mutex_init((m), NULL) +# define CSM_MUTEX_LOCK(m) pthread_mutex_lock(m) +# define CSM_MUTEX_UNLOCK(m) pthread_mutex_unlock(m) +static pthread_once_t g_once = PTHREAD_ONCE_INIT; +#endif + +/* ------------------------------------------------------------------------- */ +/* 内部状态 */ +/* ------------------------------------------------------------------------- */ + +#define CSM_MASSDATA_PREFIX "" + +typedef struct csm_massdata_state_s { + int initialized; + csm_mutex_t mutex; + uint8_t *buffer; /* 环形字节缓冲区。 */ + size_t capacity; /* 缓冲区已分配的大小。 */ + uint64_t write_total; /* 累计已写入的字节数。 */ + csm_massdata_operation_t last_read; + csm_massdata_operation_t last_write; +} csm_massdata_state_t; + +static csm_massdata_state_t g_state; + +#if defined(_WIN32) +static BOOL CALLBACK csm_massdata_do_init(PINIT_ONCE io, PVOID p, PVOID *ctx) +{ + (void)io; (void)p; (void)ctx; + g_state.buffer = (uint8_t *)malloc(CSM_MASSDATA_DEFAULT_CACHE_SIZE); + g_state.capacity = (g_state.buffer != NULL) ? CSM_MASSDATA_DEFAULT_CACHE_SIZE : 0u; + g_state.write_total = 0u; + memset(&g_state.last_read, 0, sizeof(g_state.last_read)); + memset(&g_state.last_write, 0, sizeof(g_state.last_write)); + CSM_MUTEX_INIT(&g_state.mutex); + g_state.initialized = 1; + return TRUE; +} +static void csm_massdata_lazy_init(void) +{ + InitOnceExecuteOnce(&g_init_once, csm_massdata_do_init, NULL, NULL); +} +#else +static void csm_massdata_do_init(void) +{ + g_state.buffer = (uint8_t *)malloc(CSM_MASSDATA_DEFAULT_CACHE_SIZE); + g_state.capacity = (g_state.buffer != NULL) ? CSM_MASSDATA_DEFAULT_CACHE_SIZE : 0u; + g_state.write_total = 0u; + memset(&g_state.last_read, 0, sizeof(g_state.last_read)); + memset(&g_state.last_write, 0, sizeof(g_state.last_write)); + CSM_MUTEX_INIT(&g_state.mutex); + g_state.initialized = 1; +} +static void csm_massdata_lazy_init(void) +{ + pthread_once(&g_once, csm_massdata_do_init); +} +#endif + +/* ------------------------------------------------------------------------- */ +/* 内部辅助函数 */ +/* ------------------------------------------------------------------------- */ + +static int csm_massdata_starts_with(const char *s, const char *prefix) +{ + while (*prefix) { + if (*s++ != *prefix++) { + return 0; + } + } + return 1; +} + +/** + * 解析形如 `Start:;Size:[;DataType:]` 的 MassData + * 参数字符串。 + * + * @param[out] data_type 可选的数据类型标签输出缓冲区。 + * @param[in] data_type_cap @p data_type 的容量。 + * + * @return 成功返回 CSM_MASSDATA_OK;输入非法返回 CSM_MASSDATA_ERR_PARSE; + * 数据类型标签放不下时返回 CSM_MASSDATA_ERR_BUFFER_TOO_SMALL。 + */ +static csm_massdata_status_t csm_massdata_parse(const char *argument, + uint64_t *start_out, + uint64_t *size_out, + char *data_type, + size_t data_type_cap) +{ + const char *p; + char *end; + unsigned long long tmp; + + if (argument == NULL || start_out == NULL || size_out == NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + if (!csm_massdata_starts_with(argument, CSM_MASSDATA_PREFIX)) { + return CSM_MASSDATA_ERR_PARSE; + } + p = argument + (sizeof(CSM_MASSDATA_PREFIX) - 1); + + if (strncmp(p, "Start:", 6) != 0) { + return CSM_MASSDATA_ERR_PARSE; + } + p += 6; + errno = 0; + tmp = strtoull(p, &end, 10); + if (end == p || *end != ';' || errno != 0) { + return CSM_MASSDATA_ERR_PARSE; + } + *start_out = (uint64_t)tmp; + p = end + 1; + + if (strncmp(p, "Size:", 5) != 0) { + return CSM_MASSDATA_ERR_PARSE; + } + p += 5; + errno = 0; + tmp = strtoull(p, &end, 10); + if (end == p || errno != 0) { + return CSM_MASSDATA_ERR_PARSE; + } + *size_out = (uint64_t)tmp; + p = end; + + /* 可选的 ;DataType: 后缀。 */ + if (data_type != NULL && data_type_cap > 0u) { + data_type[0] = '\0'; + } + if (*p == '\0') { + return CSM_MASSDATA_OK; + } + if (*p != ';') { + return CSM_MASSDATA_ERR_PARSE; + } + p++; + if (strncmp(p, "DataType:", 9) != 0) { + return CSM_MASSDATA_ERR_PARSE; + } + p += 9; + /* 验证 DataType 值仅包含合法字符,且字符串以 NUL 结尾。 */ + { + const char *q = p; + while (*q && *q != ';' && *q != '<' && *q != '>') { + q++; + } + if (*q != '\0') { + return CSM_MASSDATA_ERR_PARSE; + } + } + if (data_type != NULL && data_type_cap > 0u) { + size_t len = strlen(p); + if (len + 1u > data_type_cap) { + return CSM_MASSDATA_ERR_BUFFER_TOO_SMALL; + } + memcpy(data_type, p, len + 1u); + } + return CSM_MASSDATA_OK; +} + +/** + * 将 @p src 指向的 @p size 字节按照 @c g_state.write_total 暗示的位置 + * 写入环形缓冲区。调用者必须持有互斥量,并已确保 @p size 不大于 + * 缓冲区容量。 + */ +static void csm_massdata_ring_write(const uint8_t *src, size_t size) +{ + size_t offset = (size_t)(g_state.write_total % (uint64_t)g_state.capacity); + size_t first_run = g_state.capacity - offset; + + if (size <= first_run) { + memcpy(g_state.buffer + offset, src, size); + } else { + memcpy(g_state.buffer + offset, src, first_run); + memcpy(g_state.buffer, src + first_run, size - first_run); + } +} + +/** + * 从环形缓冲区中以绝对游标 @p start 起始位置读取 @p size 字节到 + * @p dst。调用者必须持有互斥量,并已确认所请求的范围仍然驻留。 + */ +static void csm_massdata_ring_read(uint64_t start, uint8_t *dst, size_t size) +{ + size_t offset = (size_t)(start % (uint64_t)g_state.capacity); + size_t first_run = g_state.capacity - offset; + + if (size <= first_run) { + memcpy(dst, g_state.buffer + offset, size); + } else { + memcpy(dst, g_state.buffer + offset, first_run); + memcpy(dst + first_run, g_state.buffer, size - first_run); + } +} + +/* ------------------------------------------------------------------------- */ +/* 公开 API */ +/* ------------------------------------------------------------------------- */ + +csm_massdata_status_t CSM_ConfigMassDataParameterCacheSize(size_t size) +{ + uint8_t *new_buf; + + if (size == 0u) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + + csm_massdata_lazy_init(); + + new_buf = (uint8_t *)malloc(size); + if (new_buf == NULL) { + return CSM_MASSDATA_ERR_NO_MEMORY; + } + + CSM_MUTEX_LOCK(&g_state.mutex); + free(g_state.buffer); + g_state.buffer = new_buf; + g_state.capacity = size; + g_state.write_total = 0u; + memset(&g_state.last_read, 0, sizeof(g_state.last_read)); + memset(&g_state.last_write, 0, sizeof(g_state.last_write)); + CSM_MUTEX_UNLOCK(&g_state.mutex); + + return CSM_MASSDATA_OK; +} + +static csm_massdata_status_t csm_massdata_encode(const void *data, + size_t data_size, + const char *data_type, + char *argument, + size_t argument_cap) +{ + uint64_t start_cursor; + int written; + + if (argument == NULL || argument_cap == 0u) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + if (data_size > 0u && data == NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + if (data_type != NULL) { + size_t dt_len = strlen(data_type); + if (dt_len + 1u > CSM_MASSDATA_MAX_DATATYPE_LEN) { + return CSM_MASSDATA_ERR_BUFFER_TOO_SMALL; + } + /* 拒绝可能破坏引用字符串语法的字符。 */ + if (strpbrk(data_type, ";<>") != NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + } + + csm_massdata_lazy_init(); + + CSM_MUTEX_LOCK(&g_state.mutex); + if (g_state.buffer == NULL) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_NO_MEMORY; + } + if (data_size > g_state.capacity) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_CACHE_TOO_SMALL; + } + + start_cursor = g_state.write_total; + if (data_size > 0u) { + csm_massdata_ring_write((const uint8_t *)data, data_size); + g_state.write_total += (uint64_t)data_size; + } + g_state.last_write.start = start_cursor; + g_state.last_write.size = (uint64_t)data_size; + CSM_MUTEX_UNLOCK(&g_state.mutex); + + if (data_type != NULL) { + written = snprintf(argument, argument_cap, + CSM_MASSDATA_PREFIX + "Start:%" PRIu64 ";Size:%" PRIu64 ";DataType:%s", + start_cursor, (uint64_t)data_size, data_type); + } else { + written = snprintf(argument, argument_cap, + CSM_MASSDATA_PREFIX + "Start:%" PRIu64 ";Size:%" PRIu64, + start_cursor, (uint64_t)data_size); + } + if (written < 0 || (size_t)written >= argument_cap) { + return CSM_MASSDATA_ERR_BUFFER_TOO_SMALL; + } + return CSM_MASSDATA_OK; +} + +csm_massdata_status_t CSM_ConvertMassDataToArgument(const void *data, + size_t data_size, + char *argument, + size_t argument_cap) +{ + return csm_massdata_encode(data, data_size, NULL, argument, argument_cap); +} + +csm_massdata_status_t CSM_ConvertMassDataToArgumentWithDataType(const void *data, + size_t data_size, + const char *data_type, + char *argument, + size_t argument_cap) +{ + if (data_type == NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + return csm_massdata_encode(data, data_size, data_type, argument, argument_cap); +} + +csm_massdata_status_t CSM_ConvertArgumentToMassData(const char *argument, + void *data, + size_t data_cap, + size_t *data_size_out) +{ + csm_massdata_status_t status; + uint64_t start = 0u; + uint64_t size = 0u; + + if (data_size_out == NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + + status = csm_massdata_parse(argument, &start, &size, NULL, 0u); + if (status != CSM_MASSDATA_OK) { + return status; + } + + csm_massdata_lazy_init(); + + /* 在 32 位平台上 size_t 可能小于 uint64_t,需先验证。 */ + if (size > (uint64_t)SIZE_MAX) { + return CSM_MASSDATA_ERR_PARSE; + } + if (size > 0u && data == NULL) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + *data_size_out = (size_t)size; + if (size > (uint64_t)data_cap) { + return CSM_MASSDATA_ERR_BUFFER_TOO_SMALL; + } + + CSM_MUTEX_LOCK(&g_state.mutex); + if (g_state.buffer == NULL) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_NO_MEMORY; + } + if (size > (uint64_t)g_state.capacity) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_OVERWRITTEN; + } + /* 当前驻留在环形缓冲区中的窗口为 + * [write_total - capacity, write_total)。任何末端落在该窗口 + * 之外的请求都视为已被覆盖。 */ + { + uint64_t oldest = (g_state.write_total > (uint64_t)g_state.capacity) + ? g_state.write_total - (uint64_t)g_state.capacity + : 0u; + uint64_t end; + + if (size > 0u && start > UINT64_MAX - size) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_OVERWRITTEN; + } + if (start < oldest || start > g_state.write_total) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_OVERWRITTEN; + } + + end = start + size; + if (end > g_state.write_total) { + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_ERR_OVERWRITTEN; + } + if (size > 0u) { + csm_massdata_ring_read(start, (uint8_t *)data, (size_t)size); + } + g_state.last_read.start = start; + g_state.last_read.size = size; + } + CSM_MUTEX_UNLOCK(&g_state.mutex); + + return CSM_MASSDATA_OK; +} + +csm_massdata_status_t CSM_MassDataDataTypeString(const char *argument, + char *argument_dup, + size_t argument_dup_cap, + char *data_type, + size_t data_type_cap) +{ + csm_massdata_status_t status; + uint64_t start = 0u; + uint64_t size = 0u; + + if (argument == NULL || data_type == NULL || data_type_cap == 0u) { + return CSM_MASSDATA_ERR_INVALID_ARG; + } + + status = csm_massdata_parse(argument, &start, &size, data_type, data_type_cap); + if (status != CSM_MASSDATA_OK) { + return status; + } + + if (argument_dup != NULL) { + size_t len = strlen(argument); + if (len + 1u > argument_dup_cap) { + return CSM_MASSDATA_ERR_BUFFER_TOO_SMALL; + } + memcpy(argument_dup, argument, len + 1u); + } + return CSM_MASSDATA_OK; +} + +csm_massdata_status_t CSM_MassDataParameterStatus(csm_massdata_operation_t *active_read, + csm_massdata_operation_t *active_write, + size_t *cache_size) +{ + csm_massdata_lazy_init(); + CSM_MUTEX_LOCK(&g_state.mutex); + if (active_read != NULL) { + *active_read = g_state.last_read; + } + if (active_write != NULL) { + *active_write = g_state.last_write; + } + if (cache_size != NULL) { + *cache_size = g_state.capacity; + } + CSM_MUTEX_UNLOCK(&g_state.mutex); + return CSM_MASSDATA_OK; +}