Truffle 字符串指南

Truffle Strings 是 Truffle 的原始字符串类型,可以在不同语言之间共享。建议语言实现者将 Truffle Strings 用作其语言的字符串类型,以实现更简单的互操作性和更好的性能。

TruffleString 支持多种字符串编码,但对最常用的以下编码进行了特别优化:

  • UTF-8
  • UTF-16
  • UTF-32
  • US-ASCII
  • ISO-8859-1
  • BYTES

TruffleString API #

TruffleString 暴露的所有操作都作为内部 Node 提供,并作为静态或实例方法提供。用户应尽可能使用提供的节点,因为静态/实例方法只是执行其各自节点未缓存版本的简写。所有节点都命名为 {NameOfOperation}Node,所有便捷方法都命名为 {nameOfOperation}Uncached

某些操作支持延迟评估,例如延迟连接或某些字符串属性的延迟评估。大多数这些操作都提供一个参数 boolean lazy,允许用户在每次调用时启用或禁用延迟评估。

处理索引值的操作(例如 CodePointAtIndex)有两种变体:基于码点的索引和基于字节的索引。基于字节的索引通过操作名称中的 ByteIndex 后缀或前缀表示,否则索引基于码点。例如,CodePointAtIndex 的索引参数是基于码点的,而 CodePointAtByteIndex 使用基于字节的索引。

当前可用操作的列表如下所示,并按类别分组。

创建一个新的 TruffleString

查询字符串属性

  • isEmpty:检查字符串是否为空。
  • CodePointLength:获取字符串的码点长度。
  • byteLength:获取字符串的字节长度。
  • IsValid:检查字符串是否正确编码。
  • GetCodeRange:获取关于字符串内容的粗略信息(此字符串中的所有码点是否都在 ASCII/LATIN-1/BMP 范围内?)。
  • GetByteCodeRange:获取关于字符串内容的粗略信息,不考虑 16/32 位编码。
  • CodeRangeEquals:检查字符串的代码范围是否等于给定代码范围。
  • isCompatibleTo:检查字符串是否与给定编码兼容/可以在该编码中查看。
  • isManaged:检查字符串是否未由原生指针支持。
  • isNative:检查字符串是否由原生指针支持。
  • isImmutable:检查字符串是否为 TruffleString 的实例。
  • isMutable:检查字符串是否为 MutableTruffleString 的实例。

比较

  • Equal:检查两个字符串是否相等。请注意,此操作对编码敏感!
  • RegionEqual:检查两个字符串在由码点偏移量和长度定义的给定区域内是否相等。
  • RegionEqualByteIndex:检查两个字符串在由字节偏移量和长度定义的给定区域内是否相等。
  • CompareBytes:逐字节比较两个字符串。
  • CompareCharsUTF16:逐字符比较两个 UTF-16 字符串。
  • CompareIntsUTF32:逐 int 比较两个 UTF-32 字符串。
  • HashCode:获取字符串的哈希码。哈希码基于字符串的字节,因此具有相同码点但不同编码的字符串可能具有不同的哈希码。

转换

访问码点和字节

搜索

  • ByteIndexOfAnyByte:在字符串中查找给定字节集中的任意字节的第一次出现,并返回其基于字节的索引。
  • CharIndexOfAnyCharUTF16:在 UTF-16 字符串中查找给定字符集中的任意字符的第一次出现,并返回其基于字符的索引。
  • IntIndexOfAnyIntUTF32:在 UTF-32 字符串中查找给定 int 集中的任意 int 的第一次出现,并返回其基于 int 的索引。
  • IndexOfCodePoint:在字符串中查找给定码点的第一次出现,并返回其基于码点的索引。
  • ByteIndexOfCodePoint:在字符串中查找给定码点的第一次出现,并返回其基于字节的索引。
  • ByteIndexOfCodePointSet:在字符串中查找包含在给定集中的码点的第一次出现,并返回其基于字节的索引。
  • LastIndexOfCodePoint:在字符串中查找给定码点的最后一次出现,并返回其基于码点的索引。
  • LastByteIndexOfCodePoint:在字符串中查找给定码点的最后一次出现,并返回其基于字节的索引。
  • IndexOfString:在字符串中查找给定子字符串的第一次出现,并返回其基于码点的索引。
  • ByteIndexOfString:在字符串中查找给定子字符串的第一次出现,并返回其基于字节的索引。
  • LastIndexOfString:在字符串中查找给定子字符串的最后一次出现,并返回其基于码点的索引。
  • LastByteIndexOfString:在字符串中查找给定子字符串的最后一次出现,并返回其基于字节的索引。

