静态对象模型

本指南演示了如何开始使用 StaticShapeStaticProperty API,这些 API 是在 GraalVM 21.3.0 中引入的。完整的文档可以在 Javadoc 中找到。

动机 #

静态对象模型提供抽象来表示对象的布局,这些对象的布局一旦定义,其属性的数量和类型就不会改变。它特别适合,但不仅限于,静态编程语言对象模型的实现。其 API 定义了对象布局 (StaticShape),执行属性访问 (StaticProperty) 以及分配静态对象 (DefaultStaticObjectFactory)。实现效率高,并在属性访问上执行安全检查,如果语言实现(例如,通过验证器)已经执行了这些检查,则可以禁用这些检查。

静态对象模型不提供构造来模拟属性的可见性,也不区分静态属性和实例属性。其 API 与 动态对象模型 的 API 不兼容,后者更适合动态语言。

入门 #

在这个第一个例子中,让我们假设

  1. language 是我们正在实现的 TruffleLanguage 的实例。
  2. 我们希望表示一个具有以下静态布局的对象
    • 一个名为 property1int 属性。
    • 一个名为 property2Object 属性,可以存储为最终字段。稍后我们将详细了解这意味着什么。

以下是如何使用静态对象模型来表示此布局

public class GettingStarted {
    public void simpleShape(TruffleLanguage<?> language) {
        StaticShape.Builder builder = StaticShape.newBuilder(language);
        StaticProperty p1 = new DefaultStaticProperty("property1");
        StaticProperty p2 = new DefaultStaticProperty("property2");
        builder.property(p1, int.class, false);
        builder.property(p2, Object.class, true);
        StaticShape<DefaultStaticObjectFactory> shape = builder.build();
        Object staticObject = shape.getFactory().create();
        ...
    }
}

我们首先创建一个 StaticShape.Builder 实例,传递对我们正在实现的语言的引用。然后,我们创建 DefaultStaticProperty 实例,这些实例表示我们要添加到静态对象布局中的属性。作为参数传递的字符串 ID 在构建器中必须是唯一的。创建完属性后,我们将它们注册到构建器实例

  • 第一个参数是我们注册的 StaticProperty
  • 第二个参数是属性的类型。它可以是基本类或 Object.class
  • 第三个参数是一个布尔值,它定义了属性是否可以存储为最终字段。这给了编译器执行额外优化的机会。例如,对该属性的读取可能被常量折叠。重要的是要注意,静态对象模型不检查作为最终存储的属性是否只被分配了一次,以及它是否在读取之前被分配。这样做可能会导致程序的行为错误,由用户来确保这种情况不会发生。然后,我们通过调用 builder.build() 来创建一个新的静态形状。要分配静态对象,我们从形状中检索 DefaultStaticObjectFactory,并调用其 create() 方法。

现在我们有了静态对象实例,让我们看看如何使用静态属性来执行属性访问。扩展上面的例子

public class GettingStarted {
    public void simpleShape(TruffleLanguage<?> language) {
        ...
        p1.setInt(staticObject, 42);
        p2.setObject(staticObject, "42");
        assert p1.getInt(staticObject) == 42;
        assert p2.getObject(staticObject).equals("42");
    }
}

形状层次结构 #

可以通过声明新形状应扩展现有形状来创建形状层次结构。这是通过在创建子形状时将父形状作为参数传递给 StaticShape.Builder.build(StaticShape) 来实现的。然后,可以使用父形状的属性来访问存储在子形状的静态对象中的值。

在以下示例中,我们创建了一个与 上一节 中讨论的形状相同的父形状,然后我们用一个子形状对其进行扩展,该子形状隐藏了父形状的一个属性。最后,我们演示了如何访问各种属性。

public class Subshapes {
    public void simpleSubShape(TruffleLanguage<?> language) {
        // Create a shape
        StaticShape.Builder b1 = StaticShape.newBuilder(language);
        StaticProperty s1p1 = new DefaultStaticProperty("property1");
        StaticProperty s1p2 = new DefaultStaticProperty("property2");
        b1.property(s1p1, int.class, false).property(s1p2, Object.class, true);
        StaticShape<DefaultStaticObjectFactory> s1 = b1.build();

        // Create a sub-shape
        StaticShape.Builder b2 = StaticShape.newBuilder(language);
        StaticProperty s2p1 = new DefaultStaticProperty("property1");
        b2.property(s2p1, int.class, false);
        StaticShape<DefaultStaticObjectFactory> s2 = b2.build(s1); // passing a shape as argument builds a sub-shape

        // Create a static object for the sub-shape
        Object o2 = s2.getFactory().create();

        // Perform property accesses
        s1p1.setInt(o2, 42);
        s1p2.setObject(o2, "42");
        s2p1.setInt(o2, 24);
        assert s1p1.getInt(o2) == 42;
        assert s1p2.getObject(o2).equals("42");
        assert s2p1.getInt(o2) == 24;    }
}

