Skip to content

string.c: ask a string what encoding it is, not whether a bit is set - #7157

Merged
matz merged 1 commit into
mruby:masterfrom
takumin:binary-flag-through-encoding-accessors
Aug 14, 2026
Merged

string.c: ask a string what encoding it is, not whether a bit is set#7157
matz merged 1 commit into
mruby:masterfrom
takumin:binary-flag-through-encoding-accessors

Conversation

@takumin

@takumin takumin commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

MRB_STR_BINARY is the whole of what a string says about how its bytes are read: a string carrying it is read by byte, a string without it is read the way the build reads strings by default. The macros around it are named after the bit rather than after the question, so every write names one value of the field instead of the field:

RSTR_SET_BINARY_FLAG(s);
RSTR_UNSET_BINARY_FLAG(s);
RSTR_COPY_BINARY_FLAG(dst, src);

This PR gives the field a name and a small vocabulary, and routes the 22 writes and copies through it. The bit stays where it is; MRB_STR_BINARY is still 16 and the layout of flags does not move.

What goes into include/mruby/string.h

#define MRB_STR_ENCODING_DEFAULT 0
#define MRB_STR_ENCODING_BINARY  1
#ifdef MRB_UTF8_STRING
# define MRB_STR_ENCODING_UTF8   MRB_STR_ENCODING_DEFAULT
#endif

#define RSTR_ENCODING(s) \
  (((s)->flags & MRB_STR_BINARY) ? MRB_STR_ENCODING_BINARY : MRB_STR_ENCODING_DEFAULT)
#define RSTR_ENCODING_SET(s, e) \
  ((s)->flags = ((s)->flags & ~MRB_STR_BINARY) | \
                (((e) == MRB_STR_ENCODING_BINARY) ? MRB_STR_BINARY : 0))
#define RSTR_BINARY_P(s) (RSTR_ENCODING(s) == MRB_STR_ENCODING_BINARY)
#define RSTR_ENC_COPY(dst, src) RSTR_ENCODING_SET(dst, RSTR_ENCODING(src))

RSTR_SET_BINARY_FLAG(), RSTR_UNSET_BINARY_FLAG() and RSTR_COPY_BINARY_FLAG() go.

Index 0 is the build's default

A fresh string is zero filled, so index 0 costs no store on the path every string is made on. That is where the literals the parser hands over end up, and the strings numbers and times spell themselves with. __ENCODING__ is what names index 0 for a given build: UTF-8 where MRB_UTF8_STRING is set, ASCII-8BIT where it is not.

Binary cannot share index 0 with the default even in a build that indexes by byte anyway. String#b and Integer#chr mark strings there too, and mruby-regexp reads the mark through re_binary_string_p() (mrbgems/mruby-regexp/src/regexp.c) to decide whether to walk the subject a byte at a time, so folding the two together would change what that build answers.

MRB_STR_ENCODING_UTF8 is defined only where the build carries UTF-8. A build without it has no UTF-8 string to name, so naming one should stop the compiler rather than quietly write the default. The only place that names it is String#force_encoding, in mruby-encoding, which sets the define itself.

RSTR_BINARY_P() stays

Whether a string is read by byte is a question about the encoding, not about the shape the answer is kept in, so the predicate keeps its name and its 18 readers are untouched. It reads the field now and answers 1 rather than 16. re_binary_string_p() returns it as mrb_bool, which is bool in the usual configurations and uint8_t in the fallback (include/mruby/value.h); 16 and 1 are both true in either, so the narrowing simply happens where the answer is made.

The 22 sites

