Skip to content

JEP 276: Dynamic Linking of Language-Defined Object Models | 语言定义对象模型的动态链接

摘要

提供一种在 INVOKEDYNAMIC 调用位置中将高级对象操作(例如“读取属性”、“写入属性”、“调用可调用对象”等)表示为名称的机制。提供一个默认的链接器,用于处理普通 Java 对象上这些操作的通常语义,以及一种安装特定语言链接器的机制。

目标

主要目标是允许在编译时未知类型的表达式上进行高级对象操作的编译成 INVOKEDYNAMIC 指令,例如 obj.color 变为 INVOKEDYNAMIC "dyn:getProp:color"。提供的基础设施将在运行时将这些调用位置的链接请求分派给一组具有特定对象类型知识的链接器,可以为操作的适当实现生成方法句柄。

这些功能为使用在编译时不知道类型的对象表达式编写的程序的运行时实现提供了基础,并且需要在这些对象上表达典型的面向对象操作。

能够组合多个语言运行时链接器允许在单个 JVM 进程中协同存在的多个语言运行时之间合理地进行互操作性;当一个运行时的对象传递给另一个运行时时,由一种语言的编译器发出的 INVOKEDYNAMIC 调用位置应该可以由另一种语言的链接器进行链接。

非目标

我们不希望为任何单个编程语言或任何该类语言的执行环境提供操作的链接语义。我们不会更改 JVMS 中描述的调用位置引导机制;我们只是使用它。

动机

INVOKEDYNAMIC 为应用程序特定方法的链接提供了 JVM 级别的基础。它没有提供一种表达对象上的高级操作或实现这些操作的方法。这些操作是面向对象环境中的常规操作:属性访问、集合元素访问、构造函数调用、命名方法调用(可能具有多重派发,例如 Java 重载方法解析的链接和运行时等效)。在 JVM 上的语言中通常希望使用这些功能,然而每个语言实现传统上都必须单独重新实现它们。虽然这些操作的语义在不同语言之间有预期的变化(毕竟,它们需要有一些区分因子,否则它们将是相同的语言),但它们都有一个共同的需求:以与 Java 语言中的使用方式语义匹配的方式链接到 Java 平台对象(通常称为“Plain Old Java Objects”,即不属于或由语言运行时生成的 Java 类的实例)。

Nashorn 是此方法可行性的成功证明;当它遇到形式为 obj.foo 的表达式时,Nashorn 字节码编译器只需发出一个 INVOKEDYNAMIC "dyn:getProp:foo",然后在运行时将实现委托给动态链接器,以根据表达式的评估结果提供属性获取器的实现,具体取决于它是一个 JavaScript 对象、某个普通 Java 对象还是其他类型的对象。

从动态语言到 Java 的链接机制提供了几乎所有连接到语言自身所需的功能,Nashorn 就是如此:它具有统一的链接机制,将根据需要将链接请求分派给其自己的链接器或“普通 Java 对象”链接器。此外,如果运行时具有将链接架构统一到自身和 Java 对象的能力,链接到来自另一个语言运行时的对象在同一个 JVM 中实际上是免费的,提供了合理程度的跨语言互操作性。

这是为实现任何具有一定编译时类型不确定性的字节码系统(例如动态语言运行时)提供的有价值的共享功能。

描述

jdk.internal.dynalink.* 包中的代码目前作为 JDK 8 中 Nashorn 的内部依赖项包含一个工作实现;我们正在寻求增强并将其公开为一组名为 jdk.dynalink.* 的包,托管在一个名为 jdk.dynalink 的新模块中。

以下将描述当前的设计,理解这一部分随着工作的进展而演变。

操作

对象的操作被表达为 INVOKEDYNAMIC 指令,以其名称描述操作。

本 JEP 定义了下列操作。该 JEP 定义的所有操作都具有前缀 "dyn:",并打算为将来的 Dynalink 扩展保留此前缀。

