acm-header
登录

ACM通信

实践

跨语言互操作性的挑战


一堆书

图片来源:Alicia Kubista / Andrij Borys Associates

回到顶部

自从第二种编程语言发明以来,语言之间的互操作性一直是一个问题。解决方案包括独立于语言的对象模型,如组件对象模型(COM)和公共对象请求代理体系结构(CORBA),以及设计用于集成语言的虚拟机(vm),如Java虚拟机(JVM)和公共语言运行时(CLR)。

随着软件变得越来越复杂,硬件越来越不同质化,单一语言作为整个程序的正确工具的可能性比以往任何时候都要低。随着现代编译器变得更加模块化,有可能出现新一代有趣的解决方案。

1961年,英国Stantec公司发布了一种名为ZEBRA的计算机,它之所以有趣,有很多原因,尤其是它基于数据流的指令集。ZEBRA很难用完整的本机指令集来编程,所以它还包含了一个更传统的版本,叫做简单代码。这个表格有一些限制,包括每个程序的指令限制为150条。手册很有帮助地告诉用户,这不是一个严重的限制,因为不可能有人会编写一个如此复杂的工作程序,它需要超过150条指令。

如今,这种说法似乎很可笑。即使是C等相对低级语言中的简单函数,在编译后也有超过150条指令,而且大多数程序远不止一个函数。从编写汇编代码到使用高级语言编写代码的转变极大地增加了可能的程序的复杂性,各种软件工程实践也是如此。

软件越来越复杂的趋势没有减弱的迹象,现代硬件带来了新的挑战。在20世纪90年代末,程序员必须瞄准低端pc,这些pc具有类似快速PDP-11的抽象模型。在高端,他们会遇到一个抽象模型,如非常快的PDP-11,可能有两个到四个(相同的)处理器。现在,手机开始出现8个内核,具有相同的ISA(指令集架构),但速度不同,一些其他的流处理器针对不同的工作负载(dsp、gpu)进行优化,以及其他专门的内核。

高级语言表示类(类似于人类对问题域的理解)和低级语言表示类(类似于硬件)之间的传统划分不再适用。没有一种底层语言具有接近可编程数据流处理器、x86 CPU、大规模多线程GPU和非常长的指令字(VLIW)数字信号处理器(DSP)的语义。希望从可用硬件中获得最后一点性能的程序员不再有一种语言可以用于所有可能的目标。

类似地,在抽象光谱的另一端,特定于领域的语言正变得越来越普遍。高级语言通常用通用性换取高效表示算法子集的能力。更通用的高级语言,如Java,牺牲了直接操作指针的能力,为程序员提供了更抽象的内存模型。诸如SQL之类的专门语言使某些类别的算法无法实现,但使其领域内的常见任务可以用几行代码表示。

您不能再期望用单一语言编写一个重要的应用程序。高级语言通常调用用低级语言编写的代码作为其标准库的一部分(例如,GUI呈现),但添加调用可能很困难。


在未来的几年里,语言之间的接口问题对编译器编写者来说将变得越来越重要。


特别是,两种非C语言之间的接口通常很难构造。即使是相对简单的例子,比如c++和Java之间的桥接,通常也不会自动处理,需要一个C接口。Kaffe本机接口4确实提供了这样做的机制,但它没有被广泛采用,而且有局限性。

在未来的几年里,语言之间的接口问题对编译器编写者来说将变得越来越重要。它提出了许多挑战,具体在这里。

回到顶部

对象模型差异

面向对象语言将一些代码和数据的概念绑定在一起。在施乐PARC帮助开发面向对象编程的Alan Kay将对象描述为“通过消息传递进行通信的简单计算机”。这个定义为不同的语言填补细节留下了很大的余地:

  • 是否应该有工厂对象(类)作为语言中的第一级构造?
  • 如果有类,它们也是对象吗?
  • 一个对象应该有0个(例如Go)、1个(例如Smalltalk、Java、JavaScript、Objective-C),还是多个(例如c++、Self、Simula)超类或原型?
  • 方法查找是否绑定到静态类型系统(如果有的话)?
  • 数据包含在静态布局还是动态布局的对象中?
  • 是否可以在运行时修改方法查找?

