☰
Windows下VS2019编译OSQP求解器:从CMake到C/C++接入全指南
2026/10/18 6:18:42 网站建设 项目流程

做控制、做机器人、做运筹优化的人,早晚要跟二次规划(QP)打交道。OSQP(Operator Splitting QP)是这一卦里口碑不错的开源求解器——用ADMM迭代解稀疏二次规划,对大规模、结构稀疏的问题内存占用低、速度可观,MPC(模型预测控制)场景里尤其常见,很多论文和开源项目拿它当默认的QP求解器。问题是,你从仓库拉下来的是源码,官方并不直接给Windows下的二进制包。在Linux上还好,一条build命令一步到位;到了Windows + Visual Studio 2019这个组合,非得自己用CMake过一遍生成工程、选配置、编译出库,再接进自己的C/C++项目里调用。这篇就是把这一整套流程串起来:CMake怎么生成VS2019工程、怎么编出静态库、C/C++侧怎么链接、怎么写第一个求解程序,以及我在实际工程里踩过的几个坑。

1. 为什么要在VS2019下自己编译OSQP

1.1 OSQP是谁,解决什么问题

二次规划的标准形式长这样:

minimize 1/2 xᵀP x + qᵀx subject to l ≤ Ax ≤ u

其中P是目标函数二次项的系数矩阵(要求半正定),q是一次项系数,A是线性约束矩阵,l和u是约束的上下界。等式约束就是把l和u设成同一个值。大量工程问题最后都能落到这个形式:最小二乘回归加约束、投资组合优化、支持向量机的对偶问题、模型预测控制(MPC)里的滚动优化,等等。

OSQP的做法是把原问题拆成两个更简单的子问题来回迭代,算法叫ADMM(交替方向乘子法)。它不需要对P做复杂的矩阵分解,不要求P严格正定,半正定就够用,而且能主动检测原问题不可行(primal infeasible)和对偶不可行(dual infeasible)。对中等规模、稀疏的问题,它比很多通用内点法求解器更省内存,还能通过设置最大迭代次数来硬性控制求解耗时——这一点对实时控制特别重要。

1.2 拿来主义行不通的几个理由

你可能觉得,开源库嘛,网上找个编译好的包装上不就完了?我帮你把常见的几条路都试过:

  • vcpkg:一条命令vcpkg install osqp:x64-windows确实能装上,但版本相对滞后,而且它把CMake配置写死在了一套默认参数里,你想换浮点类型、想开Intel MKL的PARDISO后端、想改编译器的运行时库,vcpkg的口子很难撬开。
  • conda:Python侧很舒服,conda install osqp装完import osqp就能用。但C/C++侧要的头文件和.lib文件藏在conda的pkgs目录里,找起来费劲,而且混用不同来源编译的CRT runtime迟早出事。
  • 官方release:GitHub的release页面给的是源码包,没有现成的Windows二进制。

所以自己编译不是"折腾",而是用OSQP做C/C++工程的基本功。一旦编过一次,后面想给OSQP加打印、加计时、改迭代策略,改完重新编也就是几分钟的事。

1.3 这一趟要完成的四件事

本文按实际操作的顺序分四步:准备好工具链和源码、用CMake生成VS2019工程并编译出库、把库接进自己的C/C++工程、写一个能验算的求解Demo。最后再讲几个真正干活时才会碰到的进阶问题。第一次走通整个过程,预留一个小时比较从容;之后每次换版本编译,就是复制粘贴三条命令的事。

2. 编译前的工具链与源码准备

2.1 VS2019与CMake版本检查

先把底子打好。我默认你已经在用Visual Studio 2019做C/C++开发,但有几个关键点需要确认:

  1. VS2019的安装组件:打开Visual Studio Installer,确认安装了"使用C++的桌面开发"工作负载,里面包含MSVC v142编译器工具集和Windows 10 SDK。只装了C#或者只装了Python组件的VS2019是编不了C++的。
  2. CMake版本:OSQP的CMakeLists要求最低3.10,我建议用3.15以上,后面设置MSVC运行时库选项时会用到新特性。在命令行里敲cmake --version确认一下。
  3. Git:有最好,没有的话直接下载源码zip包也没问题。