对象属性的操作包括:

  • INVOKEDYNAMIC "dyn:getProp:<name>"(Object)Object 用于检索对象上命名属性的值。签名中的类型可以比 Object 更具体(也可以是原语)。
  • INVOKEDYNAMIC "dyn:getProp"(Object,Object)Object 用于检索对象上命名属性的值,其中接收器作为第一个参数传递,属性名称作为第二个参数传递。与前一个操作不同,在这里名称不是固定的。
  • INVOKEDYNAMIC "dyn:setProp:<name>"(Object, Object)void 用于在对象上设置命名属性的值,第一个参数为接收者,第二个参数为要设置的值。
  • INVOKEDYNAMIC "dyn:setProp"(Object,Object,Object)void 用于在对象上设置命名属性的值,其中第一个参数为接收器,第二个参数为属性名称,第三个参数为要设置的值。与前一个操作不同,在这里名称不是固定的。

集合元素的操作包括:

  • INVOKEDYNAMIC "dyn:getElem:<key>"(Object)Object 用于检索具有固定键的集合对象(数组、列表、映射等)的元素。在此形式中,键必须是字符串,因为它作为操作名称的一部分表示,但运行时允许将其解析为数字字面值,以使用数值索引链接到集合。
  • INVOKEDYNAMIC "dyn:getElem"(Object,Object)Object 用于检索集合对象的元素,其中接收器作为第一个参数传递,元素键作为第二个参数传递。
  • INVOKEDYNAMIC "dyn:setElem:<key>"(Object,Object)void 用于设置集合对象(数组、列表、映射等)的元素,接收者作为第一个参数传递,值作为第二个参数传递。在此形式中,键必须是字符串,因为它作为操作名称的一部分表示,但运行时允许将其解析为数字字面值,以使用数值索引链接到集合。
  • INVOKEDYNAMIC "dyn:setElem"(Object,Object,Object)void 用于设置集合对象(数组、列表、映射等)的元素,其中第一个参数为接收器,第二个参数为元素键,第三个参数为要设置的值。

方法调用和对象创建的操作包括:

  • INVOKEDYNAMIC "dyn:getMethod:<name>"(Object)Object 用于检索对象的命名方法。
  • INVOKEDYNAMIC "dyn:call"(Object, Object...)Object 用于调用可调用对象(例如,通过较早的 dyn:getMethod 调用检索到的内容),第一个参数为要调用的可调用对象,可选的其他参数传递给它。根据情况,操作的第二个参数通常是 "this" 对象。
  • INVOKEDYNAMIC "dyn:callMethod:<name>"(Object, Object...)Object 用于调用对象上的命名方法,其第一个参数为具有命名方法的接收者,可选的其他参数传递给调用。此操作可以通过将 dyn:getMethod 折叠到 dyn:call 中来实现。仍需要单独的查找和调用操作,因为一些语言的语义要求它们是分开的步骤。
  • INVOKEDYNAMIC "dyn:new"(Object, Object...)Object 用于调用传递的可调用对象,就像它是构造函数一样,第一个参数为构造函数对象,可选的其他参数传递给它。某些语言允许将可调用对象作为普通调用和构造函数调用,因此需要与 dyn:call 不同的操作。

链接机制

系统的入口点是 INVOKEDYNAMIC,通常为引导方法。引导方法需要访问动态链接器。引导方法将创建 RELINKABLE CALL SITE 的实例,并告知其动态链接器对其进行初始化。

动态链接器是最终协调链接的对象。当动态链接器初始化 RELINKABLE CALL SITE 时,它将该站点的目标设置为其自己的 "relink" 方法的方法句柄,该方法封装了将在立即后续的第一次调用站点上触发的实际重链接算法。

当调用 relink 方法时,它将创建 LINK REQUEST,该对象包含调用站点的名称和签名,以及触发链接的调用的实际参数。通过携带实际调用参数,它提供了比引导方法可用信息更多的信息给链接器。LINK REQUEST 传递给动态链接器管理的 GUARDING DYNAMIC LINKER。