组合

  • Concat:连接两个字符串。
  • Substring:从给定字符串创建子字符串,由基于码点的偏移量和长度界定。
  • SubstringByteIndex:从给定字符串创建子字符串,由基于字节的偏移量和长度界定。
  • Repeat:将给定字符串重复 n 次。

实例化 #

TruffleString 可以从码点、数字、原始数组或 java.lang.String 创建。

任何编码的字符串都可以使用 TruffleString.FromByteArrayNode 创建,它需要一个包含已编码字符串的字节数组。通过将 copy 参数设置为 false,此操作可以是非复制的。

重要提示:TruffleStrings 将假定数组内容是不可变的,请勿在将其传递给此操作的非复制变体后修改数组。

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;

abstract static class SomeNode extends Node {

    @Specialization
    static TruffleString someSpecialization(
            @Cached TruffleString.FromByteArrayNode fromByteArrayNode) {
        byte[] array = {'a', 'b', 'c'};
        return fromByteArrayNode.execute(array, 0, array.length, TruffleString.Encoding.UTF_8, false);
    }
}

为了更容易地创建独立于系统字节序的 UTF-16 和 UTF-32 字符串,TruffleString 提供了 TruffleString.FromCharArrayUTF16NodeTruffleString.FromIntArrayUTF32Node

TruffleString 也可以通过 TruffleStringBuilder 创建,后者是 TruffleString 相当于 java.lang.StringBuilder 的类。

TruffleStringBuilder 提供以下操作:

  • AppendByte:向字符串构建器追加单个字节。
  • AppendCharUTF16:向 UTF-16 字符串构建器追加单个字符。
  • AppendCodePoint:向字符串构建器追加单个码点。
  • AppendIntNumber:向字符串构建器追加一个整数。
  • AppendLongNumber:向字符串构建器追加一个长整数。
  • AppendString:向字符串构建器追加一个 TruffleString。
  • AppendSubstringByteIndex:向字符串构建器追加一个子字符串,由基于字节的偏移量和长度定义。
  • AppendJavaStringUTF16:向字符串构建器追加一个 Java 字符串子字符串,由基于字符的偏移量和长度定义。
  • ToString:从字符串构建器创建新的 TruffleString。

参见下面的示例

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringBuilder;

abstract static class SomeNode extends Node {

    @Specialization
    static TruffleString someSpecialization(
            @Cached TruffleStringBuilder.AppendCharUTF16Node appendCharNode,
            @Cached TruffleStringBuilder.AppendJavaStringUTF16Node appendJavaStringNode,
            @Cached TruffleStringBuilder.AppendIntNumberNode appendIntNumberNode,
            @Cached TruffleStringBuilder.AppendStringNode appendStringNode,
            @Cached TruffleString.FromCharArrayUTF16Node fromCharArrayUTF16Node,
            @Cached TruffleStringBuilder.AppendCodePointNode appendCodePointNode,
            @Cached TruffleStringBuilder.ToStringNode toStringNode) {
        TruffleStringBuilder sb = TruffleStringBuilder.create(TruffleString.Encoding.UTF_16);
        sb = appendCharNode.execute(sb, 'a');
        sb = appendJavaStringNode.execute(sb, "abc", /* fromIndex: */ 1, /* length: */ 2);
        sb = appendIntNumberNode.execute(sb, 123);
        TruffleString string = fromCharArrayUTF16Node.execute(new char[]{'x', 'y'}, /* fromIndex: */ 0, /* length: */ 2);
        sb = appendStringNode.execute(sb, string);
        sb = appendCodePointNode.execute(sb, 'z');
        return toStringNode.execute(sb); // string content: "abc123xyz"
    }
}