确认完毕后在开始菜单里打开"x64 Native Tools Command Prompt for VS 2019"。注意是x64开头的那个,不是x86的。这个终端会把MSVC编译器、Windows SDK和CMake的路径都配好,后面编译省很多事。

2.2 拉取源码与版本选择

源码从官方GitHub仓库拉,我建议拉带版本号的稳定tag而不是master分支,毕竟master可能处于开发状态:

git clone --depth 1 --branch v0.6.3 https://github.com/osqp/osqp.git

--depth 1只拉最新一版历史,省时间省磁盘。v0.6.3这个版本我在多个项目里用过,稳;如果你手头项目需要某个新特性,也可以直接clone master分支,编译流程完全一样。

如果没有Git,就在仓库页面选对应tag,下载zip包解压,效果相同。

2.3 目录结构与关键文件

源码解压后长这样:

osqp/ ├── include/ # 公共头文件,核心是 osqp.h ├── src/ # 求解器实现代码 ├── cmake/ # CMake辅助模块 ├── tests/ # 单元测试 ├── examples/ # C语言示例 ├── CMakeLists.txt └── README.md

重点记两个地方:include/osqp.h是唯一的公共头文件,所有API声明、数据结构定义都在里面,后续你的工程只要包含这一个目录就行;examples/里有现成的求解示例,第一次接触API时是最好的参考读物。tests/需要额外拉Catch2依赖,日常编译建议关掉,后面会说怎么关。

3. CMake生成工程与编译全过程

3.1 生成VS2019解决方案

在源码根目录下新建一个build目录,把CMake的中间产物和最终库文件都隔离在里面,这样源码目录保持干净,想换配置删掉build重建就行:

cd osqp mkdir build cd build cmake .. -G "Visual Studio 16 2019" -A x64

这行命令干了两件事:-G "Visual Studio 16 2019"告诉CMake生成VS2019格式的解决方案(不是Makefile),-A x64指定目标平台是64位。

然后编译Release版本:

cmake --build . --config Release

VS2019的生成器是多配置生成器(multi-config),编译时用--config Release显式指定配置。这点和Linux下完全不同——Linux下用-DCMAKE_BUILD_TYPE=Release在配置阶段就定死了,Windows下配置阶段的CMAKE_BUILD_TYPE是不生效的,必须在构建阶段选。如果直接双击生成的osqp.sln在IDE里编译,也要记得把顶部的配置从Debug切到Release。

3.2 常用的CMake开关

OSQP的CMake配置有几个开关值得提前了解,我用得最多的是这组参数:

cmake .. -G "Visual Studio 16 2019" -A x64 ^ -DBUILD_SHARED_LIBS=OFF ^ -DOSQP_EXAMPLES=OFF ^ -DOSQP_TESTING=OFF
选项默认值作用
BUILD_SHARED_LIBSOFF关掉生成静态库,打开则生成osqp.dll动态库
ENABLE_OPENMPOFF开启后利用OpenMP并行加速,需VS支持
ENABLE_MKL_PARDISOOFF用Intel MKL的PARDISO做后端线性求解器,需额外装MKL
OSQP_EXAMPLESON编译examples示例,不需要可以关
OSQP_TESTINGON编译单元测试,依赖Catch2,日常强烈建议关掉

大部分场景用BUILD_SHARED_LIBS=OFF的静态库就够了,部署时不需要带dll。如果追求极致性能且机器上有Intel MKL,可以研究ENABLE_MKL_PARDISO,它对病态问题有明显加速,但前提是你能接受MKL的依赖体积。

3.3 编译产物在哪里

编译完成后,产物默认输出在build/out目录下。VS多配置构建有时会再按Release/Debug分一层子目录,不同版本行为略有差异,找不到的话在build目录下直接搜索*.lib最靠谱。

静态库模式下你最终会得到:

文件说明
osqp.libRelease版静态库
osqpd.libDebug版静态库(多了一个d后缀)
OSQPConfig.cmakeCMake包配置文件,后续find_package要用

动态库模式下则多出osqp.dll、osqp.exp和导入库osqp.lib。我一般只用静态库,省去dll拷贝环节。

3.4 初次编译最容易翻车的三个点

说几个我亲眼见过、自己也踩过的坑:

