Open-Source Wikis

/

Ruby

/

Core classes

/

String and Symbol

ruby/ruby

String and Symbol

String is the most heavily-used heap type in Ruby. Symbol is its frozen, interned cousin. Both are encoding-aware (see systems/encoding.md).

Files

File Purpose
string.c The String C implementation. ~359 KB.
string.rb Some String methods in Ruby.
symbol.c Symbol and the global symbol table.
symbol.rb Some Symbol methods in Ruby.
symbol.h Symbol IDs and helpers.
include/ruby/internal/core/rstring.h RString struct layout.

String memory layout

struct RString {
    struct RBasic basic;
    union {
        struct {
            long len;                    /* byte length */
            char *ptr;
            union {
                long capa;
                VALUE shared;             /* if shared with another String */
            } aux;
        } heap;
        struct {
            char ary[RSTRING_EMBED_LEN_MAX + 1];   /* embedded short string */
        } embed;
    } as;
};

A String is one of three flavors:

  1. Embedded: short strings (up to ~24 bytes on 64-bit) live inline in the RString. No separate heap allocation.
  2. Heap-allocated: longer strings have aux.capa and a separate ptr malloc.
  3. Shared: a substring or String#dup that copy-on-write shares the underlying buffer with another String. aux.shared points to the parent.

The flags in basic.flags encode the flavor and the encoding index. RSTRING_LEN(s) and RSTRING_PTR(s) return the length in bytes and the data pointer regardless of flavor.

Mutability and freezing

Strings are mutable by default. Each mutation may:

  • Trigger a copy-on-write if the string is shared.
  • Grow the buffer (STR_BUF_MIN_SIZE is 127 bytes; growth is rounded up to the next size class).
  • Update the byte length and possibly invalidate cached character length / coderange.

Frozen strings raise FrozenError on mutation. With # frozen_string_literal: true (or default in modern Ruby), all string literals are frozen at parse time.

The frozen-string optimisation: the parser deduplicates frozen literals — "foo" (frozen) appearing twice in a file uses the same RString. See string.c::rb_fstring.

Encoding handling

Every String has an Encoding (see systems/encoding.md). The encoding is stored as an integer index in the flags. Methods that interpret characters (length, each_char, chars, [] with a character index, =~) consult the encoding's character-stepping callback.

The "coderange" caches whether the string is ASCII-only, valid in its encoding, broken, or unknown. It's stored in the flags and updated lazily.

Selected method implementations

Method Where
String#length / #size string.c::rb_str_strlen (encoding-aware)
String#bytesize string.c::rb_str_bytesize (returns RSTRING_LEN)
String#+ string.c::rb_str_plus (with encoding-compatibility check)
String#<< (concat) string.c::rb_str_concat
String#sub / #gsub re.c (delegates to the regex engine)
String#split string.c::rb_str_split_m
String#[] string.c::rb_str_aref (handles Integer, Range, Regexp, String args)
String#each_char string.c::rb_str_each_char
String#encode transcode.c
String#hash string.c::rb_str_hash (uses SipHash)
String#unpack pack.c
String#scan re.c::rb_str_scan

Performance optimisations

  • String comparison fast path: == first compares lengths, then compares pointers (for shared strings), then memcmp.
  • Frozen literal interning: at parse time, frozen literals share storage. "foo".equal?("foo") is true in a # frozen_string_literal: true file.
  • Inline cache for String#+@/String#-@: the -@ operator returns a frozen interned copy.
  • Substring sharing: s[5, 10] returns a String that shares the parent's buffer until either is mutated.
  • Hash caching: a string's hash is cached after the first computation if the string is frozen.

The String/Symbol divide

A Symbol is essentially "an interned, frozen String identifier". Symbols are deduplicated in a global table (symbol.c::global_symbols):

:foo.equal?(:foo)    # true: same VALUE
"foo".equal?("foo")  # depends on frozen-string-literal

Internally, every Symbol has an integer ID (ID in C). IDs are reused across the symbol table; the mapping is id ↔ Symbol object ↔ String text.

There are two kinds:

  1. Static symbols: short ASCII-only symbols are encoded directly in the VALUE bits — no allocation. :foo, :bar, :method_name.
  2. Dynamic symbols: longer or non-ASCII symbols are heap objects that carry a String. These are GC-able since Ruby 2.2 (the "Symbol GC" feature).

symbol.c handles both flavors and the transition between them.

ID — the underlying integer

An ID is a unique integer for each known identifier. Methods, variables, and constants are looked up by ID, not by string. Conversions:

  • rb_intern("foo")ID
  • rb_id2sym(id)VALUE (a Symbol)
  • rb_sym2id(sym)ID

rb_intern_const("foo") is a compile-time-friendly variant for static interning.

symbol.h and symbol.c define IDs in classes (e.g., method names) and IDs in the global table (e.g., user-supplied symbols).

Mutability landmines

  • String#freeze returns the same String: it's an in-place change.
  • String#dup always returns a mutable copy even if the source is frozen.
  • String#clone preserves frozen-ness (matches Object#clone semantics).
  • String#+@ returns self if mutable, a mutable dup if frozen.
  • String#-@ returns the frozen-interned canonical form.

Common pitfalls

  • Frozen-string-literal pragma: opt-in via # frozen_string_literal: true. Without it, all "..." literals are mutable. This pragma is becoming the default in newer Rubies.
  • Encoding mismatch: "a" + japanese_string works only if both are ASCII-compatible and at least one is ASCII-only.
  • Symbol → String → Symbol round trip: cheap but allocates a String. Symbol#to_s returns a frozen string.
  • String#hash/Symbol#hash are different even for equal text content. Use the type that matches your hash key.

Entry points for modification

  • New String method: add to string.c, register in Init_String. Add a Ruby-level wrapper in string.rb if it can be expressed in Ruby.
  • Encoding-aware operation: use rb_enc_strlen, rb_enc_codepoint_len, rb_enc_codelen. Avoid RSTRING_LEN for character counts unless ASCII-only.
  • Performance: profile under YJIT (--yjit-stats for ICs). Hot paths usually benefit from RB_FL_TEST checks before calling encoding-aware variants.
  • Symbol behaviour: symbol.c::global_symbols is the central registry. Walk id_str_table for diagnostics.

See systems/encoding.md and systems/regexp.md for what String operations interact with.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

String and Symbol – Ruby wiki | Factory