编码 #

每个 TruffleString 都以特定的内部编码进行编码,该编码在实例化时设置。

TruffleString 已对以下编码进行了全面优化:

  • UTF-8
  • UTF-16
  • UTF-32
  • US-ASCII
  • ISO-8859-1
  • BYTES

支持许多其他编码,但未完全优化。要使用它们,必须在 Truffle 语言注册中将 needsAllEncodings = true 设置为启用。

TruffleString 的内部编码不公开。语言不应查询字符串的编码,而应将 expectedEncoding 参数传递给所有字符串编码重要的(几乎所有操作)方法。这允许在编码转换时重复使用字符串对象,如果字符串在两种编码中字节等效。字符串可以使用 SwitchEncodingNode 转换为不同的编码,如下例所示:

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringBuilder;

abstract static class SomeNode extends Node {

    @Specialization
    static void someSpecialization(
            @Cached TruffleString.FromJavaStringNode fromJavaStringNode,
            @Cached TruffleString.ReadByteNode readByteNode,
            @Cached TruffleString.SwitchEncodingNode switchEncodingNode,
            @Cached TruffleString.ReadByteNode utf8ReadByteNode) {

        // instantiate a new UTF-16 string
        TruffleString utf16String = fromJavaStringNode.execute("foo", TruffleString.Encoding.UTF_16);

        // read a byte with expectedEncoding = UTF-16.
        // if the string is not byte-compatible with UTF-16, this method will throw an IllegalArgumentException
        System.out.printf("%x%n", readByteNode.execute(utf16String, /* byteIndex */ 0, TruffleString.Encoding.UTF_16));

        // convert to UTF-8.
        // note that utf8String may be reference-equal to utf16String!
        TruffleString utf8String = switchEncodingNode.execute(utf16String, TruffleString.Encoding.UTF_8);

        // read a byte with expectedEncoding = UTF-8
        // if the string is not byte-compatible with UTF-8, this method will throw an IllegalArgumentException
        System.out.printf("%x%n", utf8ReadByteNode.execute(utf8String, /* byteIndex */ 0, TruffleString.Encoding.UTF_8));
    }
}

编码之间的字节等效性是 UTF-16 和 UTF-32 上进行字符串压缩后确定的,例如,压缩的 UTF-16 字符串与 ISO-8859-1 字节等效,如果其所有字符都在 ASCII 范围内(参见 CodeRange),它也与 UTF-8 字节等效。

要检查您的代码是否正确切换编码,请使用系统属性 truffle.strings.debug-strict-encoding-checks=true 运行您的单元测试。这将禁用在切换编码时重复使用字符串对象,并使编码检查更加严格:所有对单个字符串操作将强制精确匹配,而对两个字符串操作仍将允许字节等效的重新解释。

所有具有多个字符串参数的 TruffleString 操作都要求字符串采用与结果编码兼容的编码。因此,字符串要么需要采用相同的编码,要么调用者必须确保两个字符串都与结果编码兼容。这使得已经知道 SwitchEncodingNodes 将是空操作的调用者可以为了占用空间的原因而跳过它们。

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringBuilder;

abstract static class SomeNode extends Node {

    @Specialization
    static boolean someSpecialization(
            TruffleString a,
            TruffleString b,
            @Cached TruffleString.SwitchEncodingNode switchEncodingNodeA,
            @Cached TruffleString.SwitchEncodingNode switchEncodingNodeB,
            @Cached TruffleString.EqualNode equalNode) {
        TruffleString utf8A = switchEncodingNodeA.execute(a, TruffleString.Encoding.UTF_8);
        TruffleString utf8B = switchEncodingNodeB.execute(b, TruffleString.Encoding.UTF_8);
        return equalNode.execute(utf8A, utf8B, TruffleString.Encoding.UTF_8);
    }
}

