Linux 下这条命令比 Windows 多一个字母,多的那个字母省掉一半代码。
但真正踩坑的地方不在生成、在链接——.c 与 .cpp 混编时符号名会悄悄变短。
08.08.06 已经把 onvif.h 做出来了,这一节把它变成能编的 .c 与 .h。
08.01.06 是同一件事的 Windows 版:
命令少一个 -l,链接方式也不同——两节的命令可以直接对照着看。
一、一条命令,五个参数
整条命令就是下面这一行,五个参数各管一件事:
soapcpp2 -2 -c -x -l -I import onvif.h
| 参数 | 作用 | 为什么 |
|---|---|---|
-2 |
SOAP 1.2 绑定 | ONVIF 只认 1.2 |
-c |
生成纯 C | 服务端常是 C |
-x |
不生成 XML 注释 | 文件小一圈 |
-I import |
找 #import 的头 |
wsse.h 在那里 |
-l |
只生成客户端 | 省一半代码 |
第二行那个 -2 是这一节唯一不能商量的参数:省了它,soapcpp2 默认生成 SOAP 1.1 绑定,
而 ONVIF 的规范从头到尾只写 1.2。
第四行那个 -I 后面跟的是目录名 import,
不是文件名——写成 -I import/wsse.h 会报错,
因为 soapcpp2 会把它当目录去里面找 wsse.h。
二、-2 省了会怎样
SOAP 1.1 与 1.2 的信封命名空间不一样,这是最硬的一条差别:
| 版本 | 信封命名空间 | ONVIF |
|---|---|---|
| 1.1 | schemas.xmlsoap.org 系 |
不用 |
| 1.2 | www.w3.org/2003/05 系 |
只用这个 |
第一列那个 1.1 的完整地址是:http://schemas.xmlsoap.org/soap/envelope/,
1.2 的是 http://www.w3.org/2003/05/soap-envelope——
两个地址里连 soap 这个词的位置都不一样。
生成成 1.1 之后,程序能编能跑,只是设备不认:有的回 HTTP 400,有的回一个空的 SOAP Fault,
还有的干脆不回。
三种表现都不像「版本不对」,
所以这个坑经常要查半天——查法只有一个:把发出去的报文抓下来看第一行。
三、-l 是我建议加的
书上没写这个参数,因为书上要同时演示服务端。 只做客户端时加上它,好处很实在:
不加 -l |
加了 -l |
|---|---|
生成 soapServer.cpp |
不生成 |
| 带服务端分派样板 | 没有 |
| 文件多两三个 | 少两三个 |
第二行那个「服务端分派样板」才是主要收益:服务端那套是「收到请求 → 查表 → 调你的函数」,
对客户端完全没用,但会占满半个 soapStub.h。
代价也要说清:加了 -l 就只出客户端,
以后要在这台机器上做服务端,得把命令改回来重新生成一遍。
所以生成之前先想清楚「这台机器是客户端还是服务端」——这一句话能省掉一次返工。
四、生成出来怎么链接
这一步 Linux 与 Windows 差得最远:
| 场景 | 链接命令 | 要点 |
|---|---|---|
| 纯 C 客户端 | gcc *.c -lgsoap |
要加 ssl |
| C++ 客户端 | g++ *.cpp -lgsoap++ |
库名多两个加号 |
| 静态链接 | gcc *.c libgsoap.a |
体积大 |
第一行那个 ssl 指的是 -lssl -lcrypto,它来自 configure --enable-ssl:
编译 gSOAP 时没开 ssl,这里就链接不到 soap_wsse_*。
而错误信息只会说「未定义引用」,
不会告诉你「去重新 configure」——这就是 08.08.01 强调「装依赖」的原因。
第二行那个加号也是真事:C++ 版的库文件名就是 libgsoap++.a,
-l 后面写 gsoap++,少一个加号就说找不到。
五、.c 与 .cpp 不能混着编
这是 Linux 侧独有的坑,Windows 上被 VS 挡住了:
| 文件 | 用什么编 | 原因 |
|---|---|---|
soapC.c |
gcc |
它是纯 C |
main.cpp |
g++ |
我自己的代码 |
| 链接 | g++ |
它才认 C++ 符号 |
第三行是关键:链接这一步必须用 g++,因为只有它会把 C++ 标准库也带进来。
但 g++ 链接时会做 C++ 的名字改编(name mangling)——它把函数名、参数类型编进符号名里,
而 gcc 编出来的 soapC.o 里符号是「原名」。
两边一对,全是「找不到符号」,
而且报错里的符号名看起来「少了几个字符」——那不是名字被截断,是 C++ 那边的名字多了修饰。
解法一行:
#ifdef __cplusplus
extern "C" {
#endif
#include "soapH.h"
#ifdef __cplusplus
}
#endif
这七行是「让 C++ 编译器按 C 的规矩找符号」,
soapH.h 自己的声明没变,变的是 main.cpp 这边怎么理解它。
顺序不能反:extern "C" 必须包在 #include 外面,
包在里面无效——因为头文件已经展开完了。
六、哪些文件生成后别动
soapcpp2 的产物分两类,混了就会白改:
| 文件 | 类别 | 能不能改 |
|---|---|---|
soapStub.h |
生成物 | 改了会丢 |
soapH.h |
生成物 | 改了会丢 |
soapC.c |
生成物 | 改了会丢 |
main.cpp |
自己写 | 随便改 |
判据很简单:文件名以 soap 开头的都是生成物,你自己的代码不要叫 soap 开头的名字。
这条规矩看着像强迫症,其实是保命的:
改完 onvif.h 重新生成时,soapcpp2 会直接覆盖同名文件,不提示、不备份。
所以「生成物」和「手写代码」必须分目录放,
或者至少靠文件名前缀分开——前者随时可删,后者一个字都不能丢。
七、动手 Demo:Linux 下 soapcpp2 的参数多一个
本机没有 soapcpp2(08.08.01 的探针已经证明),
所以 Demo 只做能验证的那一半:把命令拼出来、把参数数清楚。
参数清单与链接方式是真事实,-l 这一点是我加的、书上没有——这条差别在输出里明确标了。
import shutil
print("soapcpp2:", shutil.which("soapcpp2"))
args = ["-2", "-c", "-x", "-l", "-I import"]
print("参数个数:", len(args))
print("完整命令: soapcpp2 %s onvif.h" % " ".join(args))
实跑输出:
--------------------------------------------------------------------
Linux 下 soapcpp2 的参数多两个
--------------------------------------------------------------------
**Linux 版和 Windows 版的 `soapcpp2` 是同一個程序**,
**差别只在「生成的代码怎么链接」**:
参数 作用 为什么要它
-------------------------------------------------------------------
`-2` SOAP 1.2 绑定 **ONVIF 只认 1.2**
`-c` 生成 C 服务端常是纯 C
`-x` 不生成 XML 注释 文件小一圈
`-I import` 找 `#import` 的头 **`wsse.h` 在那里**
**`-l`** **只生成客户端** **服务端用不上,省一半代码**
**第五行是我建议加的**,书上没写:
**只做客户端就别生成 `soapServer.cpp`**,
**省下的不只是体积,还有「服务端那套回调初始化」的样板代码。**
**代价是以后要加服务端得重新生成**——
**所以这一步要先想清楚「这台机器是客户端还是服务端」。**
**生成之后编译命令也不一样**:
场景 编译命令 说明
--------------------------------------------------------------------------
纯 C 客户端 `gcc *.c -lgsoap -lssl -lcrypto` **要带 ssl**
C++ 客户端 `g++ *.cpp -lgsoap++ -lssl -lcrypto` 库名多两个加号
静态链接 `gcc *.c libgsoap.a -lssl` 部署省事,体积大
**第一行那个 `-lssl -lcrypto` 是 `--enable-ssl` 的后续**:
**编译时没开 ssl,这里就链接不到 `soap_wsse_*`**,
**而错误信息只会说「未定义引用」,不会告诉你「重新 configure」。**
**还有一条 Linux 独有的坑**:**`.c` 和 `.cpp` 不能混着编**。
**`soapC.c` 用 gcc 编,`main.cpp` 用 g++ 编,最后一起链接**——
**但 g++ 链接时会做 C++ 的名字改编(name mangling)**,
**所以 `soapH.h` 里那些函数声明必须包在 `extern "C" { }` 里**,
**否则链接期全是「找不到符号」,而符号名看起来还少了几个字符。**
第一行那个 None 就是这一节的门槛:没有 soapcpp2,后面所有步骤都做不了。
而 soapcpp2 不是单独装的,
它是编 gSOAP 时一起编出来的(08.08.03)——
所以「装依赖 → 编 gSOAP → 才有 soapcpp2」是一条链。
输出里那个「五个参数」正好对上表里的五行:多一个少一个都编不出能用的代码,
但只有 -2 是「错了就不工作」,其余四个错了只是「费事」。
最后那条链接命令里的 ssl 要记住:-lssl -lcrypto 漏了,报错是「未定义引用」,
而它真正的病因在 configure 那一步——隔了十几分钟,很容易忘了回头查。
30 秒实验
- 把
-2从命令里去掉,猜一下生成的信封命名空间会变成哪个。 - 用
nm soapC.o | grep soap_wsse看库里的符号名有没有下划线以外的修饰。 - 把
extern "C"那七行去掉再编一次,看报错里的符号名差多少字符。
常见误区
- 以为
-I后面要写完整文件路径。 它要的是目录, 写成文件会在该「目录」里找不到wsse.h。 - 用
gcc链接含.cpp的工程。 C++ 符号要g++才认,gcc链接会一片未定义引用。 - 直接改
soapStub.h加自己的声明。 重新生成就被覆盖,自己的东西要放进单独的头文件。
自测三问(点开对答案)
1. 为什么 `-2` 是五个参数里唯一不能省的? —— **ONVIF 规范只定义 SOAP 1.2 绑定。** 生成 1.1 之后程序能跑,但设备回 400 或空 Fault, 表现都不像「版本不对」。 2. `g++` 链接时「找不到符号」,为什么符号名看起来变短了? —— **那是 C++ 的名字改编**:`g++` 编译的文件里符号带类型修饰,和 `gcc` 编出来的原名对不上。 用 `extern "C"` 包住头文件。 3. 加了 `-l` 之后有什么代价? —— **只出客户端,没有服务端代码**。以后要在这台机器上做服务端,得去掉 `-l` 重新生成一遍。下一节把「还要从 gSOAP 源码里拷哪些文件」列成清单——漏一个,链接期就炸。
- ☐ (本节完,下一节 08.08.08 复制其他文件)