GUARDING DYNAMIC LINKER 是可为特定类别的对象(例如,实例化为相同 JVM 类的所有对象)提供链接的对象。GUARDING DYNAMIC LINKER 产生的链接通常是条件的,意味着它受到一个布尔谓词的保护,该谓词范围内的有效性(例如,“只要接收者是 List.class 的实例”,或者“只要接收者的类是数组类”等)。能够处理当前 LINK REQUEST 的 GUARDING DYNAMIC LINKER 将产生一个 GUARDED INVOCATION,即实现操作的方法句柄、实现 guard 的方法句柄以及可选的切换点集,用于异步使链接失效。

当语言运行时实例化动态链接器并在其类的引导方法中使用时,它将将其自己的 GUARDING DYNAMIC LINKER 实例作为应该管理的明显链接器之一传递给它(或者传递多个链接器,因为语言可以具有多个链接器;它可以自由地将其链接功能模块化为几个类;Nashorn 当前定义了八个)。动态链接器将获取这些传递的链接器,通常添加一个“bean 链接器”作为链接普通 Java 对象的回退,并实例化并添加用于其他语言的链接器,这些语言可能通过语言运行时的类加载器使用 java.util.ServiceLoader 机制可见。

动态链接器将从其咨询的 GUARDING DYNAMIC LINKER 中获取 GUARDED INVOCATION,并将其提供给 RELINKABLE CALL SITE。可以参与此高级链接的调用站点类应实现 RelinkableCallSite 接口,该接口定义了一个 "relink" 方法,允许它们接收 GUARDED INVOCATION 并将其纳入其当前链接中。

动态链接器的职责是充当管理的 GUARDING DYNAMIC LINKER 和需要(重新)链接的 RELINKABLE CALL SITE 之间的协调者。此设计将 "what" 链接(通常为接收者语言的链接器确定)与 "how"(调用方语言运行时确定的调用站点实现的责任)分开处理。

可重定向调用站点

最简单的可重定向调用站点是单态可重定向调用站点,每次重新链接调用时,它会丢弃其当前链接,并使用传递的 GUARDED INVOCATION 的保护句柄作为“测试”句柄,其调用句柄作为“目标”句柄,并使用指向 DYNAMIC LINKER 的重新链接方法的方法句柄作为其“回退”句柄,创建 MethodHandles.guardWithTest() 组合器。这样,在每次调用时,如果参数未通过保护(即当前链接的调用不足够适合它们),则调用站点将为这些参数重新链接。 (最后,如果 GUARDED INVOCATION 还带有非空的 Switch point,则单态调用站点还将将结果组合成 SwitchPoint.guardWithTest() 作废组合器,当它作废时再次回到重新链接。)

当然,还存在更复杂的调用站点类,例如,链接到其中可以在任何时候包含几种不同方法的链接的链接调用站点。这可以通过构造一系列级联的 guardWithTest 组合器来进一步增强。它可以通过包含分析信息并定期重新链接自身来进一步增强,其中包含按被调用次数从多到少排序的调用。

值转换

这个基础设施的另一个方面是我们之前没有提到但对于任何动态语言都至关重要的值转换支持。java.lang.invoke API 已经支持 Java 语言规范描述的“方法调用转换”作为 MethodHandle.asType() 的一部分,但我们需要为允许额外隐式转换的语言做好准备,例如从 int 转换为 java.lang.String。如果调用站点链接到希望 String 参数的方法句柄,但该调用站点为该参数类型为“int”(或更可能是在字节码中将其类型化为“Object”,但实际参数值在调用中可能出现为 Integer),则链接器必须使用 MethodHandles.filterArguments() 插入特定于语言的类型转换。