macro count files
RSTR_SET_BINARY_FLAG 13 src/string.c (4), mruby-string-ext (5), mruby-sprintf, mruby-regexp, mruby-string-bitops, mruby-encoding
RSTR_UNSET_BINARY_FLAG 1 mruby-encoding (String#force_encoding)
RSTR_COPY_BINARY_FLAG 8 src/string.c (3), mruby-string-ext (4), mruby-sprintf

Each replacement is bit for bit the write it replaces. RSTR_ENCODING_SET(s, MRB_STR_ENCODING_BINARY) expands to flags = (flags & ~16) | 16, and with MRB_STR_ENCODING_UTF8 it expands to flags = (flags & ~16) | 0, since (0 == 1) is always false.

Generated code

Two full-core builds, with mruby-encoding and with the gem removed from the box, gcc -O3:

build result
full-core, MRB_UTF8_STRING every object file identical instruction for instruction, .text 1789338 unchanged
full-core without mruby-encoding every object file identical instruction for instruction, .text 1771123 unchanged

Comparison is objdump -d over every .o in the build against the same object built from master. The files differ as bytes only in debug info, since the line numbers in include/mruby/string.h moved.

Testing

rake -m test on both builds above, plus the C++ ABI build, all green:

  • full-core with MRB_UTF8_STRING: 2291 tests, 2281 OK, 10 skip, 0 KO, 0 crash, 0 warnings.
  • full-core without mruby-encoding, where strings index by byte: 2229 tests, 2200 OK, 29 skip, 0 KO, 0 crash, 0 warnings.
  • the same box with enable_cxx_abi: 2291 tests, 2281 OK, 10 skip.

No test accompanies the change. No string answers anything different, so there is nothing new to pin.

Rebased on 8bd5bd0

re_byte_substr() in mruby-regexp used to copy the flag by hand and now hands the work to mrb_str_byte_subseq(), so the copy this PR would have converted there is gone. That is the one site the count above lost.

Not in this PR

The bit layout of flags does not change, the 18 RSTR_BINARY_P() readers do not change, and the comments that still say "flag" about this bit stay as they are while it still is one.

Summary by CodeRabbit

  • Bug Fixes
    • Improved string encoding consistency across string creation, copying, slicing, formatting, substitutions, and bitwise operations.
    • Improved handling of binary and UTF-8 string encodings.
    • Preserved existing string behavior while ensuring encoding metadata propagates correctly through derived strings.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4674eb01-3650-4aea-af54-e3e120463739

📥 Commits

Reviewing files that changed from the base of the PR and between bd82ad0 and 4848467.

📒 Files selected for processing (1)
  • mrbgems/mruby-regexp/src/regexp.c

📝 Walkthrough

Walkthrough

The change replaces legacy binary-string flag macros with explicit encoding identifiers and centralized encoding APIs. Core string operations and related gems now assign or copy encoding metadata through the new interfaces.

Changes

String encoding migration

Layer / File(s) Summary
Encoding contract and force-encoding paths
include/mruby/string.h, mrbgems/mruby-encoding/src/encoding.c
The string header defines encoding identifiers and centralized access, assignment, binary detection, and copy macros. str_force_encoding uses explicit encoding assignment.
Core string encoding propagation
src/string.c
String creation and mutation paths copy encoding metadata or assign binary encoding through the new APIs.
Gem encoding propagation
mrbgems/mruby-regexp/src/regexp.c, mrbgems/mruby-sprintf/src/sprintf.c, mrbgems/mruby-string-bitops/src/string_bitops.c, mrbgems/mruby-string-ext/src/string.c
Regexp, formatting, bitwise operation, and string extension paths use explicit encoding assignment and metadata copying.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 48484

This localized change preserves existing string flag behavior and layout, with the reported builds and tests passing; no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change from binary-flag checks to encoding-based string accessors.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

`MRB_STR_BINARY` is the whole of the answer to how a string's bytes are
read: a string carrying it is read by byte, a string without it is read
the way the build reads strings by default. The macros around it are
named after the bit rather than after the question, so every write names
one value of the field instead of the field itself:

    RSTR_SET_BINARY_FLAG(s);
    RSTR_UNSET_BINARY_FLAG(s);
    RSTR_COPY_BINARY_FLAG(dst, src);

`include/mruby/string.h` now names the field and the values it takes:

    #define MRB_STR_ENCODING_DEFAULT 0
    #define MRB_STR_ENCODING_BINARY  1
    #ifdef MRB_UTF8_STRING
    # define MRB_STR_ENCODING_UTF8   MRB_STR_ENCODING_DEFAULT
    #endif

    #define RSTR_ENCODING(s)
    #define RSTR_ENCODING_SET(s, e)
    #define RSTR_ENC_COPY(dst, src)

and the 22 writes and copies go through them. `RSTR_BINARY_P()` stays,
since whether a string is read by byte is a question about the encoding
and not about the shape it is kept in. It reads the field now and
answers 1 rather than 16, which is the narrowing `re_binary_string_p()`
was doing on the way out.

Index 0 is the build's default because a fresh string is zero filled:
the strings the parser hands over, and the ones numbers and times spell
themselves with, then say what they are without a store on the path
every string is made on. `__ENCODING__` names index 0 for a given build,
so it is UTF-8 where `MRB_UTF8_STRING` is set and ASCII-8BIT where it is
not.

Binary cannot share index 0 with the default in a build that indexes by
byte anyway. `String#b` and `Integer#chr` mark strings there too, and
`mruby-regexp` reads the mark through `re_binary_string_p()` to decide
whether to walk the subject a byte at a time, so folding the two
together would change what that build answers.

`MRB_STR_ENCODING_UTF8` is defined only where the build carries UTF-8. A
build without it has no UTF-8 string to name, so naming one should stop
the compiler rather than quietly write the default. The only place that
names it is `String#force_encoding`, in the gem that sets the define.

Every write is bit for bit the write it replaces, so no answer changes
and no test comes with this. Building full-core with `mruby-encoding`
and without it, every object file is identical instruction for
instruction to the one master builds, and `.text` over the whole build
is unchanged in both.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants