wsdl2h 只干一件事:WSDL/XSD 里的类型翻成 C/C++ 的 struct 和函数原型。
它的产物顶部有几行 //gsoap 注释,那不是废话,是给 soapcpp2 看的编译指令。
上一节讲第二步 soapcpp2,这一节回到第一步 wsdl2h。
//gsoap 指令在这个文件里出现,看起来像注释,实际决定生成的代码长什么样——
这是 gSOAP 最容易被忽略的设计。
一、类型映射:XSD → C
| XSD 类型 | C 类型 | 要注意什么 |
|---|---|---|
xs:string |
char * |
要自己管内存,gSOAP 有专用分配函数 |
xs:int |
int |
32 位,和平台无关 |
xs:dateTime |
time_t |
gSOAP 有专用解析器,别自己写 |
xs:boolean |
bool |
true/1 都认 |
xs:float |
float |
注意精度,ONVIF 里用得少 |
第一行是 gSOAP 项目最经典的内存泄漏源:char * 是裸指针,谁分配、谁释放没有类型系统保证。
gSOAP 的对策是「内存池」:soap 上下文里挂着一个链表,
soap_new_string 分配的东西在 soap_destroy 时统一回收。
所以别用 malloc,一律用 soap_* 系列函数。
第三行同理:xs:dateTime 是 ISO 8601 字符串(2024-05-01T12:00:00Z),
自己解析会漏掉时区前缀 Z 和小数秒,用 soap_s2dateTime 一行搞定。
二、实测:这份 WSDL 抽出 6 个带类型的元素
迷你 wsdl2h 把 types 段里所有 xs:element 连名字带类型抓出来:
| 元素名 | XSD 类型 | 翻成 C |
|---|---|---|
Manufacturer |
xs:string |
char * |
Model |
xs:string |
char * |
FirmwareVersion |
xs:string |
char * |
SerialNumber |
xs:string |
char * |
HardwareId |
xs:string |
char * |
UTCDateTime |
xs:dateTime |
time_t |
六个里有五个是 char *——这就是 ONVIF 结构体满屏指针的原因:设备信息几乎全是字符串。
唯一那个 time_t 出现在 GetSystemDateAndTime 的响应里,
而它恰好是最容易解析错的一个(时区、本地时间、UTC 三个字段容易搞混)。
三、//gsoap 指令:看起来像注释的配置
生成的 onvif.h 顶部通常长这样:
//gsoap ns service name: Device
//gsoap ns service style: document
//gsoap ns service encoding: literal
//gsoap ns schema namespace: urn:Device
四行全是注释,但 soapcpp2 会读它们。
| 指令 | 决定什么 | 改错的后果 |
|---|---|---|
service name |
生成的服务类名/文件名 | 生成的文件名全变,编译脚本要跟着改 |
service style |
document 还是 rpc | 和 ONVIF 规范不符,设备不认 |
service encoding |
literal 还是 encoded |
ONVIF 要 literal |
schema namespace |
生成的 ns__ 指向哪个 URI |
SOAPAction 对不上,全部 400 |
第四行是最致命的一行:它决定生成的代码里 ns 映射到哪个 URI,
而 ONVIF 要求 http://www.onvif.org/ver10/device/wsdl。
写错了,客户端发出去的 SOAPAction 就是错的,设备全部拒绝。
08.01.07 那一节叫「关联自己的命名空间」,讲的就是改这几行。
四、三个常用参数
| 参数 | 作用 | 什么时候用 |
|---|---|---|
-o out.h |
指定输出文件 | 总是要 |
-t typemap.dat |
指定类型映射文件 | 要自定义类型时(ONVIF 必改) |
-s |
不生成 STL 容器 | 纯 C 工程 |
第二行在 ONVIF 项目里是必选项:typemap.dat 里写着「哪个 XSD 类型映射到哪个 C 类型」,
ONVIF 的 WSDL 引用了大量 xsd:duration、xsd:hexBinary,
不配 typemap 生成出来一堆 _xsd__ 开头的怪名字。
08.01.03 专门有一节讲改 typemap.dat,说明这确实是绕不过去的一步。
第三行在嵌入式里很重要:-s 生成的代码不用 STL,但代价是字符串处理全得用 C 的方式,
代码会啰嗦不少。
五、动手 Demo:wsdl2h 就是把 WSDL 翻成 C 头文件
同样用迷你版:从 WSDL 里抽元素和类型,按映射表翻成 C 声明,
再拼出 gSOAP 风格的 //gsoap 指令头。
跑的都是真解析(ElementTree 读真文件),只有「生成 gSOAP 代码」这一步是模仿的。
XS2C = {"xs:string": "char *", "xs:int": "int",
"xs:dateTime": "time_t", "xs:boolean": "bool"}
# ① 遍历 types 段,把每个 xs:element 的名字和类型抓出来
for el in root.iter("{http://www.w3.org/2001/XMLSchema}element"):
t = el.get("type")
if t:
print(el.get("name"), t, XS2C.get(t, "?"))
# ② 拼 gSOAP 风格的指令头 + 结构体
print("//gsoap ns service name: Device")
print("//gsoap ns schema namespace: %s" % NS_T)
print("struct ns__GetDeviceInformationResponse {")
for name, t in fields:
print(" %s %s;" % (XS2C[t], name))
print("};")
实跑输出:
--------------------------------------------------------------------
wsdl2h 就是把 WSDL 翻成 C 头文件
--------------------------------------------------------------------
**`wsdl2h` 干的事很单一:WSDL/XSD 里的类型 → C/C++ 的 struct 和函数原型。**
这一节用迷你版走一遍,看它怎么翻。
**WSDL 里的类型 → C 类型,对照如下:**
XSD 类型 C 类型 说明
----------------------------------------------------------
`xs:string` `char *` 要自己管内存
`xs:int` `int` 32 位
`xs:dateTime` `time_t` gSOAP 有专用解析
`xs:boolean` `bool` `true`/`1` 都认
**实测:从这份 WSDL 抽出所有元素和它们的类型:**
元素名 XSD 类型 翻成 C
--------------------------------------------------------------
Manufacturer xs:string char *
Model xs:string char *
FirmwareVersion xs:string char *
SerialNumber xs:string char *
HardwareId xs:string char *
UTCDateTime xs:dateTime time_t
**一共 6 个带类型的元素**,翻成 C 之后就是 6 个字段。
**注意 `xs:string` 翻成 `char *` 是个陷阱**:**要自己 malloc、自己 free**,
gSOAP 用 `soap_new_string` 帮你管,**但忘了还是会漏。**
**迷你 wsdl2h 的输出(结构照 gSOAP 风格):**
`//gsoap ns service name: Device`
`//gsoap ns service style: document`
`struct ns__GetDeviceInformationResponse {`
char * Manufacturer;
char * Model;
char * FirmwareVersion;
`…`
`};`
**顶部那两行 `//gsoap` 注释不是废话**:它们是给 `soapcpp2` 看的编译指令,
**决定生成代码的服务名、风格、命名空间前缀。**
**改这三行,生成的代码就换一套命名**——这是 gSOAP 的「宏语言」。
**三个常用参数:**
参数 作用 什么时候用
------------------------------------------------------------------
`-o out.h` 指定输出文件 总是要
`-t x.h` 指定类型映射文件 要自定义类型时
`-s` 不生成 STL 容器 纯 C 工程
**第三个参数在嵌入式项目里很重要**:ONVIF 设备端经常是纯 C,**带 STL 的代码根本编不过去。**
XS2C.get(t, "?") 里的 ? 很重要:没见过的类型要显式暴露出来,
别默认当成 char *——那会把类型错误藏到运行期。
输出里那 6 个元素是 types 段里所有带 type 属性的 xs:element,
不含 GetDeviceInformation 这种「带内联 complexType」的顶层元素(它们自己没有 type 属性)。
所以「6 个带类型的元素」这句话是有口径的:指的是叶子字段,不是顶层请求/响应元素。
汇总的时候把口径说清楚,比数字本身更重要。
30 秒实验
- 给
XS2C加一条"xs:long": "long long",看新类型能不能翻出来。 - 把
schema namespace改成urn:Device,然后想一下客户端发出去的SOAPAction会变成什么。 - 把
-s(不生成 STL)的效果手工模拟一遍:把std::string换成char *,数要改几处。
常见误区
- 以为
//gsoap是注释、可以删。soapcpp2会读它,删了等于改了生成配置。 - 以为
xs:string翻成char *之后可以malloc。 要用soap_new_string这类池分配, 否则soap_destroy收不回去,必漏。 - 以为 typemap 是可选的。 ONVIF 的 WSDL 引用了大量特殊类型,
不改
typemap.dat生成出来一堆没法用的名字。
自测三问(点开对答案)
1. `xs:string` 为什么翻成 `char *` 是个陷阱? —— 因为**要自己管内存**。 gSOAP 用内存池(`soap_new_string`),**用 `malloc` 分配的东西它收不回去,就漏了。** 2. `//gsoap ns schema namespace` 写错会怎样? —— **生成的代码里 `ns` 指向错误的 URI**,客户端发出去的 `SOAPAction` 跟着错, **设备全部返回错误**。 3. `wsdl2h` 和 `soapcpp2` 的分工是什么? —— `wsdl2h`:**WSDL → 带指令的 C 头文件**(决定「有什么」)。 `soapcpp2`:**头文件 → 四个 .cpp/.h**(决定「怎么实现」)。下一节开始动手装工具:CentOS 上编 gSOAP 要装哪六样东西,以及「编过了但用不了」是怎么回事。
- ☐ (本节完,下一节 06.02.03 CentOS编译并测试gSOAP)