返回入门教程首页 主页 | 开发工具 | 应用芯片 | 核心模块  
 
   
 

Win32Forth

调用 Windows 过程

基本概念

从 Win32Forth 中调用 Window 函数是很容易的 -- 只要您知道怎么做。这个教程告诉您与 DLL 接口的关键字,并通过一些简单的例子进行说明。

我们假设您使用的 Win32Forth 版本从内核级支持 CALL 和 PROC 语句。为了确认,可以打入 .LIBS 。 它的输出应该看起来像下面这样的(这就是说,在您的机器上,地址和库的列表可能与这里的不同,它们因操作系统和 Win32Forth 版本的不同而不同)。

.libs
Location Handle   Type Name
-------- -------- ---- -----------
000450D8 71710000 APP  COMCTL32.DLL
0002C91C 76B30000 APP  COMDLG32.DLL
0001794C 10000000 APP  WINCON.DLL
00017934 782F0000 APP  SHELL32.DLL
0001791C 7C2D0000 APP  ADVAPI32.DLL
00017908 77F40000 APP  GDI32.DLL
00013490 00D20000 APP  PRINT.DLL
0000E6D8 63180000 APP  SHLWAPI.DLL
0000BFA0 7C570000 KERN KERNEL32.DLL
0000B0FC 77E10000 APP  USER32.DLL
0000A970 00D40000 APP  CONSOLE.DLL

基本的 DLL

DLL ( 动态链接库 ) 和您在 PC 机上运行的程序类似,当然也有几个关键的差异。 DLL 通常没有一个可见的接口,也就是说,它们没有窗口。 DLL 可以在不同的进程间共享;不管有多少个程序使用它,只需要有一个拷贝驻留在存储器中。

DLL 通常为使用它的程序提供打包的功能,为了在程序中有效地使用 DLL ,您需要得到它们的文档。

在 Win32Forth 中使用的字

调用外部的 DLL 需要有几个字,它们是:

调试字: .PROCS, .LIBS

定义字: PROC, WINLIBRARY

调用字: CALL, REL>ABS, ABS>REL

命名字: AS

步骤 1. 得到DLL的文档

没有 DLL 的文档,您就不可能知道 DLL 包含的函数名字,也不可能知道它们的参数和返回值。主要的 Window DLL 文档可以在 msdn.microsoft.com 上得到,还有许多第三方的文档也包含了这些内容。对于其它的 DLL ,您需要参考与 DLL 一同来的文档。

步骤 2. 告诉 Win32Forth 使用 DLL

Win32Forth 需要知道 DLL 的名字以便装载它们。语句 WINLIBRARY KERNEL32.DLL 允许 W32F 装入 DLL ,并准备执行输出子程序。 DLL 库的查找路径因操作系统 (95/98/ME/NT/2000/XP) 而不同,但是通常情况我们只需要使用位于系统目录、 Win32Forth 启动目录中的 DLL 库。大多数 DLL 可以通过这种方法找到,需要指定路径名的情况很少见。 WINLIBRARY 可以被重复指定,但是只有最后一个拷贝被装入。 .LIBS 命令显示了被指定和被装入的库。

Location Handle   Type Name
-------- -------- ---- -----------
0003EA5C -noload- APP  TESTIT.DLL
00014C08 10000000 APP  WINCON.DLL
. . .

这个字通常只从控制台输入。它显示出 TESTIT.DLL 已经指定但是还没有被装入。如果您在调用时发生 Win32F 找不到 DLL 的错误,而您又确定它应该在您的库中,请检查 handle 地址。如果这个地址仍然是 -noload- , 那么 W32F 就不能找到相应的库。参看 CALL 以得到更多的细节。

步骤 3. 告诉Win32Forth 外部子程序

为了定义 DLL 中的函数,使用 PROC 语句。这里是一个从文档中来的 Windows 调用的例子:


AllocConsole
AllocConsole 函数为调用进程分配一个新的控制台。

BOOL AllocConsole(void);

Parameters 这个函数没有参数。

Return Values 如果成功,返回非零值。如果失败,返回零

这个函数可这样声明

0 PROC AllocConsole

