靜態物件模型

本指南示範如何開始使用 GraalVM 21.3.0 引入的 StaticShapeStaticProperty API。完整文件可以在 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();

強烈建議在沒有其他等效檢查的情況下放寬安全性檢查。如果靜態物件形狀的正確性假設錯誤,虛擬機器可能會崩潰。

與我們聯繫