类似于 DYNAMIC LINKER,系统需要有 TYPE CONVERTER FACTORY,将一组 GUARDING TYPE CONVERTER FACTORY 绑定在一起。类似于 GUARDING DYNAMIC LINKER,GUARDING TYPE CONVERTER FACTORY 会生成一个带有保护的方法句柄,该方法句柄可以在指定源和目标类型之间转换值。由于调用站点通常对于大多数参数都类型化为“Object”,因此请求的转换器将是“Object to String”、“Object to int”等。语言运行时将不得不提供转换器方法句柄以及确定其是否适用的保护,例如,方法句柄将对应于“这是我通常用于我认可的对象的一般方式将 Object 转换为 int”,而保护将回答“实际参数对象是否是我可以识别的类型?”问题。

最后,这个谜题的最后一块,通常仅用于选择重载 Java 方法,是 CONVERSION COMPARATOR。现在引入了特定于语言的转换,当我们需要链接到 Java 类上的方法时,我们实际上可能会得到比 Java 语言允许的更广泛的适用方法集,因为新的转换可以使更多的方法适用。如果我们尝试仅使用 JLS 解析规则从这个扩展的适用重载集中选择,我们经常会失败并出现歧义。因此,引入新的类型转换也引入了新的转换排名规则的需要。GUARDING TYPE CONVERTER FACTORY 可以可选地实现 CONVERSION COMPARATOR 的接口,这将由动态重载方法解析逻辑咨询,在调用站点 T 的参数的源类型和与之相同位置的被比较候选方法 U1 和 U2 的目标类型之间传递,并且它必须决定首选哪一个转换 T-to-U1 或 T-to-U2。

语言实现者将需要实现他们语言的 GUARDING DYNAMIC LINKER,如果他们的语言允许比 JLS 允许的更多隐式转换,则还将需要 GUARDING TYPE CONVERTER FACTORY,最后在大多数情况下,他们还需要一个 CONVERSION COMPARATOR。实际上,我们应该调查一下,CONVERSION COMPARATOR 功能是否应该成为 GUARDING TYPE CONVERTER FACTORY 的强制性部分。

链接失败

如果没有 GUARDING DYNAMIC LINKER 成功链接操作,DYNAMIC LINKER 将抛出 NoSuchDynamicMethodException 类型的异常,这是 RuntimeException 的子类。如果 GUARDING DYNAMIC LINKER 可以明确地确定在调用它将被链接的参数时操作将失败,它可以立即抛出特定于语言的异常,或者它可以生成一个 GUARDED INVOCATION,在通过保护时抛出异常。

Beans linker

该库包含一个预定义的 GUARDING DYNAMIC LINKER,名为 BeansLinker,它实现了普通 Java 对象上所有支持的操作(属性映射到 getter/setter 方法或公共字段;Java 类型(与 Class 对象不同)作为可以用作构造函数和静态字段和方法的对象公开;数组、列表和映射作为集合操作)。

备选方案

这个问题也可以使用接口注入来解决,但这目前不是 JVM 中的工作特性。在这种情况下,代码将包含针对预定义接口的 INVOKEINTERFACE 指令,这些接口定义实现这些操作的方法。我们的方法更加通用,因为我们不仅仅基于接收方的类型限制链接行为(虽然通常是这样),而且我们的方法允许轻松松散耦合的链接器行为组合。例如,DOM 节点可以与 DOM 链接器和普通 Java 链接器的组合链接在一起,其中 DOM 节点链接器将操作映射到 XML InfoSet 语义,但对于非 DOM 操作则会回退到普通的 Java 链接。

另外还有一个有趣的想法是,设计一种不需要 INVOKEDYNAMIC 的系统,而是具有按需自适应重新编译功能,以便为调用站点遇到的不同类型的对象改变代码形状。同样,在 JVM 中我们没有这样的系统,而 INVOKEDYNAMIC 特别设计为这些系统的替代方案。

测试

Nashorn 已经使用了该库的内部版本,并预计转换为使用本 JEP 提出的版本(请参见依赖项部分)。该库已通过 Nashorn 测试进行了充分的练习。原始外部(托管在 GitHub 上)版本的库中还存在提供相当良好的覆盖率的其他单元测试,但这些测试没有随着库内部化到 jdk.internal.dynalink.* 包而被迁移。这些测试可以被采用。