也就是说,函数有 0 个参数,名字是 AllocConsole 。注意大小写 -- PROC 和 CALL 语句是大小写敏感的!您必须按上面显示的样子拼写。但是,参数的数目只是为了文档的需要。合法的参数数目可以从 0 到 127 -- 不要指定负数,因为这些负值用于指定特殊的系统过程。

这一步是可选的 -- 不需要非得写上一个 PROC 语句不可,因为 W32F 将在 CALL 语句中动态地定义 PROC 。 只有有限数目的过程可以动态定义,现在大约是 100 到 150 个。定义的过程可以使用 .PROCS 命令查看。限制输出是一个子串(大小写没有关系)

.PROCS ALLOC

Location ProcName Prm ProcEP LibName

-------- -------- --- -------- --------

0003EA5C AllocConsole 0

00012814 GlobalLock 77E977E4 KERNEL32.DLL

000127F4 GlobalAlloc 77E96646 KERNEL32.DLL

0000B830 HeapReAlloc 4

0000B7F8 HeapAlloc 3 77FCB10F KERNEL32.DLL

Allocated Space: 12,288 bytes

Free Space Left: 6,997 bytes

Displayed 5 of 184 procedures defined

步骤 4. 调用函数

现在已经我们定义了库,选择性地定义了过程,我们可以使用 CALL 语句调用一个函数:

CALL AllocConsole

这里特别注意大小写 -- 必须与文档的大小写匹配。上面的调用执行之后,您应该得到一个 DOS 控制台和一个 0 在数据堆栈上,这个值是从函数返回的代码。

更高级的特性

参数顺序

Windows 调用的参数在堆栈上是按文档说明的相反顺序传递的。例如,如果文档说

BOOL Function (A, B, C);

则我们的编码应该是

C B A CALL Function

反序对于调用成功来说是基本的。

过程的名字

有些函数显示出多达 3 个名字。例如,有一个函数 CharUpperBuff 、 CharUpperBuffA 和 CharUpperBuffW 。 事实上,它们只有两个名字。请看下面这个例子的输出

.PROCS :

Location ProcName Prm ProcEP LibName

-------- -------- --- -------- --------

0003EA98 CharUpperBuffW 2 77E1236F USER32.DLL

0003EA74 CharUpperBuffA 2 77E1263D USER32.DLL

00003350 CharUpperBuff 2 77E1263D USER32.DLL

ProcEP 列显示了这个函数的入口点。 CharUpperBuff 和 CharUpperBuffA 函数是同一个函数,名字是 CharUpperBuffA , A 函数表示这个函数是一个访问 ASCII 字符的函数,字符宽度是 8 位。

W 函数是一个 UNICODE 或者 2 字节函数。通常,您应该使用 A 函数,因为 W32F 是一个基于字节的系统。任何 W32F PROC 或者 CALL 一个函数都有 A 和 W 的变形,不指明时就自动装入 A 版本。如果您真的想要 W 函数,则您必须指明:

2 PROC CharUpperBuffW .

返回值

从函数返回的值通常都是在调用之后留在数据堆栈顶上的一个项目。大多数函数都是用 C 或者 C++ 编写的,不能返回多个值,如果需要返回多于一个的值,还必须使用其它的方法。

返回值可以是:

•  BOOL (0 = TRUE 或者 OK, 1 = FALSE 或者 Not OK) 。为了把这个值转换成 Forth 的 TRUE / FALSE , 可在调用之后使用 0= 或者 ABORT" 比如

CALL AllocConsole 0= IF ( all is OK )

或者

CALL FreeConsole ABORT" FreeConsole failed"

•  INT 、 HANDLE 等,所有这些都是堆栈上的无符号或者有符号的 32 位值 (DWORD) 。可以把它们作为一般的 Forth 字使用。

•  VOID 堆栈上的 32 位值 (DWORD) ,没有意义,应该丢弃(使用 DROP )。

•  其它,请看下面内容。

解释函数参数

函数的参数各不相同,为了使调用能够正确地工作,这些参数不仅需要有正确的顺序,还必须有正确的类型。这里给出了如何指定不同的参数。例如:

BOOL ReadFile( HANDLE hFile, // handle to file LPVOID
lpBuffer,                    // data buffer
DWORD nNumberOfBytesToRead,  // number of bytes to read
LPDWORD lpNumberOfBytesRead, // number of bytes read
LPOVERLAPPED lpOverlapped    // overlapped buffer
   );