多重继承问题是最常见的关注领域之一。单一继承很方便,因为它简化了实现的许多方面。对象可以通过附加字段来扩展;转换到超类型只涉及忽略结束,而转换到子类型只涉及检查指针值是否保持不变。c++中的下强制转换需要通过运行时库函数对运行时类型信息中的继承图进行复杂的搜索。

单独来说,两种类型的继承都可以实现,但是,如果您想将c++对象公开到Java中,会发生什么情况呢?您也许可以遵循. net或Kaffe方法,只支持与c++的一个子集(托管c++或c++ /CLI)的直接互操作性,这些子集只支持将在Java端公开的类的单一继承。

这通常是一个很好的解决方案:定义一种语言的子集,它清晰地映射到另一种语言,但可以调用超集的全部功能。这是Pragmatic Smalltalk所采用的方法:5允许将objective - c++对象(可以将c++对象作为实例变量并调用它们的方法)直接公开,就像它们是Smalltalk对象一样,共享相同的底层表示。

然而,这种方法仍然存在认知障碍。如果您想直接使用c++框架,例如Pragmatic Smalltalk或。net中的LLVM,那么您将需要编写单继承类,这些类封装了标准库用于其大多数核心类型的多继承类。

另一种可能的方法是避免公开对象中的任何字段,而只是将每个c++类作为接口公开。但是,如果没有特殊的编译器支持,就不可能从桥接的类继承一些接口,因为实现时附带了一些接口。

尽管复杂,但这是一个比在方法查找含义上不同的语言之间进行接口更简单的系统。例如,Java和Smalltalk拥有几乎相同的对象和内存模型,但是Java将方法分派的概念与类层次结构联系在一起,而在Smalltalk中,如果两个对象实现具有相同名称的方法,则可以互换使用它们。

这是RedLine Smalltalk遇到的一个问题,1它编译Smalltalk以在JVM上运行。其实现Smalltalk方法分派的机制包括为每个方法生成Java接口,然后在分派之前将接收器转换为相关的接口类型。向Java类发送消息需要额外的信息,因为现有的Java类没有实现这一点;因此,RedLine Smalltalk必须重新使用Java的反射api。

Smalltalk(和Objective-C)的方法查找更加复杂,因为在其他语言中有许多二次机会分派机制是缺失的或有限的。当将Objective-C编译为JavaScript时,而不是使用JavaScript方法调用,您必须将发送的每个Objective-C消息封装在一个小函数中,该函数首先检查方法是否实际存在,如果不存在,则调用一些查找代码。

这在JavaScript中相对简单,因为它以一种方便的方式处理变进函数:如果调用函数或方法时带的参数比它预期的要多,那么它将接收剩余的参数作为它预期的数组。Go做了类似的事情。类c语言只是把它们放在堆栈上,并期望程序员在没有错误检查的情况下做正确的事情。

回到顶部

内存模型

在内存模型中,最明显的二分法是自动重分配和手动重分配。一个稍微更重要的问题是确定性破坏和非确定性破坏之间的区别。

可以用Boehm-Demers-Weiser垃圾收集器运行C3.在很多情况下都没有问题(除非内存耗尽,并且有很多看起来像指针的整数)。在c++中做同样的事情要困难得多,因为对象回收是一个可观察事件。中所示的代码图1

LockHolder类定义了一个非常简单的对象;互斥锁传入对象,然后对象在构造函数中锁定互斥锁,并在析构函数中解锁互斥锁。现在,想象一下在完全垃圾回收的环境中运行相同的代码,而没有定义析构函数的运行时间。

这个例子相对容易理解。此时需要一个垃圾回收的c++实现来运行析构函数,但不需要释放对象。这种习惯用法在设计为从一开始就支持垃圾收集的语言中不可用。混合使用它们的根本问题是不能确定谁负责释放记忆;相反,为一个模型编写的代码需要确定性操作,而为另一个模型编写的代码不需要。

为c++实现垃圾收集有两种简单的方法:第一种是使删除操作符调用析构函数但不回收底层存储;另一个是制造删除当检测到对象不可达时,调用无操作析构函数。

