PowerBuilder中可以使用外部DLL来扩展程序功能。但在实际使用中,许多人并不了解如何 做好类型对应声明。类型声明错误,甚至调用错误,会导致隐藏bug,往往在多次调用后系统会崩溃而不自知。本文就DLL声明参数做一些分析,希望对一些使用者有一些引导作用。
注:以下所有内容均基于win32位系统。
一、从内存和C/C++指针说起
1、内存和越界
我们知道,对于计算机而言,不管是代码,还是数据,一切都必须在内存里。
我们可以把内存理解为连续的一个大的空间,这个大空间的基础单位是字节。例如1G内存就是1 * 1024 * 1024 * 1024 个字节,对于其中指定位置的内存,用一个数字标记,这个数字我们称之为指针。我们需要一块空闲内存时,向操作系统申请,操作系统会分配一块内存,并且返回这块内存的起始地址,这是一个数字,也就是指针。通常,我们不用关心这个指针的具体值 ,只管使用这个值 ,并且一定要知道分配出来的这块内存的大小,这个必须要清楚。内存用完后,要释放,还给操作系统。如果不释放,会内存耗尽,程序出错退出。
而对于PowerBuilder这样的高级语言来说,当然不需要我们自己去分配和释放内存,由语言本身自动完成。
理论知识总是枯燥无味的,下面具体举例来说明一下。
void *ptr = 0; //初始时,ptr 这个指针为0
ptr = malloc(10); //分配了10个字节的内存了空间,首地址返回给ptr,这时候ptr是一个数字,指向一块内存
memcpy(ptr,”012345678901234567890123456789”,10); //虽然给的” 012345678901234567890123456789”超过10个字节,但长度参数为10,所以,实际 只会复制10个字节到ptr指向的内存,一切正常。
memcpy(ptr,”012345678901234567890123456789”,20); //如果长度改为20,而分配的内存只有10个字节,此时还是一样成功,看不出来问题,但多出来的10个字节,也许复制到其他不可知的指针空间去了,而那个指针空间里面,可能是数据,可能是代码。如果正好是代码,那代码执行到这这部分时,必然会出错,程序会崩溃。通常我们称之为指针越界了。这也是指针让人胆战心惊的原因之一。
越界是PowerBuilder程序员常犯的错误。也许你认为:我没有分配内存啊,怎么越界了呢?那我举个例子说明:
假如有个医保接口函数声明如下:
Function long ybcall(string InJson,ref string OutJson) library “yb.dll”
调用时:
String ls_in_json,ls_out_json
Ls_in_json = json.tostring()
Ybcall(ls_in_json,ls_out_json)
这段代码有问题吗?当然有问题,程序运行也许一两次正常,但最终必然会崩溃。因为没有为ls_out_json分配内存,DLL内部是会有memcpy内存复制动作的,把结果复制到没有分配空间位置,而这个位置默认为0,0指向的是程序初始代码那些位置,必然会破坏系统内容,程序崩溃。
那我们把程序改成这样:
String ls_in_json,ls_out_json
Ls_in_json = json.tostring()
ls_out_json = space(1000)
Ybcall(ls_in_json,ls_out_json)
这时候有没有问题呢?答案是不知道。这里虽然给了1000个空格的空间,但返回的值长度是不是大于1000呢?只有写DLL的人知道。如果长度小于1000个字符,一切正常,如果长度大于1000个字符,那么后果就是未知,也许运行几次没问题,但随时会崩溃。
如何回避这个问题呢?
作为合格的C/C++程序员,在写DLL时,必然要解决这个问题。
方法一:文档约定必须要多少字符的空间。
例如如果约定长度不得小于800000,那么
ls_out_json = space(800000)
Ybcall(ls_in_json,ls_out_json)
这样就是安全调用。
方法二:ybcall实现判断。
这个函数应该重新设计成这样:
Function long ybcall(string InJson,ref string OutJson,long OutJsonLen) library “yb.dll”
Ybcall这个函数C++内部实现应该是这样:
Int len = strlen(szJson);
If(OutJsonLen < len)
return len;
memcpy(OutJson,szJson,len);
return len;
正确调用方法是:
Long ll_len
ll_len = Ybcall(ls_in_json,ls_out_json,0)
ls_out_json = space(ll_len)
ll_len = Ybcall(ls_in_json,ls_out_json,ll_len)
这样就可以确保不会内存越界。
2、指针与内存块
上面我们说了,指针指向内存中某个已分配的地址,它是一个字节的位置。实际使用中,内存使用是以内存块的方式出现的,内存块的大小是 1-n 个字节。指针总是指向内存块开头的那个字节的位置。至于这个内存块有多大,具体要看申请多少。而申请内存,有些是是明确申请大小,有些是隐含申请大小。
我们知道,PowerBuilder是32位应用程序。所以基本类型都是32位,一个字节是8位,32位就是4个字节。也就是说,基本类型都是32位,是4个字节。接下来,我们看看内存块是如何“玩魔术”的。
void *p = malloc(16); //申请16个字节
void是没有类型的,所以大小未知。那么我们可以给它一个类型。
char *p = malloc(16); //申请16个字节
p |
此时p指向这16个字节的第一个字节位置。
char *p1 = p + 1;
此时p1指向p的下一个字节指针。是这样的:
p | p1 |
我们改一下类型
int *p = (int *)malloc(16); //分配了16个字节
int *p1 = p + 1;
此时p1在哪呢?也许你理解的内存是这样的:
p | p1 |
但不是,实际上是这样的:
p | p1 |
为什么是这样呢?因为int是32位类型,也就是4个字节。这个由编译器直接管理了指向位置。
因为是分配了16个字节,16/4=4,所以,p指针的内存可以存4个int类型数据。
p[0] = 1;
p[1] = 1;
p[2] = 1;
p[3] = 1;
这样就是存放4个字节,如果p[4] = 1,那就不行了,越界了,程序就有可能崩溃。
对于其他数据类型,long 是4字节,short是2字节,int64是8字节,double是8字节,float是4字节。
例如:
Int64 *p = (int64 *)malloc(16);
Int64 *p1 = p + 1;
p | p1 |
对于int64是这样的,因为明确指定了使用64位,是32位的双倍长,所以是8个字节。在PowerBuilder里,int64就是longlong类型。
如果是一个结构,那这个结构就是它所有成员字节数的总和(这里忽略内存对齐的概念)。例如:
Struct strTest
{
Int a;
Long b;
}
这个结构的大小就是4+4=8
3、一个特殊的类型:字符串
通常我们说“字符串”,也就是由一串字符组成的一个内存块。在PowerBuilder里就是string类型,在c/c++里就是char *或wchar_t*,即字符指针指向的一块内存块。
大多数同学都接触过C语言,知道char,对wchar_t就比较陌生。那么,这个字符类型,它特殊在哪里呢?答案就是:字符集编码。
在windows操作系统里(linux系统里与windows系统不完全一样),有2种字符类型:多字符char和宽字符wchar_t,即ansi和unicode字符集编码。它们的区别是:char是一个字节,wchar_t是2个字节,所以wchart_t又可以用short表示。
例如“中国”这两个汉字,ansi编码它是char类型的4个字节4个字符:0xd6 0xd0 0xb9 0xfa ;unicode编码是wchar_t类型的4个字节2个字符0x4e2d 0x56fd。取字符串长度,ansy编码的结果是4,unicode编码的结果是2。
字符串是连续的字符内存空间,以0为结尾。Ansi是0x00占一个字节,unicode是0x0000占两个字节。
char 作为字符串类型,同时它是一个字节,所以它也可以代表连续的字节内存空间。往往伴随着还有个字节数表示空间大小。
PowerBuilder有众多版本,可以分为2个种字符集编码,9.0及以下版本是ansi字符集编码,内部使用的是char;10.0及以上版本是unicode,内部使用的是wchar_t类型。
常用的还有utf8编码,内部实际是以char串,即字节串来表示。Windows系统不能直接显示utf8编码,必须进行转换后才可以显示。因此,utf8在windows系统里,只是传输时使用,显示时通常会被认为是“乱码”。
二、PowerBuilder里关于DLL参数的使用
1、PowerBuilder里可以使用什么样的DLL?
DLL是windows操作系统的动态库。然而,windows操作系统的动态库有许多类型,里面的函数也有众多函数接口调用约定。使用COM、OLE、__cdecl、__stdcall等。COM、OLE是作为对象使用,OLEObject http;http.ConnectToNewObject( "Msxml2.XMLHTTP "),然后可以呼叫http对象的成员函数。这类调用不在此处讨论。此处只讨论导出函数的DLL。
作为导出函数的DLL,有通常有__cdecl、__stdcall两种调用约定。__cdecl由C/C++内部使用,不同库之间可以直接使用。PowerBuilder只能使用__stdcall调用约定导出的函数。
切记:PowerBuilder只能使用__stdcall调用约定导出的函数。
如果不是__stdcall调用约定的DLL,PowerBuilder是无法调用的,会报错,然后崩溃。
2、常见C/C++类型与PowerBuilder类型对应关系
C/C++类型 | PowerBuilder类型 |
short | int |
unsigned short WORD | uint |
BOOL bool | Long |
int | long |
unsigned int UINT | ulong |
Long | Long |
unsigned long DWORD | Ulong |
HANDLE | Ulong |
COLORREF | Ulong |
Short* | Ref int |
BOOL* bool* | 不能想当然地使用 ref boolean,PB里的boolean是PB的int类型,2字节;C/C++里的BOOL是定义为int,在PB里对应的是long,是4字节。 |
unsigned short* WORD* | Ref uint |
int* | Ref long |
unsigned int* UINT* | Ref ulong |
Long* | Ref Long |
unsigned long* DWORD* | Ref Ulong |
COLORREF* | Ref Ulong |
以上是win32平台上的固定对应关系。对于字符类型来说,就不能这样简单对应了。
char | 范围在-127-127,它是传值 ,不是传地址,不涉及分配内存大小问题,所以PB里可以使用int long,10.0以上版本可以使用byte类型。 |
BYTE unsigned char | 范围是0-255,它是传值 ,不是传地址,不涉及分配内存大小问题,所以PB里可以使用int long,10.0以上版本可以使用byte类型。 |
char * unsigned char* BYTE* | 字符串的含义非常复杂,切记:不能单纯理解为string。这需要具体分析数据表示的内容。如果仅仅仅表示为类似于姓名之类的字符串内容,可以声明为string;如果表示一段 内存区域,比如读取身份证的照片,某参数接收照片数据,那绝不能声明为string类型,而应该声明为blob类型。 PB9.0及以下版本中,声明为string或blob 即可。 PB10.0及以上版本中,可以声明为blob类型,作为纯字符串内容,可以再进行转换 string(blbData,EncodingAnsi!)转为默认的string类型。或者声明为string类型,然后声明函数里要加上 alias for “xxxx;ansi”字样,这种方法强烈不建议。应该使用相应的unicode版本函数。 如果是输出类型函数,需要添加 ref 关键字。 |
WCHAR* Wchar_t* | PB9.0及以下版本中,声明为string是不行的,必须声明为blob。在后再使用WINAPI函数 WideCharToMultiByte转换为string类型。 PB10.0及以上版本中,直接声明为string类型即可。 如果是输出类型函数,需要添加 ref 关键字。 |
3、特别说明
1。、对于ansi和unicode字符集编码,即PB9.0及以下版本和PB10.0及以上版本,在处理字符串时,应该使用相应版本的函数。例如:
PB9.0及以下:
Int GetWindowText(ulong hWnd,ref string lpString,long nMaxCount) library “user32.dll” alias for “GetWindowTextA”
PB10.0及以上:
Int GetWindowText(ulong hWnd,ref string lpString,long nMaxCount) library “user32.dll” alias for “GetWindowTextW”
请注意这两个声明,实际是2个函数,分别是GetWindowTextA和GetWindowTextW,注意尾部的“A”和“W”,“A”表示这是一个ansi编码函数,“W”表示这是一个unicode编码函数。操作系统已经提供了。
然而PB比较傻,将PB9程序升级到高版本时,它不会将GetWindowTextA改为GetWindowTextW,而是改为GetWindowTextA;ansi,注意它添加了一个”;ansi“,表示按ansi编码标准来调用这个函数。这种只能算是将就使用,许多时候是有问题的。例如:
如果一个窗口的标题是“中国“,
PB9里的GetWindowTextA返回的是4,PB10以上GetWindowTextW返回的是2;GetWindowTextA;ansi同样还是返回4。PB10以上循环按位置取时,按照长度4来循环,显然是错误,因为它实际是2个字符。
因此,正确的方法是:PB9及以下和PB10及以上版本里,应尽量使用各自不同编码方式的字符函数。
2、万能的类型blob,神一般的存在
我们知道,所有内容都在内存里面,而内存是以字节为单位,内存块有相应大小。Blob类型的定义就是
struct blob{
int len;
char *data
};
从定义里可知,blob就是指定了大小的一内存块。这个内存块里,什么都可以放得下。可以说,上面类型对照表里,PB相应的类型,可以全部改为blob。是不是神一般的存在?
我们对字符串做个测试:
PB9里面“中国“可以是4个字节:0xd6 0xd0 0xb9 0xfa,对应的10进制数是214 208 185 250。
blob {5}data //5字节,因为4字节是内容,最后一个字节是空,即字符串结尾
char c1,c2,c3,c4
c1= char(214) ;c2=char(208);c3=char(185);c4=char(250)
blobedit(data,1,c1)
blobedit(data,2,c2)
blobedit(data,3,c3)
blobedit(data,4,c4)
messagebox("",string(data))
PB115里面“中国“是2个字符0x4e2d 0x56fd,也是4个字节,对应的10进制数是78 45 86 253
blob {6}data//6字节,每字符占2字节,因为4字节是内容,最后2个字节是空,即字符串结尾
byte c1,c2,c3,c4
c1= 214 ;c2=208;c3=185;c4=250
blobedit(data,1,c1)
blobedit(data,2,c2)
blobedit(data,3,c3)
blobedit(data,4,c4)
messagebox("",string(data,encodingansi!))
blob可以有更多的用法,例如接收一个结构时,可以直接声明为一个大内存块blob{1024} data,这样去接收这块内容,只要分配的内存大于所需要的内存,调用就是安全的。
与blob可以一起使用的还有一个神一般的WINAPI函数RtlMoveMemory。RtlMoveMemory可以有各种声明方式,完成各种基于内存的复制操作。
例如可以这样声明:
SUBROUTINE CopyLongToBlob(ref blob dst,ref long n, long size) LIBRARY "kernel32" ALIAS FOR "RtlMoveMemory"
SUBROUTINE CopyBlobToLong(ref long dst,ref blob n, long size) LIBRARY "kernel32" ALIAS FOR "RtlMoveMemory"
然后这样测试:
blob {5}data//long是4个字节,内存多于4个字节就是安全的
long n1,n2
n1 = 1234
CopyLongToBlob(data,n1,4)//从指向n1内存的位置复制4个字节到data内存
CopyBlobToLong(n2,data,4)//从data内存复制4个字节到指向n2内存的位置
MessageBox("",n2)
结果相当于: n2 = n1,实现内存复制
上面的例子是从blob或者其他变量ref指向的地址开始复制,那么,如何从这个地址指向的后面位置开始复制呢?PB里没有直接获取地址的函数。pbidea里将添加一个AddressOf函数来获取变量地址。
subroutine CopyString(ref string dst,ulong fromAddress,long size) library "kernel32.dll" alias for "RtlMoveMemory"
PB9.0里面:
string ls_text,ls_copy
ls_text = "hello"
ulong ll_address
uo_utils u
u = create uo_utils
ll_address = u.AddressOf(ls_text)
if ll_address > 0 then
messagebox("",string(ll_address,"address"))
ls_copy = space(2)
CopyString(ls_copy,ll_address+1,3)
MessageBox("",ls_copy)
end if
destroy u
PB的string函数,可以直接将一个数字地址复制到字符变量里面。
CopyString(ls_copy,ll_address+1,3) 即从第2位取3个字节。
最后,留一个题目,如果在PB10以上版本里面,用CopyString从第2位取3个字符出来,应该怎么写呢?
写在最后:
PowerBuilder调用DLL,一定要严格类型转换,必要时必须预留足够内存空间,否则 ,程序很容易崩溃。许多人写的程序,运行一段时间会出现这样那样的意外崩溃,总以为什么冲突,其实不然,主要还是要去查这些DLL调用。
作为DLL,pbidea比较另类,使用的是PowerBuilder system library 方式用C++写成。该方式基于 PowerBuilder SDK,可以直接在DLL内部调用PB的内存管理方式进行内存分配,因此不需要调用时做内存分配,大大降低了因程序员出现内存分配错误而造成的系统崩溃概率。