mruby-sprintf: set the result's length through RSTR_SET_LEN() - #7174
Conversation
`mrb_str_format()` writes the result's length itself, choosing between the flags word and `as.heap.len`. It is the last place outside `include/mruby/string.h` that writes a string's flags by hand, and its two arms do not agree on what they write: the embedded one writes `len`, the length of the argument being formatted, and the heap one writes `blen`, how much of the buffer has been written. `blen` is what belongs there. The line the two arms were split out of read RSTRING(result)->as.heap.len = blen; and the split, in 54132e4, took `len` into the new arm from the assignment two lines above it. Nothing reaches the embedded arm, so what it writes has never shown. `result` is allocated with a capacity of at least 120 bytes, and `mrb_str_new_capa()` embeds only what fits in `RSTRING_EMBED_LEN_MAX`, which is 27 bytes on 64-bit and 11 on 32-bit. From there `CHECK()` only grows it, through `mrb_str_resize()`, and `resize_capa()` has no way back from the heap to embedded. `RSTR_SET_LEN()` is the macro that makes that choice, and it is the one to make it here: the length goes where the header says it goes, and the header is left as the only place that knows where an embedded length sits. Both arms stay in the emitted code, since nothing tells the compiler the string is not embedded. What changes is the value the embedded arm would store: master: 19a5: 44 89 de mov %r11d,%esi # len this: 19a5: 8b 74 24 10 mov 0x10(%rsp),%esi # blen 19a9: 25 3f f8 0f 00 and $0xff83f,%eax 19b4: c1 e6 06 shl $0x6,%esi The byte the wider operand costs comes back out of alignment padding further down, so `sprintf.o` and `bin/mruby` are the size they were in all four `ci/gcc-clang` builds.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe Changessprintf result construction
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to This localized change standardizes how formatted string length is written without changing reachable output behavior, and the reported test suites pass. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
mrb_str_format()sets the result's length by reaching intostruct RStringitself, choosing between the flags word andas.heap.len. It is the last place outsideinclude/mruby/string.hthat writes a string's flags by hand, and its two arms do not write the same thing:lenis the length of the argument being formatted.blenis how much of the buffer has been written. They are different numbers, and only one of them is the result's length.Which value belongs there
blen. The line the two arms were split out of readand the split, in 54132e4 ("make embed string when create literals", 2014), took
leninto the new arm from the assignment two lines above it:Why it has never shown
The embedded arm cannot run.
resultis allocated atsprintf.c:416-421with a capacity of at least 120 bytes:mrb_str_new_capa()embeds only whatRSTR_EMBEDDABLE_P(capa)accepts, which is 27 bytes on 64-bit and 11 on 32-bit. From thereCHECK()only ever grows the string, throughmrb_str_resize(), andresize_capa()has no branch back from the heap to embedded. SoRSTRING(result)->flags & MRB_STR_EMBEDis false on every run.That is why this is a cleanup rather than a fix: no output changes, on any build. What it removes is a write that would be wrong the moment the branch became reachable, and a second hand-rolled copy of where an embedded length sits.
What the header is left holding
The one line left is over a width, not a position.
git grep -- '->flags'acrosssrc/string.cand the string gems comes back with nothing on astruct RString, so after this the flags word is reached through the header's accessors everywhere, and where a field sits is the header's business alone.Generated code
gcc -O3, against the same objects built from master, over the four buildsci/gcc-clangmakes. Both arms stay in the emitted code, since nothing tells the compiler the string is not embedded. What changes is the value the embedded arm would store.master:
this branch:
One operand, and it is the whole of the change.
%r11dand0x10(%rsp)arelenandblen, which the boundCHECK()computes a few blocks on names together:The byte the wider operand costs comes back out of alignment padding further down, where
nopl 0x0(%rax)becomesnopl (%rax), so nothing moves in size:.textofbin/mrubysprintf.osprintf.osprintf.osprintf.oTesting
rake -m test, all green with 0 KO, 0 crash, 0 warnings:build_config/default.rbfull-core,i686-linux-gnu-gccThe 32-bit build is worth having here because it is the one where
RSTRING_EMBED_LEN_MAXis 11 rather than 27, so it is the closest any build comes to the arm being reachable. It is not close: the capacity is still at least 120.build_config/host-m32.rbneeds a multilib toolchain, so that build isfull-corewith the compiler set toi686-linux-gnu-gccinstead.Relation to #7173
This is the follow-up noted in the review there. It does not depend on it: this branch is on master, and the two touch different files.
Summary by CodeRabbit
%sand%pconversions.