只调用的析构函数删除在这两种情况下都是一样的:它们实际上是无效的。释放其他资源的析构函数是不同的。在第一种情况下,它们确定性地运行,但如果程序员不显式地删除相关对象,它们将无法运行。在第二种情况下,它们保证最终运行,但不一定在底层资源耗尽之前运行。

此外,在许多语言中一个相当常见的习惯用法是一个自拥有的对象,它等待一些事件或执行长时间运行的任务,然后触发一个回调。然后,回调的接收者负责清理通知器。虽然它是活动的,但它与对象图的其余部分断开连接,因此看起来是垃圾。必须明确地告诉收集器它不是。这与没有自动垃圾收集的语言中的模式相反,在没有自动垃圾收集的语言中,假定对象是活动的,除非系统被告知有其他情况。(Hans Boehm在1996年的一篇论文中更详细地讨论了这些问题。2

所有这些问题都出现在苹果在Objective-C中添加垃圾收集的失败尝试中(谢天谢地,它不再被支持了)。很多Objective-C代码依赖于运行代码dealloc方法。另一个问题与互操作性问题密切相关。该实现同时支持跟踪和非跟踪内存,但没有在类型系统中公开此信息。考虑在图2

在垃圾收集模式下,无法判断此代码是否正确。它是否正确取决于调用者。如果调用者传递分配给NSAllocateCollectable (),NSScannedOption作为第二个参数,或者在堆栈上分配缓冲区,或者在使用垃圾收集支持编译的编译单元中的全局中,那么对象将(至少)与缓冲区保持同样长的时间。如果调用者传递分配给malloc ()或作为C或c++编译单元中的全局变量,那么对象将(潜在地)在缓冲区之前被释放。的潜在的在这句话中,问题变得更大了:因为它是不确定性的,所以很难调试。

Objective-C的自动引用计数(ARC)扩展没有提供完整的垃圾收集(它们仍然允许垃圾循环泄漏),但是它们扩展了类型系统来定义此类缓冲区的所有权类型。将对象指针复制到C需要插入包含所有权转移的显式强制转换。

引用计数还解决了非循环数据的确定性问题。此外,它还提供了一种与手动内存管理进行互操作的有趣方法:使free ()减少引用计数。循环(或潜在的循环)数据结构需要添加循环检测器。IBM的大卫·f·培根(David F. Bacon)团队设计了许多循环探测器8这允许引用计数成为一种完整的垃圾收集机制,只要可以准确地识别指针。

不幸的是,循环检测涉及从潜在的循环对象遍历整个对象图。可以采取一些简单的步骤来减少这种成本。最明显的是推迟。如果一个对象的引用计数减少但没有被释放,那么它可能只是循环的一部分。如果它后来被增加,那么它就不是垃圾循环的一部分(它可能仍然是循环的一部分,但您还不关心)。如果它后来被释放,那么它就是非循环的。

延迟周期检测的时间越长,得到的不确定性就越多,但周期检测器必须做的工作就越少。

回到顶部

异常和unwind

如今,大多数人从c++普及的意义上考虑异常:它大致相当于setjmp ()而且longjmp ()在C语言中,虽然可能有不同的机制。

还提出了一些其他的例外机制。在Smalltalk-80中,异常完全在库中实现。该语言提供的唯一基本元素是,当显式地从闭包返回时,从声明闭包的作用域返回。如果向堆栈中传递闭包,则返回将隐式展开堆栈。

当Smalltalk异常发生时,它调用堆栈顶部的处理程序块。这可能会返回,迫使堆栈展开,或者执行一些清理操作。堆栈本身是激活记录的列表(它们是对象),因此可以执行更复杂的操作。Common Lisp也提供了丰富的异常集,包括那些支持事后立即恢复或重新启动的异常。

即使在具有类似异常模型的语言中,异常互操作性也很困难。例如,c++和Objective-C都有类似的异常概念,但是c++ catch块期望捕获异常void *当它遇到Objective-C对象指针时怎么办?在GNUstep Objective-C运行时6在美国,我们决定不模仿苹果的细分错误行为后,选择不去捕捉它。最近版本的OS X采用了这种行为,但这个决定有点武断。

即使您从c++中捕获了对象指针,也不意味着您可以对它做任何事情。当它被捕获时,您已经丢失了所有的类型信息,并且无法确定它是否是一个Objective-C对象。

当您开始考虑性能时,更微妙的问题就出现了。VMKit的早期版本7(在LLVM之上实现Java和CLR vm)使用了为c++设计的零成本异常模型。这是零成本因为输入一个try块不需要花费什么。然而,在抛出异常时,您必须解析一些描述如何展开堆栈的表,然后调用每个堆栈帧的个性函数来决定是否(以及在哪里)应该捕获异常。

这种机制在c++中工作得非常好,因为在c++中异常很少,但Java使用异常来报告许多相当常见的错误条件。在基准测试中,放卷器的性能是一个限制因素。为了避免这种情况,对可能引发异常的方法修改了调用约定。这些函数将异常作为第二个返回值返回(通常在不同的寄存器中),每次调用都必须检查该寄存器是否包含0,如果不包含0则跳转到异常处理块。

当您为每个调用者控制代码生成器时,这很好,但在跨语言场景中就不是这样了。您可以通过向C语言添加另一种调用约定来解决这个问题,该约定可以反映这种行为,或者提供类似于Go中常用的用于返回错误条件的多返回值机制的东西,但这需要每个C调用者都了解外语语义。

回到顶部

变异和副作用

当您开始在希望进行互操作的集合中包含函数式语言时,可变性的概念就变得很重要了。像Haskell这样的语言没有可变类型。修改适当的数据结构是编译器可以作为优化来做的事情,但它不是在语言中公开的事情。

这是f#遇到的一个问题,它作为OC-aml的方言出售,可以与其他。net语言集成,使用c#编写的类,等等。c#已经有了可变和不可变类型的概念。这是一个非常强大的抽象,但不可变类只是一个不公开任何非只读字段的类,而只读字段可能包含对对象的引用(通过任意引用链),引用可变对象的对象的状态可以从函数代码中更改。在其他语言中,如c++或Objective-C,可变性通常是在类系统中通过定义一些不可变的类来实现的,但是没有语言支持,也没有确定对象是否可变的简单方法。

C和c++在该语言提供的类型系统中有非常不同的可变性概念:对对象的特定引用可能会修改它,也可能不会修改它,但这并不意味着对象本身不会改变。这再加上深度复制问题,使得函数式和面向对象语言的接口成为一个难题。

单子为界面提供了一些诱人的可能性。单子是计算步骤的有序序列。在面向对象的世界中,这是一系列消息发送或方法调用。具有c++ const概念的方法(即不修改对象的状态)可以在单子外部调用,因此可以进行推测执行和回溯,而其他方法应该按照单子定义的严格顺序调用。

回到顶部

并行度模型

可变性和并行性密切相关。编写可维护的、可扩展的并行代码的基本规则是,任何对象都不能同时是可变的和别名的。在纯函数式语言中实现这一点是微不足道的:根本没有对象是可变的。Erlang对可变性做出了一个让步,即进程字典。进程字典是一个可变的字典,只能从当前Erlang进程访问,因此永远不能共享。

将具有不同共享概念的语言进行接口会出现一些独特的问题。这对于以大规模并行系统或gpu为目标的语言来说很有趣,因为语言的模型与底层硬件密切相关。

这是在试图提取C/ c++ /Fortran程序的部分内容以将其转换为OpenCL时遇到的问题。源语言通常将就地修改作为实现算法的最快方式,而OpenCL鼓励使用处理源缓冲区以生成输出缓冲区的模型。这很重要,因为每个内核在许多输入上并行运行;因此,为了最大吞吐量,它们应该是独立的。


如何向程序员展示具有非常不同的抽象机器模型的多个处理单元是一个有趣的研究问题。要提供一种能够有效捕获语义的单一语言是非常困难的。


然而,在c++中,确保两个指针不别名是非常重要的。restrict关键字的存在是为了允许程序员提供这种注释,但在一般情况下,编译器不可能检查它是否被正确使用。

高效的互操作性对于异构多核系统非常重要。在传统的单核或SMP(对称多处理)计算机上,接近问题域的高级语言和接近体系结构的低级语言之间存在一维谱。在异构系统上,没有一种语言接近底层架构,因为在gpu上运行任意C/ c++和Fortran代码的难度已经显示出来。

当前的接口,例如OpenCLare,离理想还有很长的路要走。程序员必须编写C代码来管理设备上下文的创建和数据在设备之间的移动,然后用OpenCL C编写内核。用另一种语言表示设备上运行的部分的能力是有用的,但当用于简单操作的大部分代码与两个处理元素之间的边界有关,而不是与在任何一边所做的工作有关时,那么就有问题了。

如何向程序员展示具有非常不同的抽象机器模型的多个处理单元是一个有趣的研究问题。要提供一种能够有效捕获语义的单一语言是非常困难的。因此,这个问题变成了专门语言之间的互操作性问题。这是一个有趣的变化:特定于领域的语言,传统上处于频谱的高级端,现在作为低级语言的作用越来越大。

回到顶部

虚拟机错觉

虚拟机经常被吹捧为解决语言互操作性问题的一种方法。当Java被引入时,承诺之一是您很快就可以编译所有遗留的C或c++代码,并在JVM中与Java一起运行,从而提供一条干净的迁移路径。今天,Ohloh.net(跟踪公共开源存储库中可用的代码行数)报告了40亿行C代码,c++和Java各约15亿行。而其他语言,如Scala(近600万行代码被跟踪Ohloh.net)在JVM中运行,而遗留的低级语言则不能。

更糟糕的是,从Java调用本机代码非常麻烦(在认知和运行时开销方面都是如此),以至于开发人员最终只能用c++编写应用程序,而不是从Java调用c++库。微软的CLR做得稍微好一点,允许用c++的一个子集编写的代码运行;它使调用本机库更容易,但仍然提供了一堵墙。

这种方法对于像Smalltalk这样没有大公司支持的语言来说是一场灾难。Smalltalk VM以持久性模型和反射式开发环境的形式提供了一些CLR和JVM都没有提供的优势,但是它通过将世界划分为内部和外部的事物,形成了一个非常大的PLIB(编程语言互操作性障碍)。

一旦您有两个或更多的vm,就会变得更加复杂,并且现在有源语言互操作性问题和两个vm之间的互操作性问题(非常相似),这通常是非常低级的编程语言。

回到顶部

前进的道路

许多年前,互操作性的主要问题是C和pascal——这两种语言有着几乎相同的抽象机器模型。问题是Pascal编译器将它们的参数从左往右推到堆栈上(因为这样需要的临时时间更少),而C编译器将它们从右往左推(以确保对于可变变量函数,第一个参数位于堆栈的顶部)。

通过将调用约定定义为平台应用程序二进制接口(ABI)的一部分,这个简单的权略在很大程度上解决了互操作性问题。不需要虚拟机或中间目标,也不需要任何源到源的转换。虚拟机的等价物由ABI和目标机器的ISA定义。

Objective-C提供了另一个有用的案例研究。Objective-C中的方法使用C调用约定,首先传递两个隐藏参数(对象和选择器,这是方法名的抽象形式)。语言中不能简单地映射到目标ABI或ISA的所有部分都被分解到库调用中。方法调用实现为对objc_msgSend ()函数,它被实现为一个简短的汇编例程。所有的内省都是通过调用运行时库的机制进行的。

我们使用GNUstep的Objective-C运行时在LanguageKit中实现Smalltalk和JavaScript方言的前端。这使用LLVM,但只是因为有一个低级的中间表示可以在编译器之间重用优化:互操作性发生在本机代码中。这个运行时还支持Apple定义的块ABI;因此,闭包可以在Smalltalk和C代码之间传递。

Boehm垃圾收集器(GC)和Apple AutoZone都旨在以库的形式提供垃圾收集,但要求不同。并发压缩收集器是否可以作为库公开,并将对象单独标记为不可移动,当它们传递给低级代码时?是否可能在ABI或库中强制执行可变性和并发性保证?这些都是开放的问题,成熟的编译器设计库的可用性使它们成为有趣的研究问题。

也许更有趣的问题是,有多少这样的东西可以嵌入到硬件中。在值得崩溃的可信系统研发(CTSRD)中,这是SRI国际和剑桥大学计算机实验室的一个联合项目,研究人员一直在试验将细粒度的内存保护表达到硬件中,他们希望这将提供更有效的方式来表达特定的语言内存模型。这只是一个开始,但在硅中为高级语言提供更丰富的特性集方面还有很大的潜力,这在20世纪80年代被避免了,因为晶体管是稀缺且昂贵的资源。现在晶体管很丰富,但电源却很稀缺,因此CPU设计中的权衡非常不同。

在过去的30年里,这个行业一直在为运行C等语言而优化cpu,因为需要快速代码的人使用C语言(因为设计处理器的人针对C语言对它们进行了优化,因为……)也许是时候开始探索更好的内置支持其他语言的通用操作了。RISC项目诞生于对原语编译器从编译C代码中生成的指令的研究。如果我们从naïve JavaScript或Haskell编译器会发出什么开始,我们会得到什么结果?

ACM队列的q戳相关文章
queue.acm.org

重新考虑稳健性原则
埃里克·奥尔曼
http://queue.acm.org/detail.cfm?id=1999945

并行编程的Erlang
吉姆•拉森
http://queue.acm.org/detail.cfm?id=1454463

CORBA的兴衰
Michi亨宁
http://queue.acm.org/detail.cfm?id=1142044

回到顶部

参考文献

1.Allen, S. RedLine Smalltalk。在国际Smalltalk会议(2011)上发表。

2.Boehm周宏儒。简单的garbage-collector-safety。ACM SIGPLAN通知, 5(1996), 8998。

3.Boehm周宏儒。和威瑟,M.垃圾收集在一个不合作的环境。软件实践与经验, 9(1988), 807820。

4.Bothner, P.和Tromey, T. Java/ c++集成(2001);http://per.bothner.com/papers/UsenixJVM01/CNI01.pdf/而且http://gcc.gnu.org/java/papers/native++.html/

5.奇斯纳尔,D. 2012。C语言世界中的Smalltalk。在Smalltalk技术国际研讨会论文集(2012), 4:14:12。

6.一个新的objective-C运行时:从研究到生产。ACM队列、(2012);http://queue.acm.org/detail.cfm?id=2331170/

7.Geoffray, N., Thomas, G., Lawall, J., Muller, G.和Folliot, B. VMKit:托管运行时环境的基板。ACM SIGPLAN通知, 7(2010), 5162。

8.Paz, H., Petrank, E., Bacon, D. F., Kolodner, E.K., Rajan, v.t。在14国会议记录th编译器结构国际会议.斯普林格-弗拉格,柏林,海德堡,2001,156171。

回到顶部

作者

David Chisnall他是剑桥大学的一名研究员,在那里他从事编程语言的设计和实现。他花了几年的时间做咨询,在此期间他还写了关于Xen、Objective-C和Go编程语言的书。他还为LLVM、Clang、FreeBSD、GNUstep和Étoilé开源项目做出了贡献。

回到顶部

脚注

根据合同FA8750-10-C-0237,部分工作由国防高级研究计划局(DARPA)和空军研究实验室(AFRL)赞助。本文中包含的观点、意见和/或研究结果仅为作者个人观点,不应被解释为代表DARPA或美国国防部的官方观点或政策,无论是明示的还是暗示的。

回到顶部

数据

F1图1。使用确定性自动释放锁的c++代码。

F2图2。Objective-C代码演示了在现有语言中改进垃圾收集的问题。

回到顶部


©2013 acm 0001-0782/13/12

允许为个人或课堂使用部分或全部作品制作数字或硬拷贝,但不得为盈利或商业利益而复制或分发,且副本在首页上附有本通知和完整的引用。除ACM外,本作品的其他组件的版权必须受到尊重。允许有信用的文摘。以其他方式复制、重新发布、在服务器上发布或重新分发到列表,都需要事先获得特定的许可和/或费用。请求发布的权限permissions@acm.org传真(212)869-0481。

数字图书馆是由计算机协会出版的。版权所有©2013 ACM, Inc.


没有找到条目

Baidu
map