Foreign Language Code(外语代码)

From wiki.visual-prolog.com

Revision as of 13:50, 17 June 2015 by Yiding (talk | contribs) (Created page with "(以下内容译自Category:Tutorials中的Foreign_Language_Code。) “外语代码”指的是用非Visual Prolog语言的其它编程语言写的代码。Visual Prol...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

(以下内容译自Category:Tutorials中的Foreign_Language_Code。)

“外语代码”指的是用非Visual Prolog语言的其它编程语言写的代码。Visual Prolog可以直接调用外语代码,这里解释一些概念及细节。直接调用外语代码需要在很底层的二进制上与对应代码交互,简单的情况中这很容易,但复杂起来也不得了。不言而喻,处理复杂情况需要很多Visual Prolog及另外那种编程语言方面的知识,不过也用不着害怕,在多数情况下所需要的知识其实也是相当简单的。

这里描述的原理还可以帮助我们调用Microsoft Win32平台的API,这能极大开阔我们编程的领域。


关键概念

外语编译器处理一些事情与Visual Prolog的不一样,一方面它是其他人开发的,另一方面也因为它必须处理的是不同语言的不同特性。对Visual Prolog来说,要与所有的外语代码交互是不可能的,因为很难了解任意其它编译器所使用的原则。因而,Visual Prolog要与外语代码打交道,则这个外语代码必须遵守某些规则。

Visual Prolog可以调用这样的代码:它是用C语言编写的并且是用Microsoft的C编译器编译的。它不能调用任何还需要编译的代码。这么说够严格了,只要有创造力,处理所谓“不可能”也是常事儿。

为使用外语代码,当然需要访问这些代码。这里分两种与代码打交道的情况:一种是把代码直接链接到自己的程序中,另一种是让代码在动态链接库(DLL)中。

接下来需要定位代码,这是通过名称来实现的。如果代码直接链接到自己的程序中,必须要用链接(link)名;如果代码是放在DLL里则必须使用外引(export)名。不管是链接名还是外引名,在Visual Prolog代码中没有什么差别,不过要想在外语代码(或系统)中找相应的名称,那还是有差别的。说系统,是因为有时要使用的名称在代码里根本就没有,这样我们还是在概念上来使用链接名这个说法吧。

现在可以编写代码了,我们知道了它在什么地方。接着要传递输入参数,调用代码,代码执行后再获取输出。如此这般。有很多方法执行这个过程,很显然,调用者与被调用者在这个过程中应该有默契,也就是说有一致的调用协议。

调用者与被调用者不仅在参数如何传递上要一致,两者对被传递字节的解释上也要一致。换言之,两者的数据表示法 必须一致 。

最后,还必须要处理好内存的管理。调用程序与被调程序必须在分配内存与释放内存上相协调,在什么时间分配和释放上相协调。如果分配器与释放器不是同一个,释放器必须知道如何释放内存。

总之,调用外语代码有四个必须关照好的关键问题:

  • 链接名
  • 调用协议
  • 数据表示法
  • 内存管理

调用协议与链接名

这里把调用协议与链接名放在一起来讨论,因为传统上一些编译器总是以某种调用协议配合使用某种链接名。

所谓链接名(或外引名)是用于标识要调用的外语代码的一个名称。不同的编译器使用不同的缺省链接名,而且很多编译器可以指定特殊的链接名。在Visual Prolog中,可以在谓词声明中用“as”限定符声明链接名:

predicates
    pppp : (integer) as "LinkName".

在Visual Prolog程序中,上面谓词的名称是pppp,但其链接名是LinkName。

注意,只有 类谓词 才有链接名,这意味着它必须是在类中声明的,或是在实现的类谓词段中声明的。

Visual Prolog支持若干种不同的调用协议。调用协议也是在谓词声明中指定的,使用的限定符是language:

predicates
    qqqq : (integer) language c.

对于这个协议,C编译器通过在C程序所使用的名称前加一个下划线来创建链接名。如果使用c调用协议而又没有指定链接名,Visual Prolog也使用这个方法(注意,build 6107及以后的编译器实际上使用了另一种命名策略,必须用as来得到这样的链接名)。例如,上面qqqq的链接名是_qqqq。如果指定了链接名,则会使用指定的那个名字:

predicates
    rrrr : (integer) language c as "LinkName".

rrrr将使用C调用协议,其链接名是LinkName(前面没有下划线)。

C++编译器传统上使用C调用协议,但却不依赖C的链接名,因为C++是可以重载的。也就是说,在C++里同样的名称可以由不同的函数使用,只要这些函数有不同数量的参数或是其参数具有可区分的类型。这些不同的东西必须要有不同的链接名,因此C++编译器基于C++名称、参数数量及其类型创建精细的名称,这个过程称作名称分解。

不同的编译器使用不同的分解算法,它们之间通常并不兼容。因此,通常的做法或是对要在外语代码中访问的C++例程使用明确的链接名,或是在 export "C" 段包含一个声明,让编译器使用C命名协议。

Visual Prolog还支持所谓 stdcall 调用协议。这个协议由Microsoft Visual Basic用于Microsoft Win32平台的API调用,很多Pascal系列的编译器也使用了它,包括Borland Delphi。实际上,C和C++程序也常常在由外语代码访问的例程序中使用这个调用协议。Visual Prolog对 stdcall 使用了与c调用相同的命名协议(注意,build 6107及以后的编译器实际上使用了另一种命名策略,必须用as来得到这样的链接名)。也就是说,缺省情况下Prolog名前加一个下划线作为链接名,但如果指定了链接名,则使用指定的链接名。

Microsoft Win32平台的API使用 stdcall 调用协议,但另有其命名协议。其名称修饰比c调用协议稍多一点儿,详细内容请参看Visual Prolog语言参考手册。 Visual Prolog有一个特殊的调用协议 apicall 用于支持这种命名协议。事实上, apicall 与 stdcall 调用协议是相同的,只不过名称修饰不一样。在 apicall 调用协议中,显式声明于as限定符的链接名也要被修饰。如果需要一个特殊修饰的名字,就必须使用 stdcall 调用协议并且自行指定一个修饰名。

数据表示法

由于内容太多,这里对数据表示法不做详细讨论,只对Visual Prolog如何表示各类数据作一概要介绍。

输入参数的个数用值来传递,也就是说这个值是直接压进调用栈的。输出参数的个数用指针传递,也就是说压进调用栈的是这个值的指针。调用栈中所有整数都占32比特,而实数都占64比特。

字符由数字来表示。

所有其它的数据由指向实际数据的指针来表示。输入参数通过压入调用栈的指针来传递,输出参数通过压入栈中的指向结果指针的指针来传递。

一个函子域的值由指向一片内存的指针来表示,内存中先是函子(用一个32比特的数表示),接着是各子构件,其表示方法如上述,或直接是数值,或是实际数据的指针。

如果函子域只有一个项,则函子就省略了,因为它总是具有相同的值。(注意, Visual Prolog 5中函子是会有的,除非域声明中使用了struct关键字,但当前版本的Visual Prolog函子总是不会出现的)。

使用align限定符,函子可以使用不同的表示法,请参看语言参考手册。

串由指向以零结尾的字符序列的指针来表示,如同在C语言中一样。

二进制数由指向二进制数据的指针来表示。在数据本身之前,存放有数据的长度值加无符号数值的尺度(The value, which equals to the length of the data plus the size of unsigned, is stored immediately before the data)。

设要在Visual Prolog程序中调用一个C例程。C中的声明是这样的:

int myRoutine(wchar_t * TheString, int BufferLength, wchar_t * TheResult);

“wchar_t *”是C中的 Unicode 串。这个例程中有三个参数:

  • TheString,这是一个串
  • Bufferlength,这是一个整数
  • TheResult,它也是一个串

返回值是一个整数。相应的Visual Prolog声明如下(假设是在 实现 中声明的):

class predicates
    myRoutine : (string  TheString, integer BufferLength, string TheResult) -> integer language c.

如果谓词是在类声明中声明的,则需要去掉 class 关键字。

Externally和库

如果按上面说的声明一个谓词,编译器还是会给出错误消息说它找不到所声明谓词的子句。由于该谓词并非在Visual Prolog中实现的,这并不奇怪。所要做的是,告诉编译器这个谓词的代码应该在外部某个地方去找。之所以用externally这个字,因为正是要把它用在声明谓词的类实现中限定符resolve后面。如果类是叫作xxx,则它会是这样:

implement xxx
    resolve
        myRoutine externally
    ...

这样一来,编译器就会接受这个声明了,不过或许会碰到这样的情况,链接器还会出错,说_myRoutine没有定义。这是因为做这个工程时还缺包含有_myRoutine的库。

如果_myRoutine已经在一个静态库(在一个LIB文件)里了,只需要把这个文件加到工程中去,链接器就可以从该LIB中提取相应的代码并把它加到程序中。

如果_myRoutine在一个DLL里,则还是需要一个LIB文件,这个文件描述该DLL,然后把这个LIB文件加到工程中去。这种情况下链接器会在程序中加入描述该在哪个DLL中找该例程的信息。

如果_myRoutine在一个DLL中,但却没有相应的LIB文件,也还是可以调用该例程的。这只需要在resolve限定符后写出该DLL的文件名,像这样:

implement xxx
    resolve
        myRoutine externally from "myDLL.dll"
    ...

在这种情况下,编译器会添加代码,这个代码在myRoutine第一次调用时动态加载该DLL。这个DLL直到程序退出都不会再卸载。

pfc/application/useDll 包可以用来动态地加载和卸载DLL。如何(及为什么要)用这个包这里就不介绍了。

内存管理

数字和字符是通过调用栈直接传递的,或是直接写到需要的一片内存里,对于这些类型的内存处理是很容易的,没问题。

不直接依靠调用栈传递的数据,就是问题了。因为只要例程(调用程序或被调程序)还要用那个数据,数据占用的那片内存就都得是活的。但是,一旦它们不要用那个数据了,这片内存就得收回以免造成内存泄漏。

通常,只能是那个内存片的分配者来收回该内存,因为别人不知道怎样回收。当然,大家都使用相同机制来解决这个问题也是可以的,一方外露自己的机制给别人(如外露例程)。

Visual Prolog中用的内存分配程例程已经由(在Vip6kernel.dll中的)运行时系统外露了,可以在外语代码中调用。

为了处理好何时内存需要收回的问题,程序与库代码都要使用协调一致的方法。

典型解决方案

只要不涉及COM和.Net,下面就是解决内存处理问题最普通的方法。

原理很简单:

  • 输入参数只在调用时有效,因此如果被调程序以后还需要用某些输入参数,就应自行拷贝它们到一片内存中,这片内存由被调程序自己控制。
  • 输出拷贝到内存缓冲区,缓冲区是由调用程序提供的,这样,就没有被调程序分配的内存传递给调用程序。

上述myRoutine是这种例程的一个典型例子:输出写到TheResult中,这是个调用程序分配内存的串,它对被调程序是个输入,所以才需要传递缓冲区的长度BufferLength。

调用上面代码的Visual Prolog 6 的代码大致可以是这样的:

clauses
    p(TheString) = TheResult :-
        TheResult = string::create(bufferLength),
        RetCode = myRoutine(TheString, bufferLength, TheResult),
        checkRetCode(RetCode).

string::create用于分配串所使用的内存;myRoutine拷贝自己的结果到TheResult中(Visual Prolog程序什么也不做),它没有返回任何由它分配的内存。

垃圾收集

如果不依靠上面这个“典型”解决方案,可以考虑这个情况:Visual Prolog使用垃圾收集器管理它的堆(heap)。

垃圾收集器其实做的就是一轮一轮的回收工作:内存不再要用时,你不用管它,让它活着好像外语代码还要用它一样。一般情况下这没问题,因为它完全是自动工作的。不过垃圾收集器未必能知道外语代码部分中的数据何时是存活的,所以编程者应该保证让它在Visual Prolog代码部分中存活的时间比在外语代码部分中存活的时间要长。

使一内存片存活的一个典型的办法就是把它插入到一个事实中,当外语代码不再要用它时,再把它从事实中撤消。

Visual Prolog 6 还用了叫做G栈的一些内存,但Visual Prolog 7.0 开始不再用G栈了。

Microsoft Win32平台的API

你可能会想:我不会去写什么外语代码,所以我用不着这个。前一半你说了算,后一半的结论却未必对。原因是:操作系统就是“外语”。

操作系统提供了数千个有意思的外语例程,可以按这里说的从Prolog程序中调用它们。这些例程被称为Microsoft Windows XXX 平台 API。不同的平台例程不一样,尤其是平台越新例程越多(XP的就比NT 4.0的多得多)。

各平台的API可以参看Microsoft的MSDN库。

不过对我们来说,上述文档有一个问题:它说了很多常数,都说的是名字,但具体是什么值,没说。比如,PlaySound例程的文档中说,可以用一个叫做SND_ASYNC的标志,异步地由应用程序播放声音(也就是说一边播放声音一边应用程序做其它的什么事情)。SND_ASYNC 是一个数值,但具体是多少,文档中没说。

要找到这样常数的值,可以下载该平台C/C++的SDK,在C/C++头文件(*.h)里可以找到常数值。

按照得到的这些信息,可以自己创建类似下面这样的代码:

implement playSoundFile
    open core
 
domains
    soundFlag = unsigned32.
 
class predicates
    playSound : (string Sound, pointer HModule, soundFlag SoundFlag) -> booleanInt Success
        language apicall.
resolve playSound externally
 
constants
    snd_sync : soundFlag = 0x0000.
         /* 同步播放(缺省) */
    snd_async : soundFlag = 0x0001.
         /* 异步播放 */
    snd_nodefault : soundFlag = 0x0002.
         /* 静音(如果没有找到声音文件它就是缺省项!) */
    snd_memory : soundFlag = 0x0004.
         /* 声音指向一个内存文件 */
    snd_loop : soundFlag = 0x0008.
         /* 循环播放声音直到出现另一个sndplaysound */
    snd_nostop : soundFlag = 0x0010.
         /* 不停止任何正在播放的声音 */
    snd_nowait : soundFlag = 0x00002000.
         /* 若驱动器忙则不等待 */
    snd_alias : soundFlag = 0x00010000.
         /* 名称是一个注册别名 */
    snd_alias_id : soundFlag = 0x00110000.
         /* 别名是一个预定义的ID */
    snd_filename : soundFlag = 0x00020000.
         /* 名称是文件名 */
    snd_resource : soundFlag = 0x00040004.
         /* 名称是资源名或原子(atom) */
    snd_purge : soundFlag = 0x0040.
         /* 清除任务非静态事件 */
    snd_application : soundFlag = 0x0080.
         /* 查找应用程序指定的关联 */
 
clauses
    run() :-
        console::init(),
        _ = playSound("tada.wav", null, snd_nodefault+snd_filename).
 
end implement playSoundFile
 
goal
    mainExe::run( playSoundFile::run ).

参考