PowerBuilder中调用DLL参数类型

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 httphttp.ConnectToNewObject( "Msxml2.XMLHTTP "),然后可以呼叫http对象的成员函数。这类调用不在此处讨论。此处只讨论导出函数的DLL

作为导出函数的DLL,有通常有__cdecl、__stdcall两种调用约定。__cdecl由C/C++内部使用,不同库之间可以直接使用。PowerBuilder只能使用__stdcall调用约定导出的函数。

切记:PowerBuilder只能使用__stdcall调用约定导出的函数。

如果不是__stdcall调用约定的DLLPowerBuilder是无法调用的,会报错,然后崩溃。

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 booleanPB里的booleanPBint类型,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及以下版本中,声明为stringblob 即可。

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。、对于ansiunicode字符集编码,即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个函数,分别是GetWindowTextAGetWindowTextW,注意尾部的“A”和“W”,“A”表示这是一个ansi编码函数,“W”表示这是一个unicode编码函数。操作系统已经提供了。

然而PB比较傻,将PB9程序升级到高版本时,它不会将GetWindowTextA改为GetWindowTextW,而是改为GetWindowTextA;ansi,注意它添加了一个”;ansi“,表示按ansi编码标准来调用这个函数。这种只能算是将就使用,许多时候是有问题的。例如:

如果一个窗口的标题是“中国“,

PB9里的GetWindowTextA返回的是4PB10以上GetWindowTextW返回的是2GetWindowTextA;ansi同样还是返回4PB10以上循环按位置取时,按照长度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函数RtlMoveMemoryRtlMoveMemory可以有各种声明方式,完成各种基于内存的复制操作。

例如可以这样声明:

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//long4个字节,内存多于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调用。

作为DLLpbidea比较另类,使用的是PowerBuilder system library 方式用C++写成。该方式基于 PowerBuilder SDK,可以直接在DLL内部调用PB的内存管理方式进行内存分配,因此不需要调用时做内存分配,大大降低了因程序员出现内存分配错误而造成的系统崩溃概率。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值