字符串属性 #

TruffleString 暴露以下属性:

  • byteLength:字符串的字节长度,通过 byteLength 方法公开。
  • codePointLength:字符串的码点长度,通过 CodePointLengthNode 公开。
  • isValid:可以通过 IsValidNode 查询,以检查字符串是否正确编码。
  • codeRange:提供有关字符串内容的粗略信息,通过 GetCodeRangeNode 公开。此属性可以具有以下值:
    • ASCII:此字符串中的所有码点都属于基本拉丁 Unicode 块,也称为 ASCII(0x00 - 0x7f)。
    • LATIN-1:此字符串中的所有码点都属于 ISO-8859-1 字符集(0x00 - 0xff),这等同于基本拉丁和拉丁-1 补充 Unicode 块的并集。字符串中至少有一个码点大于 0x7f。仅适用于 ISO-8859-1、UTF-16 和 UTF-32。
    • BMP:此字符串中的所有码点都属于 Unicode 基本多语言平面 (BMP)(0x0000 - 0xffff)。字符串中至少有一个码点大于 0xff。仅适用于 UTF-16 和 UTF-32。
    • VALID:此字符串编码正确,并且包含至少一个超出其他适用代码范围的码点(例如,对于 UTF-8,这意味着有一个码点超出 ASCII 范围,对于 UTF-16,这意味着有一个码点超出 BMP 范围)。
    • BROKEN:此字符串编码不正确。无法确定其内容的进一步信息。
  • hashCode:字符串的哈希码,通过 HashCodeNode 公开。哈希码取决于字符串的编码;在比较哈希码之前,字符串必须始终转换为通用编码!

请参见下面的示例,了解如何查询 TruffleString 暴露的所有属性。

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;

abstract static class SomeNode extends Node {

    @Specialization
    static TruffleString someSpecialization(
            TruffleString string,
            @Cached TruffleString.CodePointLengthNode codePointLengthNode,
            @Cached TruffleString.IsValidNode isValidNode,
            @Cached TruffleString.GetCodeRangeNode getCodeRangeNode,
            @Cached TruffleString.HashCodeNode hashCodeNode) {
        System.out.println("byte length: " + string.byteLength(TruffleString.Encoding.UTF_8));
        System.out.println("codepoint length: " + codePointLengthNode.execute(string, TruffleString.Encoding.UTF_8));
        System.out.println("is valid: " + isValidNode.execute(string));
        System.out.println("code range: " + getCodeRangeNode.execute(string));
        System.out.println("hash code: " + hashCodeNode.execute(string, TruffleString.Encoding.UTF_8));
    }
}

字符串相等性和比较 #

TruffleString 对象应使用 EqualNode 检查相等性。就像 HashCodeNode 一样,相等性比较对字符串的编码敏感,因此在任何比较之前,字符串应始终转换为通用编码。Object#equals(Object) 的行为类似于 EqualNode,但由于此方法没有 expectedEncoding 参数,它将自动确定字符串的通用编码。如果字符串的编码不相等,TruffleString 将检查一个字符串是否与另一个字符串的编码二进制兼容,如果是,则匹配它们的内容。否则,字符串被视为不相等,不应用自动转换。

请注意,由于 TruffleStringhashCodeequals 方法对字符串编码敏感,因此在使用 TruffleString 对象作为 HashMap 中的键等操作之前,必须始终将其转换为通用编码。

TruffleString 还提供三个比较节点:CompareBytesNodeCompareCharsUTF16NodeCompareIntsUTF32Node,分别用于逐字节、逐字符和逐 int 比较字符串。

连接 #