第一,忘记-A x64。生成器默认平台有时候是Win32,编出来一个32位库,你外面64位的工程一链接,报LNK1112(计算机类型冲突),查半天才反应过来是架构不匹配。这属于最冤的一类错误,所以每次敲cmake命令都养成检查-A x64的习惯。

第二,开关开太多。OSQP_TESTING默认是开的,它会尝试去拉Catch2依赖,国内网络环境下经常卡住。只要不是想跑官方测试,尽量关掉。

第三,在普通cmd里跑cmake。如果MSVC环境变量没配好,cmake生成工程时可能找不到编译器,报"CMAKE_C_COMPILER not set"。用VS自带的"x64 Native Tools Command Prompt"就完全没有这个问题。当然,如果你在CMake GUI里选对了编译器路径,普通终端也没事,但我懒得折腾。

4. 在自己的C/C++工程里接入OSQP

4.1 方案A:VS属性页直接配置

如果你是在VS2019里建了一个普通控制台工程,不想动CMake,那就用属性页配置。

在解决方案资源管理器里右键项目 → 属性,依次设置:

  1. C/C++ → 常规 → 附加包含目录:加上D:\dev\libs\osqp\include(换成你的实际路径)
  2. 链接器 → 常规 → 附加库目录:加上D:\dev\libs\osqp\build\out\Release
  3. 链接器 → 输入 → 附加依赖项:填osqp.lib

然后在代码里直接#include "osqp.h"就能用。如果你写的是.cpp文件,也不用担心C/C++混链的问题——OSQP的osqp.h内部已经用extern "C"包好了,C++工程直接调用。

4.2 方案B:CMake的find_package

如果你的工程本身就是CMake管理的,用find_package更优雅。这里我踩过一个小坑:不同版本OSQP导出的CMake目标名不完全一致,有的版本叫osqp,有的版本叫OSQP::osqp。最稳妥的办法是打开build/out/OSQPConfig.cmake看一眼里面定义了哪个目标。大多数情况下这样写没什么问题:

cmake_minimum_required(VERSION 3.15) project(osqp_demo C CXX) find_package(OSQP REQUIRED) add_executable(osqp_demo main.cpp) target_link_libraries(osqp_demo PRIVATE osqp)

配置时指定OSQP包的位置:

cmake .. -Dosqp_DIR=D:/dev/libs/osqp/build/out

osqp_DIR指向包含OSQPConfig.cmake的目录。find_package找到后会自动带上头文件路径和库路径,比手工配置省心得多。

4.3 运行库匹配是链接成败的生死线

这是Windows下链接静态库最经典也最恶心的坑——MSVC运行库设置必须一致。

MSVC的C/C++运行库有四种模式:/MT(静态多线程)、/MTd(静态多线程调试)、/MD(动态多线程)、/MDd(动态多线程调试)。VS2019里Release默认是/MD,Debug默认是/MDd。

如果OSQP是用/MD编出来的,你的工程却用了/MT,链接时就会报LNK2038:RuntimeLibrary不匹配。解法有两个:

  1. 改你的工程,把运行库调成和OSQP一致的/MD(Release)或/MDd(Debug)。
  2. 重新编OSQP,在cmake配置时加-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded(对应/MT)或MultiThreadedDLL(对应/MD)。

我的建议是:如果OSQP不是性能瓶颈,就让它和你的主工程统一成/MD;如果OSQP要做极致性能优化想用/MT,那就提前把所有依赖库全部用/MT编一遍。混合CRT在简单场景下可能跑得欢,一旦涉及跨模块malloc/free或异常处理,崩溃随机出现,非常难查。

5. 写一个能验算的QP求解Demo

5.1 问题设计:一眼能看懂解

先选一道不需要计算器就能验算的题:

minimize 1/2(x₁² + x₂²) subject to x₁ + x₂ = 1

用拉格朗日乘子法手算:目标函数对x₁和x₂求导等于零得到x₁ = λ、x₂ = λ,代入等式约束得2λ = 1,所以x₁ = x₂ = 0.5,最优目标值0.25。这个结果干净利落,正好用来验证OSQP有没有算对。

对应标准形式:P是2×2单位阵,q是零向量,A是[1, 1],l和u都等于1(等式约束)。

5.2 CSC矩阵存储:把稀疏结构写对

