跳到主要内容

JEP 276:语言定义对象模型的动态链接

QWen Max 中英对照

概述

提供一种机制,用于将对象上的高级操作(例如“读取属性”、“写入属性”、“调用可调用对象”等)与 INVOKEDYNAMIC 调用站点中表达的名称进行链接。为普通 Java 对象上的这些操作提供默认的链接器以支持其常见语义,同时提供安装语言特定链接器的功能。

目标

主要目标是允许对编译时类型未知的表达式进行高级对象操作的编译,将其编译为 INVOKEDYNAMIC 指令。例如,obj.color 会被转换为 INVOKEDYNAMIC "dyn:getProp:color"。所提供的基础设施将在运行时把这些调用点的链接请求分派给一组了解特定对象类型的链接器,这些链接器能够生成用于适当实现这些操作的方法句柄。

这些设施为实现以下语言的程序运行时提供了基础:这类语言具有对象表达式的概念,其类型在编译时未知,需要对这样的对象表达典型的面向对象操作。

组合多种语言运行时链接器的能力使得在单个 JVM 进程中共存的多种语言运行时之间具有相当程度的互操作性;当一个属于某运行时的对象被传递给另一个运行时,一种语言的编译器发出的 INVOKEDYNAMIC 调用点应该能够被另一种语言的链接器链接。

非目标

我们不希望为任何单一编程语言或其执行环境的操作提供链接语义。我们不会改变 JVMS(Java 虚拟机规范)中描述的用于引导调用站点的机制;我们只是使用它。

动机

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 8 中作为 Nashorn 的内部依赖项提供的 jdk.internal.dynalink.* 包中的代码包含一个有效的实现;我们希望对其进行增强并将其作为一组名为 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 时一样,是一个引导方法(bootstrap method)。该引导方法需要能够访问一个 动态链接器(DYNAMIC LINKER)。引导方法会创建一个 可重链接调用点(RELINKABLE CALL SITE) 的实例,并告诉它的 动态链接器(DYNAMIC LINKER) 对其进行初始化。

动态链接器是一个最终协调链接的对象。当动态链接器初始化一个可重链接调用点时,它会将该调用点的目标设置为自己的“重链接”方法的 Method Handle,这个方法封装了实际的重链接算法,该算法将在紧接着的、调用点的第一次调用时被触发。

当调用 relink 方法时,它会创建一个 LINK REQUEST,这是一个包含调用点名称、签名以及触发链接的实际调用参数的对象。通过携带实际的调用参数,它为链接器提供了比引导方法(bootstrap method)可用的更多信息。LINK REQUEST 会被传递给由 DYNAMIC LINKER 管理的 GUARDING DYNAMIC LINKER

GUARDING DYNAMIC LINKER 是一种可以为特定类别的对象提供链接的对象(例如,为同一个 JVM 类实例化的所有对象)。GUARDING DYNAMIC LINKER 生成的链接通常是条件性的,这意味着它的有效性由一个布尔谓词来保护(例如,“只要接收者是 List.class 的实例”,或者“只要接收者的类是一个数组类”等)。能够处理当前 LINK REQUEST 的 GUARDING DYNAMIC LINKER 将生成一个 GUARDED INVOCATION,即一个三元组:实现操作的方法句柄、实现保护条件的方法句柄,以及用于异步失效链接的开关点集合(可选)。

当一个语言运行时实例化一个它将在其类的引导方法中使用的动态链接器(DYNAMIC LINKER)时,它会传递一个自己的保护性动态链接器(GUARDING DYNAMIC LINKER)实例作为其应该管理的显而易见的链接器之一。(或者传递多个链接器,因为一种语言可以有不止一个链接器;它可以自由地将其链接功能模块化为多个类;Nashorn 当前定义了八个。)该动态链接器(DYNAMIC LINKER)将接收这些传递的链接器,通常还会添加一个“beans 链接器”作为链接普通 Java 对象的后备方案,并且还会实例化并添加其他可能通过语言运行时的类加载器使用 java.util.ServiceLoader 机制可见的语言的链接器。

动态链接器(DYNAMIC LINKER)会获取由其咨询的某个保护动态链接器(GUARDING DYNAMIC LINKER)生成的受保护调用(GUARDED INVOCATION),并将其交给可重链接调用点(RELINKABLE CALL SITE)。能够参与这种高级链接的调用点类需要实现 RelinkableCallSite 接口,该接口定义了一个“relink”方法,允许它们接收受保护调用(GUARDED INVOCATION)并将其整合到当前的链接中。

DYNAMIC LINKER 的职责是作为它所管理的 GUARDING DYNAMIC LINKER 与其需要(重新)链接的 RELINKABLE CALL SITE 之间的协调者。这种设计将“链接什么”的关注点(由链接器决定,通常是接收方语言的链接器)与“如何链接”分开,后者是调用方语言运行时确定的调用站点实现的责任)。

可重链接的调用点