连接通过 ConcatNode 完成。此操作要求两个字符串都在 expectedEncoding 中,这也是结果字符串的编码。通过 lazy 参数支持延迟连接。当两个字符串进行延迟连接时,新字符串内部数组的分配和初始化会延迟,直到另一个操作需要直接访问该数组。这种“延迟连接字符串”的物化可以使用 MaterializeNode 显式触发。这在循环中访问字符串之前很有用,例如在以下示例中:

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;

abstract static class SomeNode extends Node {

    @Specialization
    static TruffleString someSpecialization(
            TruffleString utf8StringA,
            TruffleString utf8StringB,
            @Cached TruffleString.ConcatNode concatNode,
            @Cached TruffleString.MaterializeNode materializeNode,
            @Cached TruffleString.ReadByteNode readByteNode) {
        // lazy concatenation
        TruffleString lazyConcatenated = concatNode.execute(utf8StringA, utf8StringB, TruffleString.Encoding.UTF_8, /* lazy */ true);

        // explicit materialization
        TruffleString materialized = materializeNode.execute(lazyConcatenated, TruffleString.Encoding.UTF_8);

        int byteLength = materialized.byteLength(TruffleString.Encoding.UTF_8);
        for (int i = 0; i < byteLength; i++) {
            // string is guaranteed to be materialized here, so no slow materialization code can end up in this loop
            System.out.printf("%x%n", readByteNode.execute(materialized, i, TruffleString.Encoding.UTF_8));
        }
    }
}

子字符串 #

子字符串可以通过 SubstringNodeSubstringByteIndexNode 创建,它们分别使用基于码点和基于字节的索引。子字符串也可以是 lazy 的,这意味着不会为结果字符串创建新数组,而是重用父字符串的数组,并仅使用传递给子字符串节点的偏移量和长度进行访问。目前,惰性子字符串的内部数组永远不会被修剪(即,不会被新的精确长度的数组替换)。请注意,这种行为有效地在每次创建惰性子字符串时都会造成内存泄漏。一个可能出现问题的极端示例是:给定一个大小为 100 兆字节的字符串,从此字符串创建的任何惰性子字符串都将使 100 兆字节的数组保持活动状态,即使原始字符串已被垃圾回收器释放。请谨慎使用惰性子字符串。

java.lang.String 的互操作性 #

TruffleString 提供 FromJavaStringNode 用于将 java.lang.String 转换为 TruffleString。要从 TruffleString 转换为 java.lang.String,请使用 ToJavaStringNode。如果需要,此节点将在内部将字符串转换为 UTF-16,并从该表示创建 java.lang.String

Object#toString() 是使用 ToJavaStringNode 的未缓存版本实现的,应避免在快速路径上使用。

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;

abstract static class SomeNode extends Node {

    @Specialization
    static void someSpecialization(
            @Cached TruffleString.FromJavaStringNode fromJavaStringNode,
            @Cached TruffleString.SwitchEncodingNode switchEncodingNode,
            @Cached TruffleString.ToJavaStringNode toJavaStringNode,
            @Cached TruffleString.ReadByteNode readByteNode) {
        TruffleString utf16String = fromJavaStringNode.execute("foo", TruffleString.Encoding.UTF_16);
        TruffleString utf8String = switchEncodingNode.execute(utf16String, TruffleString.Encoding.UTF_8);
        System.out.println(toJavaStringNode.execute(utf8String));
    }
}

TruffleString 还暴露 #toStringDebug() 用于调试目的。除了调试之外,请勿将此方法用于任何其他目的,因为其返回值未指定,并且可能随时更改。

java.lang.String 的差异 #