OSQP使用的矩阵存储格式是CSC(压缩列存储),理解它是一切的基础。CSC由三个数组组成:

  • p:列指针数组,长度n+1。第j列的元素在后续数组中的范围是p[j]到p[j+1]-1。
  • i:行号数组,长度nnz,记录每个非零元素所在的行。
  • x:数值数组,长度nnz,按列存放非零元素。

关键点:P矩阵只存上三角部分。OSQP假设P是对称矩阵,只读上三角,你如果把下三角也塞进去,它要么报错要么算错,这是新手最容易踩的。

对单位阵P(2×2):

  • 第0列:只有一个非零元,位于行0,所以i[0]=0, x[0]=1
  • 第1列:只有一个非零元,位于行1,所以i[1]=1, x[1]=1
  • 列指针:p[0]=0, p[1]=1, p[2]=2

对A=[1, 1](一行两列):

  • 第0列:非零元在行0,i[0]=0, x[0]=1
  • 第1列:非零元在行0,i[1]=0, x[1]=1
  • 列指针:p[0]=0, p[1]=1, p[2]=2

注意A两列的行号都是0,因为约束矩阵只有一行。

5.3 完整求解代码与输出

完整代码贴在下面,我直接在注释里标注了每一步的作用:

#include <stdio.h> #include "osqp.h" int main(void) { c_int n = 2; /* 变量个数 */ c_int m = 1; /* 约束个数 */ /* P = I (上三角CSC存储) */ c_float P_x[2] = {1.0, 1.0}; /* 非零数值 */ c_int P_i[2] = {0, 1}; /* 行号 */ c_int P_p[3] = {0, 1, 2}; /* 列指针 */ /* A = [1, 1] */ c_float A_x[2] = {1.0, 1.0}; c_int A_i[2] = {0, 0}; c_int A_p[3] = {0, 1, 2}; /* 组装csc结构体 */ csc P; P.nzmax = 2; P.m = n; P.n = n; P.p = P_p; P.i = P_i; P.x = P_x; csc A; A.nzmax = 2; A.m = m; A.n = n; A.p = A_p; A.i = A_i; A.x = A_x; /* 目标一次项和约束边界 */ c_float q[2] = {0.0, 0.0}; c_float l[1] = {1.0}; c_float u[1] = {1.0}; /* 组装OSQP数据 */ OSQPData data; data.n = n; data.m = m; data.P = &P; data.q = q; data.A = &A; data.l = l; data.u = u; /* 默认设置,只把打印打开 */ OSQPSettings settings; osqp_set_default_settings(&settings); settings.verbose = 1; /* 建模并求解 */ OSQPWorkspace *work = NULL; c_int ret = osqp_setup(&work, &data, &settings); if (ret != 0) { fprintf(stderr, "osqp_setup failed, code %d\n", (int)ret); return 1; } osqp_solve(work); /* 结果输出 */ if (work->info->status_val == OSQP_SOLVED) { printf("status: %s\n", work->info->status); printf("x = [%.6f, %.6f]\n", work->solution->x[0], work->solution->x[1]); printf("objective = %.6f\n", work->info->obj_val); } else { printf("not solved: %s\n", work->info->status); } osqp_cleanup(work); return 0; }

编译链接跑起来,输出末尾应该能看到:

status: solved x = [0.500000, 0.500000] objective = 0.250000

和手算结果完全一致。第一次跑通这个Demo,后续换自己的问题就只是换P、q、A、l、u的问题。

5.4 状态码与失败排查

求解完成后,work->info->status会给出字符串状态,work->info->status_val给出对应的数值,需要关注这几种:

status_val含义应对
OSQP_SOLVED (1)收敛到最优解直接用结果
OSQP_SOLVED_INACCURATE (2)解出来了但不满足精度收紧eps或打开polish
OSQP_PRIMAL_INFEASIBLE (3)原问题无可行解检查约束是否矛盾
OSQP_DUAL_INFEASIBLE (4)对偶不可行,原问题无界或松弛检查P是否半正定
OSQP_MAX_ITER_REACHED (-2)迭代次数用尽增大max_iter、调rho

我在一开始就栽过"明明数学上无解却期待有解"的坑——约束条件本身矛盾时OSQP会诚实地报PRIMAL_INFEASIBLE。遇到这种情况先回头检查l和u有没有设反,A的行号列号有没有对应错,多半是建模问题。