扩展自定义基类 #

为了减少内存占用,语言实现者可能希望静态对象扩展代表客体级别对象的类。这很复杂,因为 StaticShape.getFactory() 必须返回分配静态对象的工厂类的实例。为了实现这一点,我们首先需要声明一个接口,该接口

  • 为我们想要调用的静态对象超类的每个可见构造函数定义一个方法。
  • 每个方法的参数必须与相应构造函数的参数匹配。
  • 每个方法的返回类型必须可从静态对象超类分配。

例如,如果静态对象应扩展此类

public abstract class MyStaticObject {
    final String arg1;
    final Object arg2;

    public MyStaticObject(String arg1) {
        this(arg1, null);
    }

    public MyStaticObject(String arg1, Object arg2) {
        this.arg1 = arg1;
        this.arg2 = arg2;
    }
}

我们需要声明以下工厂接口

public interface MyStaticObjectFactory {
    MyStaticObject create(String arg1);
    MyStaticObject create(String arg1, Object arg2);
}

最后,以下是如何分配自定义静态对象

public void customStaticObject(TruffleLanguage<?> language) {
    StaticProperty property = new DefaultStaticProperty("arg1");
    StaticShape<MyStaticObjectFactory> shape = StaticShape.newBuilder(language).property(property, Object.class, false).build(MyStaticObject.class, MyStaticObjectFactory.class);
    MyStaticObject staticObject = shape.getFactory().create("arg1");
    property.setObject(staticObject, "42");
    assert staticObject.arg1.equals("arg1"); // fields of the custom super class are directly accessible
    assert property.getObject(staticObject).equals("42"); // static properties are accessible as usual
}

从上面的示例中可以看出,自定义父类的字段和方法可以直接访问,并且不会被静态对象的静态属性隐藏。

减少内存占用 #

阅读 Javadoc,您可能已经注意到 StaticShape 没有提供访问关联静态属性的 API。如果语言实现已经有一种存储此信息的方法,这将减少内存占用。例如,Java 语言的实现可能希望将静态形状存储在代表 Java 类的类中,并将静态属性存储在代表 Java 字段的类中。在这种情况下,代表 Java 类的类应该已经有一种方法来检索与其关联的 Java 字段,因此与形状关联的静态属性。为了进一步减少内存占用,语言实现者可能希望代表 Java 字段的类扩展 StaticProperty

而不是将静态属性存储在代表字段的类中

class MyField {
    final StaticProperty p;

    MyField(StaticProperty p) {
        this.p = p;
    }
}

new MyField(new DefaultStaticProperty("property1"));

代表字段的类可以扩展 StaticProperty

class MyField extends StaticProperty {
    final Object name;

    MyField(Object name) {
        this.name = name;
    }

    @Override
    public String getId() {
        return name.toString(); // this string must be a unique identifier within a Builder
    }
}

new MyField("property1");

安全检查 #

在属性访问上,静态对象模型执行两种类型的安全检查

  1. StaticProperty 方法与静态属性的类型匹配。

错误访问的示例

public void wrongMethod(TruffleLanguage<?> language) {
    StaticShape.Builder builder = StaticShape.newBuilder(language);
    StaticProperty property = new DefaultStaticProperty("property");
    Object staticObject = builder.property(property, int.class, false).build().getFactory().create();

    property.setObject(staticObject, "wrong access type"); // throws IllegalArgumentException
  1. 传递给访问器方法的对象与构建器生成的形状匹配,或者与构建器生成的形状的其中一个子形状匹配。

错误访问的示例

public void wrongShape(TruffleLanguage<?> language) {
    StaticShape.Builder builder = StaticShape.newBuilder(language);
    StaticProperty property = new DefaultStaticProperty("property");;
    Object staticObject1 = builder.property(property, Object.class, false).build().getFactory().create();
    Object staticObject2 = StaticShape.newBuilder(language).build().getFactory().create();

    property.setObject(staticObject2, "wrong shape"); // throws IllegalArgumentException
}

虽然这些检查通常很有用,但如果语言实现已经执行了这些检查(例如,使用验证器),则它们可能是多余的。虽然第一类检查(对属性类型的检查)非常高效,无法禁用,但第二类检查(对形状的检查)在计算上比较昂贵,可以通过命令行参数禁用

--experimental-options --engine.RelaxStaticObjectSafetyChecks=true

或在创建 Context 时禁用

Context context = Context.newBuilder() //
                         .allowExperimentalOptions(true) //
                         .option("engine.RelaxStaticObjectSafetyChecks", "true") //
                         .build();

在没有其他等效检查的情况下,强烈建议不要放松安全检查。如果关于静态对象形状正确性的假设是错误的,VM 很可能会崩溃。

联系我们