Foreign Language Code（外语代码）

（以下内容译自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 ).