JEP 276:语言定义的对象模型的动态链接
概括
提供用于链接对象上的高级操作的工具,例如“读取属性”、“写入属性”、“调用可调用对象”等,在 INVOKEDYNAMIC 调用站点中表示为名称。为普通 Java 对象上的这些操作的常用语义提供默认链接器,以及用于安装特定于语言的链接器的工具。
目标
主要目标是允许将编译时类型未知的表达式上的高级对象操作编译为 INVOKEDYNAMIC 指令,例如obj.color
变为INVOKEDYNAMIC "dyn:getProp:color"
。所提供的基础设施将在运行时将这些调用站点的链接请求分派给一组了解特定对象类型的链接器,并且可以生成用于适当实现操作的方法句柄。
这些设施为用具有对象表达式概念的语言编写的程序提供了运行时的实现基础,这些对象表达式的类型在编译时未知,并且需要在此类对象上表达典型的面向对象操作。
组合多个语言运行时链接器的能力允许在单个 JVM 进程中共存的多个语言运行时之间实现合理的互操作性;当属于一个运行时的对象传递到另一个运行时时,由一种语言的编译器发出的 INVOKEDYNAMIC 调用站点应该可以由另一种语言的链接器链接。
非目标
我们不希望为任何单一编程语言或任何此类语言的执行环境提供操作的链接语义。我们不会改变 JVMS 描述的用于引导调用站点的机制;我们只是使用它。
动机
INVOKEDYNAMIC 为特定于应用程序的方法链接提供了 JVM 级基础。它不提供表达对对象的高级操作的方法,也不提供实现它们的方法。这些操作是面向对象环境中的常见操作方案:属性访问、集合元素的访问、构造函数的调用、命名方法的调用(可能具有多重分派,例如 Java 重载方法的链接时和运行时等效项)解决)。这些都是 JVM 上的语言通常需要的所有功能,但传统上每种语言实现都必须单独重新发明它们。虽然不同语言之间的此类操作的语义存在预期的可变性(毕竟它们需要一些鉴别器,否则它们都是相同的语言),但它们都有一个共同的需求:链接到 Java 平台对象(通常是(称为“普通旧 Java 对象”,即不属于语言运行时或由语言运行时生成的 Java 类的实例),其方式在语义上与它们在 Java 语言中的用法相匹配。
Nashorn 成功证明了该方法的可行性;当它遇到形式的表达式时,例如obj.foo
,Nashorn 字节码编译器可以只发出INVOKEDYNAMIC "dyn:getProp:foo"
, 然后在运行时推迟到动态链接器,根据表达式是否计算为 JavaScript 对象来提供属性 getters 的实现,一些简单的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 一样,系统的入口点是引导方法。引导方法需要访问动态链接器。引导方法将创建可重新链接调用站点的实例,并告诉其动态链接器对其进行初始化。
动态链接器是最终协调链接的对象。当动态链接器初始化可重新链接调用站点时,它将站点的目标设置为其自己的“重新链接”方法的方法句柄,该方法封装了将在调用站点的紧随其后的首次调用时触发的实际重新链接算法。
调用 relink 方法时,它将创建一个 LINK REQUEST,它是一个包含调用站点的名称和签名以及触发链接的调用的实际参数的对象。通过携带实际的调用参数,它为链接器提供了比引导方法可用的更多信息。 LINK REQUEST 被传递到 DYNAMIC LINKER 管理的 GUARDING DYNAMIC LINKER。
GUARDING DYNAMIC LINKER 是一个可以为特定类对象(例如,为同一 JVM 类实例化的所有对象)提供链接的对象。 GUARDING DYNAMIC LINKER 生成的链接通常是有条件的,这意味着它由限定其有效性范围的布尔谓词保护(例如,“只要接收者是...的实例List.class
”,或“只要接收者的类是数组类” “, ETC。)。可以处理当前链接请求的保护动态链接器将产生一个保护调用,即实现该操作的方法句柄的三元组,一个实现保护的方法句柄,以及可选的用于异步无效的切换点的集合。连锁。
当语言运行时实例化一个动态链接器时,它将在其类的引导方法中使用,它将向其传递一个自己的 GUARDING DYNAMIC LINKER 实例,作为它应该管理的明显链接器之一。 (或者传递几个链接器,因为一种语言可以有多个链接器;它可以自由地将其链接功能模块化为多个类;Nashorn 当前定义了八个。)动态链接器将采用这些传递的链接器,通常添加一个“bean 链接器”作为链接普通 Java 对象的后备,还可以使用该java.util.ServiceLoader
机制实例化和添加其他语言的链接器,这些链接器可能通过语言运行时的类加载器可见。
动态链接器将采用由其咨询的保护动态链接器之一生成的保护调用,并将其提供给可重新链接调用站点。可以参与此高级链接的调用站点类预计将实现RelinkableCallSite
定义“重新链接”方法的接口,该方法允许它们接收 GUARDED INVOCATION 并将它们合并到当前链接中。
动态链接器的职责是充当其管理的保护动态链接器和需要(重新)链接的可重新链接调用站点之间的协调器。这种设计将“链接什么”的关注点与“如何”链接的关注点分开,链接器由链接器(通常是接收者语言的链接器)决定,“如何链接”是调用站点实现的责任,由语言运行时确定呼叫者的)。
可重新链接的调用站点
最简单的此类调用站点是单态可重新链接调用站点,它在每次重新链接调用时都会丢弃其当前链接,并MethodHandles.guardWithTest()
使用传递的 GUARDED INVOCATION 的保护句柄作为“测试”句柄、其调用句柄作为“目标”句柄来创建组合器,以及一个方法句柄,该方法句柄指向动态链接器的重新链接方法作为其“后备”句柄。这样,在每次调用时,如果参数未通过防护 - 意味着当前链接的调用不足以满足它们 - 将为这些参数重新链接调用站点。 (最后,如果 GUARDED INVOCATION 还带有一个非空 Switch 点,那么单态调用站点将进一步将生成的组合器与SwitchPoint.guardWithTest()
失效组合器组合在一起,当它失效时再次回退到重新链接。)
当然,还存在更复杂的调用站点类,例如,通过构造组合器的级联链,可以包含随时链接到其中的多个不同方法的链式调用站点guardWithTest
。它可以通过包含分析信息并定期将其自身重新链接到包含从最多调用到最少调用等顺序的调用的新链来进一步增强。
价值转换
我们之前没有提到但对于任何动态语言都至关重要的该基础设施的另一个方面是对值转换的支持。 APIjava.lang.invoke
已经支持“方法调用转换”,如 Java 语言规范中所描述的MethodHandle.asType()
,但我们需要为允许额外隐式转换的语言做好准备,例如从int
到java.lang.String
。如果调用站点链接到需要 String 参数的方法句柄,但调用站点对该参数键入“int”(或者更可能在字节码中键入“Object”,但 Integer 可以作为实际值出现在此处)调用中的参数值),链接器必须使用 插入特定于语言的类型转换MethodHandles.filterArguments()
。
与动态链接器类似,系统需要有一个类型转换器工厂,它将一组保护类型转换器工厂连接在一起。与 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,最后在大多数情况下还提供一个 GUARDING TYPE CONVERTER FACTORY 的 CONVERSION COMPARATOR 。实际上,我们应该研究一下 CONVERSION COMPARATOR 功能是否应该成为 GUARDING TYPE CONVERTER FACTORY 的强制部分。
链接失败
如果没有 GUARDING DYNAMIC LINKER 成功链接操作,则 DYNAMIC LINKER 将抛出类型为 的异常NoSuchDynamicMethodException
,它是 的子类RuntimeException
。如果 GUARDING DYNAMIC LINKER 可以明确地确定操作在为其链接的参数调用时会失败,那么它本身可以立即抛出特定于语言的异常,也可以生成 GUARDED INVOCATION,在通过防护时抛出异常。
Bean 链接器
该库包含一个预定义的 GUARDING DYNAMIC LINKER ,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 内的特权代码传送、访问并分发方法句柄来隐藏安全错误。
不过,我们确实将其限制为仅对导出包中的公共类的公共成员进行操作。内置 bean 链接器使用 发现方法句柄MethodHandles.publicLookup()
,然后出于性能原因在内部缓存并重用它们,标记为调用者敏感的方法的调用除外;这些总是通过MethodHandles.Lookup
传递给引导方法来查找并且从不缓存。
依赖关系
Nashorn JavaScript 引擎 ( JEP 174 ) 预计将在此 JEP 的基础上重新构建,因为jdk.internal.dynalink
JDK 8 中的软件包将在 JDK 9 中停用。预计更改很小,因为我们将努力尽量减少与现有 API 的偏差在合理的范围内。