使用 C 突破 VB6 的字符串长度限制

来源:互联网 发布:淘宝网商贷款 编辑:程序博客网 时间:2024/06/05 23:42
VB6 代码是必需要维护的“遗产”,但公司之前使用 VB 开发的产品一直都在不定期地蹦出“长期不可解”的 Bug,维护人员捣浆糊和客户的抱怨成为经年的常态。最近痛下决心欲把公司之前使用 VB 开发的所谓的“中间件”或者“中间层”的组件服务以及蹩脚的 Socket 通讯服务换成 webservice ,期望能够解决大部分的问题。但是目前还没有计划把客户端的 VB 代码替换掉,于是使用 C 开发了与 webservice 交互的 DLL 。不再打算开发 COM 组件来让 VB 调用了,版本兼容带来的问题一直都是客户和技术人员都很沮丧的真实体验。经过良好开发和测试的 DLL 很容易替换原来存在缺陷的 DLL ,所以相比之下普通的 DLL 反而显得更容易维护。

使用 DLL 而不使用 COM 带来的最直接的技术问题就是 VB6 处理字符串的方式。在最近针对 VB6 的字符串操作的研究中竟意外地发现即使 BSTR 支持理论上 4GB 的字符串长度,但在 VB6 中实际只能处理不超过 32KB 的字符串长度,准确地说,在 VB6 中能够直接进行处理的字符串的字节长度限制为 64K-1,对应的字符个数为 32K-1。进而挖出了一些 Bug 的原因:试图拼接一个较长的字符串时失败。不知道为什么会有这样的限制,大概在 VB6 刚推出的时候比尔认为 32K 个字符已经够用了。这其实并不新鲜,但多年来公司的程序员们可能并没有太多地意识到这个限制,从遗留下来的代码就可以看出来。说不准若干年后的技术员为了 4G 的限制而苦恼呢。

为了能够让 VB 与 webservice 交互,首先要解决的就是这个 32K 的长度限制,向 webservice 提交一个超过 32K 的请求是最正常不过的事情了。使用 C 语言可以帮助 VB 克服这个困难:可以很方便地使用 C 语言分配一个远超出 64K 的连续空间,把这个空间的指针当作字符串给 VB 使用是有可能的。当使用 COM 时,可以使用 BSTR (其实 BSTR 也是一种效率不高的字符串使用方式)。当不使用 COM 时,其实也可以按 VB 字符串的约定来向 VB 提供字符串,方法就是当给 VB 的字符串分配空间时,多分配 6 个字节。具体地,假如要给 VB 分配一个 N 字节的空间来容纳某个字符串,则需要分配的内存空间为:4 + N + 2 字节,开始的 4 个字节用来放字符串的字节长度(即 N 的值),结尾的 2 个字节填 0,然后把这个内存空间偏移 4 个字节,即 N 个字节开始的地址返回,VB 就可以直接使用这个内容为 N 个字节的“字符串”了。

下面给出一个简单的 C 语言向 VB 提供字符串的代码实例,使得 C 与 VB 进行交互变得简单有效。有兴趣的读者可以进一步改善这个 C 类,使得更有效率,功能也更加全面(如果在项目中需要实际使用的话)。

// C 程序:
// 给 VB 使用的字符串的类型
// 被定义成 void 是出于防止 C 程序员对它进行可能不正确的字符串操作。
typedef void zigstr_t;

// 简单地给 VB 分配一个指定大小的字符串(私有函数,也是关键所在)
zigstr_t * _zigstr_alloc(size_t size) {
unsigned char* data = NULL;
if(!size) return NULL;
data = (unsigned char*) malloc(size + 6); // 多分配 6 个字节
if(!data) return NULL; // 没有内存了
memcpy(data, &size, 4); // 设置字符串的前置长度标志
data += 4; // 偏移 4 个字节,指向“字符串”的内容
data[size] = 0;
data[size+1] = 0; // 设置结尾的双字节 0
return data; // 这个指针就是 VB 可以使用的字符串
}

// 释放资源
void __stdcall zigstr_free(zigstr_t* str) {
unsigned char* start = NULL;
if(!str) return; // 字符串 NULL
start = (unsigned char*)str;
start -= 4; // 调整 4 个字节的偏移,指向最初分配时的位置
free(start); // 释放这个“字符串”
}

// 根据给定的内容创建一个 str
zigstr_t* __stdcall zigstr_create(const void* content, size_t size) {
zigstr_t* str = NULL;
if(!content || !size) return NULL;
if(! (str = _zigstr_alloc(size)) ) return NULL;
memcpy(str, content, size);
return str;
}