最简单的这种调用点是一个单态可重链接的调用点,它在每次重链接调用时都会丢弃当前的链接,并使用传递的 GUARDED INVOCATION 的保护句柄作为“测试”句柄、其调用句柄作为“目标”句柄,以及一个指向 DYNAMIC LINKER 的重链接方法的方法句柄作为“回退”句柄,创建一个 MethodHandles.guardWithTest() 组合器。通过这种方式,在每次调用时,如果参数未通过保护检查(意味着当前链接的调用不适用于这些参数),调用点将为这些参数重新进行链接。(最后,如果 GUARDED INVOCATION 还携带了一个非空的切换点,单态调用点还会将生成的组合器与 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 FACTORIES(带保护的类型转换工厂)绑定在一起。与 GUARDING DYNAMIC LINKER(带保护的动态链接器)类似,GUARDING TYPE CONVERTER FACTORY 会生成一个受保护的方法句柄,该句柄可以在指定的源类型和目标类型之间进行值转换。这个方法句柄必须附带一个保护条件,因为在许多情况下,调用站点的参数通常会被声明为 "Object" 类型,因此所需的转换器将是 "Object 转 String"、"Object 转 int" 等等。语言运行时需要同时提供一个转换器方法句柄以及一个用于判断其是否适用的保护条件。例如,方法句柄可以表示“这是我为我识别的对象从 Object 转换为 int 的通用方式”,而保护条件则负责回答“实际参数对象是否是我识别的类型?”这个问题。

拼图的最后一部分,通常仅在选择重载的 Java 方法时需要的是一个转换比较器(CONVERSION COMPARATOR)。现在我们引入了语言特定的转换,当我们需要链接到 Java 类中的方法时,实际上可能会得到比 Java 语言本身允许的更广泛的方法集合,因为新的转换会使更多方法变得适用。如果我们试图仅使用 JLS(Java Language Specification)解析规则在这扩展的适用重载方法集中进行选择,往往会因歧义而失败。因此,新类型转换的引入也带来了对新转换排序规则的需求。一个**保护类型转换工厂(GUARDING TYPE CONVERTER FACTORY)可以可选地实现一个用于转换比较器(CONVERSION COMPARATOR)**的接口,该接口会被动态重载方法解析逻辑调用,传入调用点参数的源类型 T 和被比较候选方法中相同位置参数的目标类型 U1 和 U2,并且必须决定是 T 到 U1 的转换还是 T 到 U2 的转换更为优先。

语言实现者需要为他们的语言实现一个GUARDING DYNAMIC LINKER,如果其语言允许比 JLS(Java Language Specification)更多的隐式转换,则还需要可选地实现一个GUARDING TYPE CONVERTER FACTORY,并且在大多数情况下,如果他们提供了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 链接器

该库包含一个预定义的守护动态链接器,名为 BeansLinker,它实现了对普通 Java 对象的所有支持操作(属性被映射到 getter/setter 方法或公共字段;Java 类型(不同于 Class 对象)被暴露为可用作构造函数及静态字段和方法持有者的对象;数组、列表和映射则充当集合)。

替代方案

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

另外,考虑一个完全不需要 INVOKEDYNAMIC 的系统也十分有趣,该系统可以通过按需自适应重新编译来调整代码,以适应调用点遇到的不同类型的对象。然而,目前的 JVM 中并不存在这样的系统,而 INVOKEDYNAMIC 正是被设计为这类系统的一种替代方案。

测试

Nashorn 已经在使用该库的内部版本,并且预计将过渡到使用此 JEP 提出的版本(参见依赖关系部分)。通过 Nashorn 测试,这个库已经得到了充分的实践。在该库外部(GitHub 托管)的原始版本中存在额外的单元测试,提供了相当不错的覆盖率,但在将该库内部化到 jdk.internal.dynalink.* 包时这些测试并未被迁移。这些测试可以被采纳。

风险与假设

该库旨在对 JVM 上的各种语言实现都有用。如果我们没有收到足够的反馈,某些语言的某些需求可能无法得到满足;另一方面,针对任何此类额外需求,验证其是否存在潜在的范围蔓延也很重要。

由于它提供了对类上方法的动态访问,因此它可能成为有吸引力的攻击目标。我们非常清楚这些方面,并花费了大量时间进行推理和设计,以防止任何访问或其他权限提升。也就是说,据我们所知,它不包含任何安全漏洞,但由于它是特权代码,随 JRE 一起发布,访问并分发方法句柄,因此有可能隐藏安全漏洞。

不过,我们确实将其限制为只能在导出包中的公共类的公共成员上运行。内置的 beans 链接器使用 MethodHandles.publicLookup() 来发现方法句柄,然后为了性能原因在内部缓存并重用它们,但调用被标记为调用者敏感的方法除外;这些方法总是通过传入引导方法的 MethodHandles.Lookup 查找,并且从不缓存。

依赖

Nashorn JavaScript 引擎(JEP 174)预计将基于此 JEP 进行调整,因为 JDK 8 中的 jdk.internal.dynalink 包将在 JDK 9 中被弃用。这一变化预计会很小,因为我们将会尽力将对现有 API 的偏离降到合理范围内的最低程度。