变成了:

0 VALUE HANDLE
CREATE BUFFER 255 ALLOT
20 VALUE BYTESTOREAD
VARIABLE BYTESREAD
: MYFUNC
  0                    \ null address for overlapped buffer
  BYTESREAD            \ bytes read
  BYTESTOREAD          \ bytes to read
  BUFFER               \ address of buffer
  HANDLE               \ value of handle
  CALL ReadFile ;

•  HANDLE 、 BYTE 、 WORD 、 DWORD: 都是简单的堆栈值,或者是如上面例子中指定值的名字。如果一个 HANDLE 值是 13 ,那么 13 应该在堆栈上。

•  LPxxx 类型 ( 除 STR 外 ): 指向一个值的指针。如果您使用 VALUE <name> 作为 LPxxx 参数时请特别小心 – 它们通常不能正常工作,特别是当函数返回一个值时,比如在 BYTESREAD 的情况下。使用 VARIABLE ,它可以把地址放到堆栈上。

•  LPxSTR 类型 : Null (0) 结束的串。上面的方法可以使用,比如:

: UPPERCASE ( caddr -- caddr )
    DUP COUNT SWAP
    call CharUpperBuff DROP ;

这个函数得到一个计数串,把它转成 (addr len) ,再转换到 (len addr) 。这种计数在后、串指针在先的格式对调用是必要的。函数在堆栈上返回一个值(在本例中是 n ),不过我们并不关心。实际上,上面的例子并不是一个真正的 null 结束串的调用,下面是一个:

int lstrlen(LPCTSTR lpString);

这将计数串中的字符。于是, Z" ABCDEFG" CALL lstrlen 返回 7 ,这是串的长度。 在 W32F 中,下面这些串类型是 null (0) 结束的串,可以安全地用在调用中:

C" – 计数的字节也被同样计算。也就是说, C" ABCDEFG" COUNT DROP 将指向开始的 A

S" ABCDEFG" 把 (addr len) 放到堆栈上。地址指向一个 null 结尾的串。或者 DROP 或者 SWAP 依赖于您对计数的需要。

Z" ABCDEFG" 与 C" 相同,但是没有计数字节,所以 Z" 能够直接使用。

如果您希望创建自己的串缓冲区,这里是一个创建 null 结尾串的技术:

CREATE MYSTR 256 ALLOT  \ my string
S" ABCDEGF" MYSTR PLACE \ move string to MYSTR
MYSTR +NULL             \ add null on the end
MYSTR CALL lstrlen \ use in call.

使用 ABS>REL 和 REL>ABS (旧的特性)

ABS>REL 和 REL>ABS 是旧的特性。不要再使用了,如果您在现存的代码中见到它们,就简单的忽略。它们不做任何事情。

使用 AS

有时我们可能有这样的 Forth 字,它需要得到过程的 XT (可执行标记,或称代码域地址);有时候是 DLL 的过程名字太长,或者意义隐晦,我们需要一个有意义的、短小的名字。我们可以这样:

1 PROC ExitThread AS EXIT-TASK

可以把 EXIT-TASK 加入到当前的字列表中。 AS 后面应该是过程的声明 ( 如果需要,可以在任何的注释之后 ) 。

危险的技巧

这是一个危险的技巧,请在您对调用一个 DLL 函数更加熟悉之后再使用。对于函数 BOOL function(LPWORD param) ; ( 假设它返回 1, 并把一个 0 放到指向的区域 ) ,您可以像下面这样:

0 SP@ CALL function

在 CALL 之前,堆栈看起来是这样的: 0 | addr of previous cell |

在调用之后,堆栈应该是 10 | 1 。

不需要变量! ReadFile 调用看起来是这样的:

: MYFUNC 0        \ null address for overlapped buffer
  0 SP@           \ bytes read
  BYTESTOREAD     \ bytes to read
  BUFFER          \ address of buffer
  HANDLE          \ value of handle
  CALL ReadFile ;

将在堆栈返回值 2 、读入的字节数和返回码,并按这里的顺序放置。


 
   

(C) ForthChina.com 版权所有 2004-2010
Email:forthchina@163.com