它们被称为“蹦床”。这是我对它们用途的理解,虽然我不是 GNAT 专家,所以我的一些理解可能是错误的。
背景:Ada(与 C 不同)支持嵌套子程序。嵌套子程序能够访问封闭子程序的局部变量。例如:
procedure Outer is
Some_Variable : Integer;
procedure Inner is
begin
...
Some_Variable := Some_Variable + 1;
...
由于每个过程都有自己的堆栈帧来保存自己的局部变量,所以Inner 必须有一种方法可以访问Outer 的堆栈帧,以便它可以访问Some_Variable,无论是什么时候Outer 调用Inner,或Outer 调用其他一些调用Inner 的嵌套子程序。一个典型的实现是将隐藏参数传递给Inner,通常称为“静态链接”,它指向Outer 的堆栈帧。现在Inner 可以使用它来访问Some_Variable。
当您使用Inner'Access 时,乐趣就开始了,这是一个access procedure 类型。这可用于将Inner 的地址存储在access procedure 类型的变量中。其他子程序稍后可以使用该变量间接调用程序。如果您使用'Access,则必须在Outer 内声明该变量--您不能将过程访问存储在Outer 之外的变量中,因为之后有人可以在Outer 退出后调用它并且它的局部变量不再存在。 GNAT 和其他 Ada 编译器有一个 'Unrestricted_Access 属性可以绕过这个限制,因此 Outer 可以调用一些 outside 子程序间接调用 Inner。但是你在使用的时候一定要非常小心,因为如果你在错误的时间调用它,后果会很严重。
不管怎样,问题就出现了,因为当Inner'Access存储在一个变量中,后来用来间接调用Inner时,调用Inner时必须使用带有静态链接的隐藏参数。那么间接调用者怎么知道要传递什么静态链接呢?
一种解决方案(Irvine Compiler 的,可能还有其他的)是使这种访问类型的变量有两个值——过程地址和静态链接(所以access procedure 是一个“胖指针”,而不是一个简单的指针)。然后对该过程的调用将始终传递静态链接,以及其他参数(如果有)。 [在 Irvine Compiler 的实现中,如果指针内部的静态链接实际上指向一个全局过程,则该指针内部的静态链接将为空,因此代码知道在这种情况下不传递隐藏参数。] 缺点是这在以下情况下不起作用将过程地址作为回调参数传递给 C 例程(这在位于 C 图形库(如 gtk)之上的 Ada 库中非常常见)。 C 库例程不知道如何处理这样的胖指针。
GNAT 使用,或曾经使用,蹦床来解决这个问题。基本上,当它看到Inner'Unrestricted_Access' 时,它会即时生成新代码(“蹦床”)。这个蹦床使用正确的静态链接调用Inner(链接的值将嵌入到代码中)。然后访问值将是一个细指针,只有一个地址,即蹦床的地址。因此,当 C 代码调用回调时,它会调用蹦床,然后蹦床将隐藏参数添加到参数列表中并调用Inner。
这解决了问题,但在堆栈上生成蹦床时会产生安全问题。
编辑:当我用现在时提到 GNAT 的实现时我犯了错误。几年前我最后一次看到这个,我真的不知道 GNAT 是否仍然这样做。 [Simon 对此有更好的信息。] 顺便说一下,我确实认为可以使用蹦床但不将它们放在堆栈上,我认为这会减少安全问题。当我上次对此进行调查时,如果我没记错的话,Windows 已经开始阻止堆栈上的代码被执行,但它也允许程序请求可用于动态生成可执行代码的内存。