// 简单地追加字符串
// 如果需要处理很长很长的字符串,可以把 zigstr 类写得更复杂些,
// 通过减少内存的分配和复制来提高效率
zigstr_t* __stdcall zigstr_concat(const zigstr_t* str1, const zigstr_t* str2) {
zigstr_t* str = NULL;
size_t size1 = 0, size2 = 0;
if(str1) size1 = *((size_t*)str1 - 1); // 取得第一个字符串的长度
if(str2) size2 = *((size_t*)str2 - 1); // 取得第二个字符串的长度

if( !(str = _zigstr_alloc(size1 + size2)) ) // 分配空间
return NULL;
if(str1) memcpy(str, str1, size1); // 复制第一个字符串
if(str2) memcpy((unsigned char*)str+size1, str2, size2); // 复制第二个
return str;
}

// 得到字符串的长度
// 这比通过遍历得出长度要高效,但必须保证设置了前置的长度标志。
size_t __stdcall zigstr_size(zigstr_t* str) {
if(!str) return 0;
return *((size_t*)str - 1);
}

把上面的几个简单的函数(create, free, concat, size)导出到 DLL 中,就可以给 VB 使用理论上最长可达 4G-7 个字节的字符串了。因为 size_t 的最大值为 4G-1,另外需要 6 个字节的额外开支,所以最长可达 4G-7 个字节。如果用来存放 VB 的 Unicode 内码,也可以放 2G-4 个字符了,目前在通常情况下不会向 webservice 发送这么大容量的请求,也不大会收到这么大容量的响应。下面给出 VB6 使用这个字符串的方法:

' 定义一个 VB6 类的代码:ZigStr.cls
'
Option Explicit

Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Long, Source As Long, ByVal Length As Long)
Private Declare Function zigstr_create Lib "zigstr.dll" _
(ByVal hContent As Long, ByVal nSize As Long) As Long
Private Declare Sub zigstr_free Lib "zigstr.dll" (ByVal hStr As Long)
Private Declare Function zigstr_concat Lib "zigstr.dll" _
(ByVal hStr1 As Long, ByVal hStr2 As Long) As Long
Private Declare Function zigstr_size Lib "zigstr.dll" _
(ByVal hStr As Long) As Long

' 存放字符串句柄
' 所谓句柄,实际上就是C中的指针
Private m_hStr As Long

' 释放字符串空间
Private Sub Class_Terminate()
If m_hStr <> 0 Then zigstr_free m_hStr
End Sub

' 使用句柄追加字符串
Private Sub append_by_handler(ByVal hText As Long)
Dim hStr As Long
If hText = 0 Then Exit Sub
If m_hStr = 0 Then
' 如果 m_hStr = 0 则创建一个字符串
m_hStr = zigstr_create(hText, zigstr_size(hText))
If m_hStr = 0 Then
err.Raise vbObjectError + 1, "ZigStr.append_by_handler", "内存不足"
End If
Else
' 如果原来已经有了一个字符串,则追加字符串
hStr = zigstr_concat(m_hStr, hText)
If m_hStr = 0 Then
err.Raise vbObjectError + 1, "ZigStr.append_by_handler", "内存不足"
End If
' 释放原来的字符串
zigstr_free m_hStr
' 记住新的字符串
m_hStr = hStr
End If
End Sub

' 追加 VB 字符串
Public Sub AppendStr(ByRef Text As String)
' 使用 StrPtr() 取得 VB 字符串对象的字符串句柄
append_by_handler StrPtr(Text)
End Sub

' 追加 ZigStr 字符串
Public Sub AppendZigStr(ByRef zigText As ZigStr)
append_by_handler zigText.m_hStr
End Sub

' 取得字符串的字节长度
Public Property Get Size() As Long
Size = zigstr_size(m_hStr)
End Property

' 取得字符串的内容
Public Property Get Text() As String
Dim sText As String, hText_temp As Long
If m_hStr = 0 Then Text = ""

' 把句柄作为字符串指针直接赋给 vb 字符串变量,实现快速复制
CopyMemory ByVal VarPtr(sText), m_hStr, 4

' 把结果返回(VB还需要再把字符串的内容完整地复制一遍)
Text = sText

' 清除 vb 字符串变量,这样可以阻止 vb 回收这个字符串的内存
CopyMemory ByVal VarPtr(sText), 0, 4
End Property

以下是对这个 VB 类的测试,验证了 C 与 VB 字符串的互操作性和对 VB 字符串长度的突破:

Dim zstr As New ZigStr
zstr.AppendStr "hello world! "
zstr.AppendStr "先生您好!"
Debug.Print zstr.Size, zstr.Text

Dim i As Integer, s As String
s = Space(1024) ' 如果直接创建 Space(64 * 1024) 会报“溢出”错误
Set zstr = New ZigStr
For i = 1 To 64 ' 通过这个循环演示了在 VB 中操作长字符串的可能性
zstr.AppendStr s
Next
' 注意到 VB 的某些字符串操作仍然有效,如 Trim()
Debug.Print zstr.Size, "|" & Trim(zstr.Text) & "|"

顺便说一下,VB 中默认使用 Unicode (UTF-16LE) 字符编码格式,如果需要在 C 中使用传统的字符串函数进行处理(Multi-byte String functions),则需要使用 MultiByteToWideChar/WideCharToMultiByte 或者 mbstowcs/wcstombs 等函数进行转换。