6. 工程化使用中常见的进阶问题

6.1 矩阵构造比求解更贵

很多人以为用OSQP的瓶颈在求解器本身,用多了才发现,把业务数据转换成CSC格式的过程往往才是耗时大户。尤其做MPC这类滚动优化,每步都要构造一次P和A,如果每次都用malloc重新分配再逐个填非零元,光矩阵组装就能吃掉大半预算。

我的做法是:确定问题规模后,一次性分配好CSC三个数组的最大空间,后续每步只更新数值数组x,行号i和列指针p基本不变(除非约束结构动态变化)。配合OSQP提供的更新接口,能省掉整个osqp_setup的开销:

  • osqp_update_lin_cost(work, q_new):只更新目标一次项
  • osqp_update_bounds(work, l_new, u_new):只更新约束边界
  • osqp_update_A(work, A_new_x, OSQP_NULL, 0):只更新A的数值
  • osqp_update_P(work, P_new_x, OSQP_NULL, 0):只更新P的数值

这些接口比每次重建workspace快一个量级,是我实际优化效果最明显的一处。

6.2 设置项调参与warm start

OSQP默认设置对大多数问题够用,但碰到收敛慢或精度不足的情况,重点看这几个参数:

参数默认值说明
rho0.1ADMM步长,直接影响收敛速度
sigma1e-6增广拉格朗日正则项
alpha1.6松弛因子
max_iter4000最大迭代次数
eps_abs1e-3绝对终止容差
eps_rel1e-3相对终止容差
polish0是否在迭代后做抛光,能显著提升精度

实践经验:如果追求高精度解,把polish设为1通常比单纯把eps_abs、eps_rel调到1e-6更划算,抛光是额外解一次KKT系统,小规模问题开销不大。如果发现到了max_iter还没收敛,先别急着加大迭代次数,试着手动把rho调大或调小一个数量级,经常立竿见影。

warm start是MPC场景的杀手锏。OSQP的workspace可以在一次求解后直接复用,把上一时刻的解作为当前时刻的初值,迭代起点离最优解很近,收敛极快。我实际测试过,MPC滚动优化里启用warm start后单步求解耗时能降一半以上。这是OSQP这类ADMM求解器相对内点法的最大优势之一。

6.3 多实例、线程与内存边界

OSQP的workspace不是线程安全的,同一个workspace不能同时被两个线程调用来求解,否则迭代数据互相污染,结果一塌糊涂。但不同线程各自独立创建独立的workspace来求解是没问题的,因为OSQP本身没有全局状态。

内存方面有一个容易误解的点:osqp_setup内部会把问题数据复制进workspace,所以setup之后,你原来那个csc数组理论上可以释放。但我个人的习惯是让数据数组的生命周期覆盖到solve结束——少写一个free,多一份安心,尤其是在团队维护的项目里,别人不一定清楚这个拷贝语义。

记得善用osqp_cleanup释放workspace,只调osqp_solve不调osqp_cleanup的话,内存泄漏会随时间累积,长驻服务上特别明显。

6.4 从C到其他语言的扩展路径

OSQP编出来的库不只服务C/C++。我见过有人把编译好的osqp.dll用ctypes在Python里直接调,绕开Python包装层的开销;也有人通过C接口把OSQP嵌进实时控制器的C代码里,配合共享内存和外部仿真环境做闭环。

如果后续要接入Eigen这类矩阵库,可以借助Eigen的SparseMatrix直接把innerIndexPtr()、outerIndexPtr()和valuePtr()指针喂给OSQP的csc结构体,零拷贝转换,非常干净。将来要是换了VS2022,CMake的generator字符串改成"Visual Studio 17 2022",其余流程一字不改。

我在实际项目里用OSQP做过一个带约束的轨迹规划Demo,最开始每次求解都重建矩阵、重建workspace,单步要七八毫秒;后来改成warm start加只更新q和边界,直接降到一两毫秒。如果你也是拿它跑MPC这类滚动优化,这条经验比任何编译技巧都值钱。编译那点事,跟着这篇踩一遍,之后换版本、换平台基本就是复制粘贴命令的问题了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询