java.lang.String 切换到 TruffleString 时应考虑以下事项:

  • TruffleString 实例的静态开销大于 java.lang.String 对象的开销。一个 TruffleString 对象包含 2 个指针字段、4 个 int 字段和 4 个 byte 字段,通常导致总对象大小为 40 字节(对象头 12 字节,每个指针 4 字节,带压缩 oops,8 字节内存对齐)。一个 java.lang.String 对象包含一个指针字段、一个 int 字段和一个 byte 字段,在相同条件下导致总对象大小为 24 字节。这种内存占用差异可能会对生成大量小字符串的某些情况产生负面影响。
  • TruffleStringjava.lang.String 一样执行字符串压缩。
  • 如果您的语言需要将字符串转换为其他编码(例如 UTF-8,这在 Web 应用程序中非常常见),如果字符串不包含特殊字符,TruffleString 可以将此操作变为无操作。例如,纯 ASCII 字符串可以重新解释为几乎任何编码,并且将纯 ASCII UTF-16 字符串转换为 UTF-8 是无操作的。在无法避免转码字符串的情况下,TruffleString 会在原始字符串中缓存转码后的字符串,因此每个字符串和编码只进行一次转码。
  • 为了使用第三方库,TruffleString 对象必须转换为 java.lang.String 并返回。为了使此操作尽可能廉价,TruffleString 在从 java.lang.String 转换为 TruffleString 时重用 Java String 的内部字节数组,并在从 TruffleString 对象创建 Java String 时将 Java Strings 缓存到对象本身中。
  • TruffleString 提供了 java.lang.String 中不存在的额外功能:
    • 延迟连接和字符串视图,可以显著减少您的语言可能需要执行的数组复制操作的数量。
    • 原生内存中的 String 视图,完全避免在使用原生内存之前将其复制到 Java 数组中的需要。
    • 通过 codeRange 属性进行的 String 内容分类,允许对纯 ASCII 等字符串进行特殊化。这可以显著降低某些字符串操作的复杂性。
  • 所有 TruffleString 操作的性能应与或优于其 java.lang.String 对应项。

码点迭代器 #

TruffleString 提供 TruffleStringIterator 作为迭代字符串码点的方式。此方法应优于在循环中使用 CodePointAtIndexNode,尤其是在可变宽度编码(如 UTF-8)上,因为 CodePointAtIndexNode 可能需要在每次调用时重新计算给定码点索引的等效字节索引。

参见示例

import com.oracle.truffle.api.dsl.Cached;
import com.oracle.truffle.api.strings.TruffleString;
import com.oracle.truffle.api.strings.TruffleStringIterator;

abstract static class SomeNode extends Node {

    @Specialization
    static void someSpecialization(
            TruffleString string,
            @Cached TruffleString.CreateCodePointIteratorNode createCodePointIteratorNode,
            @Cached TruffleStringIterator.NextNode nextNode,
            @Cached TruffleString.CodePointLengthNode codePointLengthNode,
            @Cached TruffleString.CodePointAtIndexNode codePointAtIndexNode) {

        // iterating over a string's codepoints using TruffleStringIterator
        TruffleStringIterator iterator = createCodePointIteratorNode.execute(string, TruffleString.Encoding.UTF_8);
        while (iterator.hasNext()) {
            System.out.printf("%x%n", nextNode.execute(iterator));
        }

        // suboptimal variant: using CodePointAtIndexNode in a loop
        int codePointLength = codePointLengthNode.execute(string, TruffleString.Encoding.UTF_8);
        for (int i = 0; i < codePointLength; i++) {
            // performance problem: codePointAtIndexNode may have to calculate the byte index corresponding
            // to codepoint index i for every loop iteration
            System.out.printf("%x%n", codePointAtIndexNode.execute(string, i, TruffleString.Encoding.UTF_8));
        }
    }
}

可变字符串 #

TruffleString 还提供了一个可变字符串变体,称为 MutableTruffleString,它也接受所有 TruffleString 节点。MutableTruffleString 不是线程安全的,并允许通过 WriteByteNode 覆盖其内部字节数组或原生指针中的字节。内部数组或原生指针的内容也可以在外部修改,但必须通过 notifyExternalMutation() 通知相应的 MutableTruffleStringMutableTruffleString 不是 Truffle 互操作类型,在跨越语言边界之前必须通过 TruffleString.AsTruffleString 转换为不可变的 TruffleString

联系我们