You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This type is used in the StreamerInfo for the TString class, which suggests its implementation.
Here I'm printing out the TString streamer from uproot-sample-5.25.02-uncompressed.root:
n is a full signed 4-byte length, not a length byte.
No NUL terminator and no 255 escape — this is not the counted string of Conventions §5.1.
n <= 0 means nothing follows. A null char* and an empty string are both 00 00 00 00 and cannot be told apart.
Bytes are uninterpreted; the spec says a reader SHOULD NOT assume UTF-8, so bytes (as in the issue body) is the right Python type, not str.
But the sketch in the issue body would be wrong for TString.TString has a hand-written Streamer that writes a counted string (one length byte, 255 escape, 4-byte length) — Conventions §5.1, root/core/base/src/TString.cxx:1418. Its recorded streamer info is fiction: StreamerDriven.md §7 and StreamerInfo.md §3.3 call out exactly this element (fData / char*, and on newer files TString's info lists no members at all). So fData: Annotated[bytes, Fmt("charstar")] would read a 4-byte length where the file has one byte. rootfilespec already gets this right — bootstrap/strings.py:23-37 implements the counted string and TString is in BOOTSTRAP_TYPES, so dynamic.py:27-30 skips the file's info for it. No change needed there; kCharStar is only needed for genuine user char* members.
Where it bites today.ElementType.as_fmt maps 7 → "charstar" (bootstrap/TStreamerInfo.py:272) and structutil.py:17-19 then raises Unimplemented format charstar at read time. Scanning scikit-hep-testdata, the only non-TStringkCharStar members are:
Event::fEventName in uproot-Event.root (currently masked — that file xfails earlier on double32, Implement double32 type #41)
mydata::name in uproot-issue31.root (latent; that file passes because the member is never instantiated)
Implementation is a few lines in structutil.py (or a dedicated SerDe): unpack >i, consume that many bytes, None/b"" on n <= 0. No corpus file uses the kOffsetL/kOffsetP forms (27/47), so those can stay unimplemented.
Tie-in with docs/design.md: the design promises ("Annotated builtin types vs. objects") that where a builtin Python type fully captures a ROOT type, the deserialized dataclass member should be that builtin — bytes for strings. kCharStar should therefore land directly as a plain bytes member with its own reader (n:i32 + n raw bytes, per the previous comment), not as a wrapper class and not via the TString.fData sketch in the issue body.
The general version of that promise — TString, std::string, RNTuple strings and char* all as plain bytes members, which is a large but mechanical refactor since TString/STLString/RString are wrapper dataclasses today — is now #68, with this issue as its smallest first instance. #68 also notes the interaction with #67: if streamer-generated classes stop using annotations as their serialization definition, their string members need no Annotated[...] marker at all.
Activity
nsmith- commentedon Sep 18, 2026
From the root-io-spec reference now vendored at
reference/root-io-spec(pin9c3f776): ElementTypes.md §2.2 and 00-conventions.md §5 §5.4.The encoding (
TBufferFile.cxx:285-317):nis a full signed 4-byte length, not a length byte.n <= 0means nothing follows. A nullchar*and an empty string are both00 00 00 00and cannot be told apart.bytes(as in the issue body) is the right Python type, notstr.But the sketch in the issue body would be wrong for
TString.TStringhas a hand-writtenStreamerthat writes a counted string (one length byte,255escape, 4-byte length) — Conventions §5.1,root/core/base/src/TString.cxx:1418. Its recorded streamer info is fiction: StreamerDriven.md §7 and StreamerInfo.md §3.3 call out exactly this element (fData/char*, and on newer filesTString's info lists no members at all). SofData: Annotated[bytes, Fmt("charstar")]would read a 4-byte length where the file has one byte. rootfilespec already gets this right —bootstrap/strings.py:23-37implements the counted string andTStringis inBOOTSTRAP_TYPES, sodynamic.py:27-30skips the file's info for it. No change needed there;kCharStaris only needed for genuine userchar*members.Where it bites today.
ElementType.as_fmtmaps 7 →"charstar"(bootstrap/TStreamerInfo.py:272) andstructutil.py:17-19then raisesUnimplemented format charstarat read time. Scanning scikit-hep-testdata, the only non-TStringkCharStarmembers are:Event::fEventNameinuproot-Event.root(currently masked — that file xfails earlier ondouble32, Implement double32 type #41)mydata::nameinuproot-issue31.root(latent; that file passes because the member is never instantiated)Implementation is a few lines in
structutil.py(or a dedicated SerDe): unpack>i, consume that many bytes,None/b""onn <= 0. No corpus file uses thekOffsetL/kOffsetPforms (27/47), so those can stay unimplemented.nsmith- commentedon Sep 18, 2026
Tie-in with
docs/design.md: the design promises ("Annotated builtin types vs. objects") that where a builtin Python type fully captures a ROOT type, the deserialized dataclass member should be that builtin —bytesfor strings.kCharStarshould therefore land directly as a plainbytesmember with its own reader (n:i32+nraw bytes, per the previous comment), not as a wrapper class and not via theTString.fDatasketch in the issue body.The general version of that promise —
TString,std::string, RNTuple strings andchar*all as plainbytesmembers, which is a large but mechanical refactor sinceTString/STLString/RStringare wrapper dataclasses today — is now #68, with this issue as its smallest first instance. #68 also notes the interaction with #67: if streamer-generated classes stop using annotations as their serialization definition, their string members need noAnnotated[...]marker at all.Read RNTuple strings as plain bytes
Read RNTuple strings as plain bytes
Read every ROOT string type as plain bytes with ROOTString (rest of n…
Read RNTuple strings as plain bytes
Read every ROOT string type as plain bytes with ROOTString (rest of n…
Read every ROOT string type as plain bytes with ROOTString (#68, #20) (…
sathabbott commentedon Oct 1, 2026
Fixed by #127: a
kCharStarmember reads asAnnotated[bytes, ROOTString("charstar")]. Its "Fixes #68 and #20" only closed #68.