Languages: English | 简体中文 | 繁體中文 | 日本語 | 한국어 | Français | Deutsch | Español | Italiano | Русский | العربية
← NeverC Built-in Runtime System · NeverC Documentation
NeverC provides a built-in string value type for C. It combines the interface design of C++ std::string with Qt QString's Unicode capabilities, enabling safe and efficient string operations in C code.
Enabling: Pass -fbuiltin-string at compile time (off by default). It is enabled automatically when:
- The input file has a
.ncextension (see.ncextension docs) -fdyncodemode is active
neverc hello.nc -o hello # automatic — .nc enables it
neverc -fbuiltin-string main.c -o main # explicit flag for .c filesstring greeting = "Hello World";
string msg = greeting + ", NeverC!";
printf("Byte length: %zu\n", msg.len);
printf("Char count: %zu\n", msg.utf8_count());
printf("Content: %s\n", msg.c_str());- Value semantics:
stringis a{ data, len, cap }three-field struct, passed and returned by value - Automatic memory management: The compiler attaches
CleanupAttrto automatically release owned strings on scope exit — no manualfreeneeded - Native UTF-8: All literals (including
L"...",u"...",U"...") are folded to UTF-8 at compile time - Borrowed views: Literal assignment
string s = "hello"produces a zero-allocation view (cap == 0) that does not own memory - Dot-call syntax:
s.find("abc"),s.to_upper().trim()are rewritten by Sema into corresponding C function calls - Namespace isolation: All runtime symbols use the
neverc_string_*prefix, avoiding collisions with user identifiers likestring_eqorSTRING_NPOS
typedef struct __neverc_string {
const char *data; // pointer to UTF-8 bytes
__SIZE_TYPE__ len; // byte length
__SIZE_TYPE__ cap; // capacity (0 = borrowed view, >0 = owned memory)
} string;The cap field serves double duty as both "capacity" and "ownership flag":
| State | cap |
Meaning | Example |
|---|---|---|---|
| Borrowed (view) | == 0 |
Does not own memory; points to external storage (literals, stack buffers, etc.) | string s = "hello" |
| Owned | > 0 |
Owns a buffer allocated via NEVERC_STRING_ALLOC |
string s = a + b |
string a = "hello"; // Borrowed: data → literal storage, cap == 0
string b = a + ", world!"; // Owned: allocates new buffer, cap > 0
string c = a.to_upper(); // Owned: transformation produces new buffer
string d = neverc_string_view(buf, len); // Borrowed: wraps external bufferThe compiler automatically handles the lifetime of owned strings:
void example() {
string a = "hello"; // borrowed, no release needed
string b = a + ", world!"; // owned, compiler attaches CleanupAttr
string c = b.to_upper().trim(); // chaining: intermediate values auto-released
printf("%s\n", c.c_str());
} // scope exit: b, c auto-released; a needs no release (borrowed)Wide-character pointers returned by w_str(), to_utf16_owned(), and to_utf32_owned() are also auto-released — Sema attaches __neverc_wptr_cleanup when it detects these initializations.
The compiler recursively detects and auto-releases string members inside arrays, structs, and nested combinations:
// Arrays
string keys[] = {"admin".encrypt(), "root".encrypt(), "user".encrypt()};
// auto-released on scope exit — no manual free needed
// Structs
typedef struct { string name; string value; } kv_pair;
kv_pair p = {.name = "key".encrypt(), .value = "val".encrypt()};
// auto-released on scope exit
// Nested combinations
typedef struct { string label; string tags[2]; } tagged_item;
tagged_item item = {.label = "title", .tags = {"t1", "t2"}};
// all string members auto-released on scope exit
// 2D arrays
string grid[2][2] = {{"a", "b"}, {"c", "d"}};
// auto-released on scope exitThe cleanup traverses the type tree at compile time (CodeGen layer), generating per-element cleanup calls for every nested string — including string[N], struct { string; }, struct { string[N]; }, and arbitrary nesting depths. Only owned strings (cap != 0) are freed; views are skipped.
- Forged handle protection: Handles with
len > 0 && data == NULLare intercepted by__neverc_string_invalid, short-circuiting to the empty string - Oversized handle protection: Handles with
len > NEVERC_STRING_MAX_LENare similarly intercepted - Temporary safety:
.c_str()and.data()on temporaries (prvalues) trigger compiler errorerr_neverc_string_cstr_temporary, preventing dangling pointers - Zero-leak testing: All string tests on macOS run under
leaks --atExit, asserting "0 leaks"
| Constant | Value | Description |
|---|---|---|
NEVERC_STRING_NPOS |
(size_t)-1 |
Return value when search finds no match, equivalent to std::string::npos |
NEVERC_STRING_MAX_LEN |
(size_t)-2 |
Maximum payload length ceiling |
Sema rewrites operators to corresponding runtime calls:
| Operator | Equivalent Call | Return Type |
|---|---|---|
a + b |
__neverc_string_cat(a, b) |
string (owned) |
a += b |
__neverc_string_assign(&a, __neverc_string_cat(a, b)) |
— |
a == b |
neverc_string_eq(a, b) |
int |
a != b |
!neverc_string_eq(a, b) |
int |
a < b |
neverc_string_compare(a, b) < 0 |
int |
a <= b |
neverc_string_compare(a, b) <= 0 |
int |
a > b |
neverc_string_compare(a, b) > 0 |
int |
a >= b |
neverc_string_compare(a, b) >= 0 |
int |
a = b |
__neverc_string_assign(&a, b) |
— |
s[i] |
neverc_string_at(s, i) |
char |
| Dot-call | Aliases | Runtime Function | Signature | Returns |
|---|---|---|---|---|
s.len() |
s.size() s.length() |
neverc_string_len |
(string s) → size_t |
Byte length |
s.empty() |
— | neverc_string_empty |
(string s) → int |
s.len == 0 |
s.c_str() |
— | neverc_string_cstr |
(string s) → const char * |
NUL-terminated string pointer (borrowed) |
s.data() |
— | neverc_string_data |
(string s) → void * |
Raw byte pointer (borrowed, no const) |
s.at(i) |
s[i] |
neverc_string_at |
(string s, size_t i) → char |
Returns '\0' on out-of-bounds |
s.front() |
— | neverc_string_front |
(string s) → char |
First byte |
s.back() |
— | neverc_string_back |
(string s) → char |
Last byte |
Note:
.c_str()and.data()return borrowed pointers into the string's internal buffer. Using them on temporaries is a compile error.
| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.reserve(n) |
neverc_string_reserve |
(string s, size_t n) → string |
Reserve at least n bytes of capacity |
s.shrink_to_fit() |
neverc_string_shrink_to_fit |
(string s) → string |
Reduce excess capacity |
s.capacity() |
neverc_string_capacity |
(const string *s) → size_t |
Current capacity (passed via &s) |
s.max_size() |
neverc_string_max_size |
(string s) → size_t |
Returns NEVERC_STRING_MAX_LEN |
| Dot-call | Runtime Function | Signature | Returns |
|---|---|---|---|
s.eq(t) |
neverc_string_eq |
(string a, string b) → int |
1=equal, 0=not equal |
s.compare(t) |
neverc_string_compare |
(string a, string b) → int |
-1 / 0 / +1 |
s.compare(pos, n, t) |
neverc_string_compare_substr |
(string a, size_t pos, size_t n, string b) → int |
Substring compare |
s.compare(p1, n1, t, p2, n2) |
neverc_string_compare_substr2 |
(string a, size_t p1, size_t n1, string b, size_t p2, size_t n2) → int |
Two-substring compare |
Fold rule: A-Z → a-z, all other bytes (including >= 0x80 UTF-8 continuation bytes) unchanged. Suitable for HTTP header matching, file extension comparison, etc.
| Dot-call | Runtime Function | Description |
|---|---|---|
s.eq_ic(t) |
neverc_string_eq_ic |
Case-insensitive equality |
s.compare_ic(t) |
neverc_string_compare_ic |
Case-insensitive 3-way compare |
s.find_ic(t) |
neverc_string_find_ic |
Case-insensitive find |
s.contains_ic(t) |
neverc_string_contains_ic |
Case-insensitive contains |
s.starts_with_ic(t) |
neverc_string_starts_with_ic |
Case-insensitive prefix match |
s.ends_with_ic(t) |
neverc_string_ends_with_ic |
Case-insensitive suffix match |
string ct = "Content-Type";
if (ct.eq_ic("content-type")) { /* matches */ }
string path = "photo.PNG";
if (path.ends_with_ic(".png")) { /* matches */ }All search functions support both string and char overloads — Sema dispatches automatically based on the first argument type.
| Dot-call | char overload | Runtime Function | Returns |
|---|---|---|---|
s.find(t) |
s.find(ch) |
neverc_string_find / find_char |
Byte offset of first occurrence, or NEVERC_STRING_NPOS |
s.find(t, pos) |
s.find(ch, pos) |
neverc_string_find_from / find_char_from |
Search starting from pos |
s.rfind(t) |
s.rfind(ch) |
neverc_string_rfind / rfind_char |
Last occurrence offset |
s.rfind(t, pos) |
s.rfind(ch, pos) |
neverc_string_rfind_to / rfind_char_to |
Reverse search within [0, pos] |
s.contains(t) |
s.contains(ch) |
neverc_string_contains / contains_char |
1=contains, 0=does not |
s.starts_with(t) |
s.starts_with(ch) |
neverc_string_starts_with / starts_with_char |
Prefix match |
s.ends_with(t) |
s.ends_with(ch) |
neverc_string_ends_with / ends_with_char |
Suffix match |
s.count(t) |
s.count(ch) |
neverc_string_count / count_char |
Occurrence count |
Internally uses a 256-bit bitmap for O(1) character set membership testing, yielding O(n+m) overall complexity.
| Dot-call | With position | Runtime Function | Description |
|---|---|---|---|
s.find_first_of(chars) |
s.find_first_of(chars, pos) |
neverc_string_find_first_of / _from |
First occurrence of any character in chars |
s.find_last_of(chars) |
s.find_last_of(chars, pos) |
neverc_string_find_last_of / _to |
Last occurrence |
s.find_first_not_of(chars) |
s.find_first_not_of(chars, pos) |
neverc_string_find_first_not_of / _from |
First character not in chars |
s.find_last_not_of(chars) |
s.find_last_not_of(chars, pos) |
neverc_string_find_last_not_of / _to |
Last character not in chars |
Char overloads also work: s.find_first_of('x') is equivalent to s.find('x'), and s.find_first_not_of(' ') is the classic "skip leading spaces" idiom.
| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.substr(pos) |
neverc_string_substr |
(string s, size_t pos, size_t len) → string |
len defaults to NPOS (to end) with 1 arg |
s.substr(pos, len) |
same | same | Extract [pos, pos+len) substring |
s.copy(out, n) |
neverc_string_copy |
(string s, char *out, size_t n) → size_t |
Copy to external buffer, returns bytes copied |
s.copy(out, n, pos) |
neverc_string_copy_from |
(string s, char *out, size_t n, size_t pos) → size_t |
Copy starting from pos |
Mutation follows the value-type "consume input + return new value" pattern: the function consumes the input string (releases its owned buffer) and returns a new owned string. The compiler automatically rewrites s.append(x) as s = neverc_string_append(s, x), appearing as "in-place modification" from the user's perspective.
Only assign, swap, and capacity use string * pointer receivers (see "Pointer Receiver Methods" below).
| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.append(t) |
neverc_string_append |
(string s, string t) → string |
Append string |
s.append(n, ch) |
neverc_string_append_char |
(string s, size_t n, char ch) → string |
Append n copies of ch |
s.push_back(ch) |
neverc_string_push_back |
(string s, char ch) → string |
Append single character |
s.pop_back() |
neverc_string_pop_back |
(string s) → string |
Remove last character |
s.insert(pos, t) |
neverc_string_insert |
(string s, size_t pos, string t) → string |
Insert string |
s.insert(pos, n, ch) |
neverc_string_insert_char |
(string s, size_t pos, size_t n, char ch) → string |
Insert n copies of ch |
s.erase(pos) |
neverc_string_erase |
(string s, size_t pos, size_t count) → string |
count defaults to NPOS with 1 arg |
s.erase(pos, count) |
same | same | Erase [pos, pos+count) |
s.replace(pos, n, t) |
neverc_string_replace |
(string s, size_t pos, size_t n, string t) → string |
Replace substring |
s.replace(pos, n, m, ch) |
neverc_string_replace_char |
(string s, size_t pos, size_t n, size_t m, char ch) → string |
Replace with m copies of ch |
s.replace_all(from, to) |
neverc_string_replace_all |
(string s, string from, string to) → string |
Global replace (NeverC extension) |
s.clear() |
neverc_string_clear |
(string s) → string |
Clear contents (returns empty string) |
s.resize(n) |
neverc_string_resize |
(string s, size_t n, char ch) → string |
ch defaults to '\0' with 1 arg |
s.resize(n, ch) |
same | same | Resize, filling extension with ch |
s.pad_left(w, ch) |
neverc_string_pad_left |
(string s, size_t width, char ch) → string |
Left-pad |
s.pad_right(w, ch) |
neverc_string_pad_right |
(string s, size_t width, char ch) → string |
Right-pad |
s.clone() |
neverc_string_clone |
(string s) → string |
Deep copy (owned moves directly, borrowed allocates new buffer) |
s.to_lower() |
neverc_string_to_lower |
(string s) → string |
ASCII lowercase |
s.to_upper() |
neverc_string_to_upper |
(string s) → string |
ASCII uppercase |
s.trim() |
neverc_string_trim |
(string s) → string |
Trim whitespace from both ends |
s.ltrim() |
neverc_string_ltrim |
(string s) → string |
Trim left whitespace |
s.rtrim() |
neverc_string_rtrim |
(string s) → string |
Trim right whitespace |
s.repeat(n) |
neverc_string_repeat |
(string s, size_t n) → string |
Repeat n times |
s.reverse() |
neverc_string_reverse |
(string s) → string |
Byte-level reversal |
s.hash() |
neverc_string_hash |
(string s) → unsigned long long |
FNV-1a 64-bit hash |
These methods need to operate directly on the caller's storage; Sema wraps the receiver as &s:
| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.assign(t) |
neverc_string_assign |
(string *dst, string src) |
Replace contents (release old + install new) |
s.assign(n, ch) |
neverc_string_assign_char |
(string *dst, size_t n, char ch) |
Replace with n copies of ch |
s.swap(t) |
neverc_string_swap |
(string *a, string *b) |
Swap two string handles |
| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.split_first(sep) |
neverc_string_split_first |
(string s, string sep) → string |
First segment before separator |
s.split_rest(sep) |
neverc_string_split_rest |
(string s, string sep) → string |
Everything after first separator |
s.split_before_last(sep) |
neverc_string_split_before_last |
(string s, string sep) → string |
Before last separator |
s.split_after_last(sep) |
neverc_string_split_after_last |
(string s, string sep) → string |
After last separator |
s.split(sep, &items, &n) |
neverc_string_split |
(string s, string sep, string **items, size_t *count) |
Full split into array |
| — | neverc_string_split_free |
(string *parts, size_t n) |
Free array from split |
| — | neverc_string_join |
(const string *items, size_t n, string sep) → string |
Join array with separator |
// Peel one piece at a time
string path = "/usr/local/bin";
string first = path.split_first("/"); // ""
string rest = path.split_rest("/"); // "usr/local/bin"
// Path decomposition
string file = "src/lib/main.c";
string dir = file.split_before_last("/"); // "src/lib"
string base = file.split_after_last("/"); // "main.c"
// Full split
string csv = "a,b,c";
string *parts;
size_t count;
csv.split(",", &parts, &count);
for (size_t i = 0; i < count; i++)
printf("[%s]", parts[i].c_str());
neverc_string_split_free(parts, count);| Direction | Dot-call | Runtime Function | Signature |
|---|---|---|---|
| int → string | — | neverc_string_from_int |
(ptrdiff_t v) → string |
| uint → string | — | neverc_string_from_uint |
(size_t v) → string |
| int → string (with base) | — | neverc_string_from_int_base |
(ptrdiff_t v, int base) → string |
| uint → string (with base) | — | neverc_string_from_uint_base |
(size_t v, int base) → string |
| string → int | s.to_int() |
neverc_string_to_int |
(string s) → ptrdiff_t |
| string → uint | s.to_uint() |
neverc_string_to_uint |
(string s) → size_t |
to_int / to_uint parse decimal digits only, stopping at the first non-digit character. from_int_base / from_uint_base support bases 2-36.
string hex = neverc_string_from_int_base(255, 16); // "ff"
string dec = neverc_string_from_int(-42); // "-42"
string s = "12345";
ptrdiff_t v = s.to_int(); // 12345Not used via dot-call; require the full neverc_string_* prefix:
| Runtime Function | Signature | Description |
|---|---|---|
neverc_string_from_cstr(p) |
(const char *p) → string |
Create owned string from C string |
neverc_string_from_char(ch) |
(char ch) → string |
Create from single character |
neverc_string_from_utf32_char(cp) |
(uint32_t cp) → string |
Create UTF-8 from Unicode codepoint |
neverc_string_from_utf16(d, n) |
(const uint16_t *d, size_t n) → string |
Create from UTF-16 buffer |
neverc_string_from_utf32(d, n) |
(const uint32_t *d, size_t n) → string |
Create from UTF-32 buffer |
neverc_string_from_latin1(d, n) |
(const char *d, size_t n) → string |
Create from Latin-1 (ISO-8859-1) |
neverc_string_view(p, n) |
(const char *p, size_t n) → string |
Create borrowed view (no copy) |
neverc_string_clone(s) |
(string s) → string |
Deep copy |
neverc_string_free(s) |
(string s) |
Release owned string (no-op for borrowed) |
neverc_string_array_free(items, n) |
(string *items, size_t n) |
Release all elements in a string array |
neverc_string_format provides a libc-free printf subset:
string name = "World";
string msg = "Hello %S, number=%d".format(name, 42);
// msg = "Hello World, number=42"| Specifier | Type | Description |
|---|---|---|
%d / %i |
int |
Signed decimal |
%u |
unsigned int |
Unsigned decimal |
%ld / %lu |
long / unsigned long |
Long integer |
%lld / %llu |
long long / unsigned long long |
Long long integer |
%x / %lx / %llx |
corresponding unsigned | Lowercase hexadecimal |
%s |
const char * |
C string (NUL-terminated; NULL outputs (null)) |
%S |
string |
NeverC string value (passed by value, released after output) |
%c |
int |
Single byte character |
%p |
void * |
Pointer (lowercase hex with 0x prefix) |
%% |
— | Literal % |
%s vs %S:
string s = "NeverC";
const char *c = "world";
string result = "hello %s, %S!".format(c, s);
// %s takes const char *, %S takes string valueNeverC string cannot be passed directly to the C standard library's printf. Use .c_str() to convert:
string s = "Hello World";
printf("Content: %s\n", s.c_str());
printf("Length: %zu\n", s.len);Internally always UTF-8 encoded. Byte-level operations (s.len, s.at(i), s.find(...), etc.) work in bytes; codepoint-level operations use the utf8_* family:
| Dot-call | Aliases | Runtime Function | Returns |
|---|---|---|---|
s.utf8_count() |
s.utf8_size() s.utf8_length() |
neverc_string_utf8_count |
Codepoint count |
s.utf8_valid() |
s.is_utf8() |
neverc_string_utf8_valid |
1=valid UTF-8 |
s.utf8_at(i) |
— | neverc_string_utf8_at |
i-th codepoint (uint32_t) |
s.utf8_byte_index(i) |
— | neverc_string_utf8_byte_index |
Byte offset of i-th codepoint |
s.utf8_substr(pos) |
— | neverc_string_utf8_substr |
From pos-th codepoint to end |
s.utf8_substr(pos, n) |
— | same | Extract n codepoints |
s.is_ascii() |
— | neverc_string_is_ascii |
1=all bytes < 0x80 |
string s = "Hello \xF0\x9F\x8E\x89"; // "Hello 🎉"
printf("Bytes: %zu\n", s.len); // 10
printf("Chars: %zu\n", s.utf8_count()); // 6| Dot-call | Runtime Function | Signature | Description |
|---|---|---|---|
s.to_utf16(out, cap) |
neverc_string_to_utf16 |
(string s, uint16_t *out, size_t cap) → size_t |
Write to caller-owned buffer |
s.to_utf32(out, cap) |
neverc_string_to_utf32 |
(string s, uint32_t *out, size_t cap) → size_t |
Write to caller-owned buffer |
s.to_utf16_owned() |
neverc_string_to_utf16_owned |
(string s) → uint16_t * |
Returns freshly allocated NUL-terminated buffer |
s.to_utf32_owned() |
neverc_string_to_utf32_owned |
(string s) → uint32_t * |
Returns freshly allocated NUL-terminated buffer |
s.w_str() |
neverc_string_w_str |
(string s) → wchar_t * |
Platform-adaptive: Windows=UTF-16, Linux/macOS=UTF-32 |
s.to_latin1(out, cap) |
neverc_string_to_latin1 |
(string s, char *out, size_t cap) → size_t |
Convert to Latin-1 (ISO-8859-1) |
Pointers returned by to_utf16_owned(), to_utf32_owned(), and w_str() are auto-released (Sema attaches __neverc_wptr_cleanup). For manual release use neverc_string_wfree(buf).
// UTF-8 direct output (modern terminals support it natively)
string s = "Hello World";
printf("%s\n", s.c_str());
// Win32 wide-char API
string title = "NeverC";
__WCHAR_TYPE__ *ws = title.w_str();
MessageBoxW(NULL, ws, L"Title", MB_OK);
// ws is auto-released, no manual management neededAll encoding methods consume the receiver and return a new owned string, supporting method chaining.
| Dot-call | Runtime Function | Standard | Description |
|---|---|---|---|
s.to_base64() |
neverc_string_to_base64 |
RFC 4648 §4 | Standard Base64 with = padding |
s.from_base64() |
neverc_string_from_base64 |
RFC 4648 §4 | Decode |
s.to_base64_url() |
neverc_string_to_base64_url |
RFC 4648 §5 | URL-safe Base64 (no padding), for JWT/JOSE |
s.from_base64_url() |
neverc_string_from_base64_url |
RFC 4648 §5 | Decode |
s.to_base32() |
neverc_string_to_base32 |
RFC 4648 §6 | Uppercase + = padding, for TOTP/Google Authenticator |
s.from_base32() |
neverc_string_from_base32 |
RFC 4648 §6 | Decode (case-insensitive) |
s.to_hex() |
neverc_string_to_hex |
— | Lowercase hex encoding |
s.from_hex() |
neverc_string_from_hex |
— | Case-insensitive decode |
string data = "Hello, NeverC!";
string b64 = data.to_base64(); // "SGVsbG8sIE5ldmVyQyE="
string back = b64.from_base64(); // "Hello, NeverC!"
string jwt_part = data.to_base64_url(); // URL-safe, no padding
string hex = data.to_hex(); // "48656c6c6f2c204e657665724321"| Dot-call | Runtime Function | Standard | Description |
|---|---|---|---|
s.url_encode() |
neverc_string_url_encode |
RFC 3986 | Percent-encoding (unreserved set preserved, uppercase %XX) |
s.url_decode() |
neverc_string_url_decode |
RFC 3986 | Decode |
s.form_encode() |
neverc_string_form_encode |
application/x-www-form-urlencoded |
Space → +, + → %2B |
s.form_decode() |
neverc_string_form_decode |
same | + → space |
url_encodeandform_encodeare not interchangeable — form-encoded+decoded viaurl_decodestays as literal+.
HTML / JSON / CSV literal-level escaping/unescaping. Each pair maintains strict round-trip fidelity.
| Dot-call | Runtime Function | Standard | Description |
|---|---|---|---|
s.html_escape() |
neverc_string_html_escape |
OWASP Rule #1/#2 | 5 special character entity escapes (& < > " ') |
s.html_unescape() |
neverc_string_html_unescape |
HTML5 | Entity decode (including numeric &#xNNNN;) |
s.json_escape() |
neverc_string_json_escape |
RFC 8259 §7 | JSON string literal escaping |
s.json_unescape() |
neverc_string_json_unescape |
RFC 8259 §7 | Decode (including \uXXXX surrogate pairs) |
s.csv_escape() |
neverc_string_csv_escape |
RFC 4180 §2 | CSV field escaping |
s.csv_unescape() |
neverc_string_csv_unescape |
RFC 4180 §2 | CSV field decode |
string html = "<div class=\"test\">Hello & World</div>";
string safe = html.html_escape();
// "<div class="test">Hello & World</div>"
string json_val = "line1\nline2\ttab";
string escaped = json_val.json_escape();
// "line1\\nline2\\ttab".encrypt() provides compile-time XOR encryption for string literals. The plaintext is never stored in the binary — it is encrypted at compile time and decrypted at runtime via an always_inline function, making static analysis tools (e.g. strings) unable to extract the original content.
string secret = "API_KEY_12345".encrypt();
printf("%s\n", secret.c_str()); // prints "API_KEY_12345" at runtime
// but `strings ./binary` will NOT find "API_KEY_12345"- Compile time: Sema intercepts
.encrypt()on a string literal, XOR-encrypts every byte with a per-literal key, and stores the ciphertext in.rodata - Runtime (general case): The
always_inlinefunction__neverc_string_decrypt_literalXOR-decrypts the bytes into a freshly allocated owned buffer. After decryption, the result is a normal ownedstring. - Runtime (comparison/search fast path): When the encrypted literal appears directly in a comparison or search expression, Sema rewrites to a zero-allocation
__neverc_string_decrypt_*variant that decrypts and compares byte-by-byte without any heap allocation (see "Zero-Allocation Decrypt-and-Compare" below)
- Default: Seed
0obtains fresh operating-system entropy in the compiler for each encrypted literal and fails closed if entropy is unavailable. Independent builds produce different ciphertext. - CLI override: Use a nonzero
-fstring-encrypt-key=0xDEADBEEFto select a deterministic full 64-bit seed; a translation-unit-local nonce separates literals, making this suitable for reproducible builds and tests.
.encrypt() automatically supports all string literal types — the compiler folds wide/UTF-16/UTF-32 literals to UTF-8 before encryption:
string a = "hello".encrypt(); // ordinary
string b = u8"héllo".encrypt(); // UTF-8
string c = L"中文".encrypt(); // wide
string d = u"\u4E2D\u6587".encrypt(); // UTF-16
string e = U"\U0001F389party".encrypt(); // UTF-32
string f = R"(line1\nline2)".encrypt(); // rawWhen an encrypted literal is used directly in a comparison or search expression, the compiler automatically bypasses heap allocation. Instead of decrypting the entire string into memory and then comparing, it XOR-decrypts and compares byte-by-byte — plaintext never materialises in memory.
Optimized expressions (all zero-allocation when one operand is .encrypt()):
| Category | Expressions |
|---|---|
| Equality | s == "key".encrypt(), s != "key".encrypt() |
| Relational | s < "key".encrypt(), s > "key".encrypt(), <=, >= |
| Prefix/Suffix | s.starts_with("prefix".encrypt()), s.ends_with("suffix".encrypt()) |
| Containment | s.contains("needle".encrypt()) |
| Case-insensitive | s.eq_ic(...), s.starts_with_ic(...), s.ends_with_ic(...), s.contains_ic(...) |
| Search | s.find("needle".encrypt()), s.rfind(...), s.find(... , pos), s.rfind(..., pos) |
| Count | s.count("pattern".encrypt()) |
| Case-insensitive search | s.find_ic("needle".encrypt()) |
Security benefit: In a memory dump or scan, the plaintext of the encrypted literal is never fully present — each byte exists only momentarily in a register during the comparison loop.
string input = get_user_input();
// Zero-allocation: decrypts and compares byte-by-byte, short-circuits on mismatch
if (input == "admin".encrypt()) {
grant_access();
}
// Also zero-allocation
if (input.starts_with("/api/v1".encrypt())) {
handle_api_request();
}When .encrypt() is used in a context that needs the actual string value (e.g., s.to_upper(), printf("%s", s.c_str())), the normal decrypt-to-heap path is used.
.encrypt()can only be applied to string literals. Calling it on a variable produces a compile error:
string s = "hello";
string e = s.encrypt(); // ERROR: .encrypt() can only be applied to string literals.encrypt()takes no arguments:
string e = "hello".encrypt(42); // ERROR: .encrypt() takes no arguments- Double encryption is rejected:
string e = "hello".encrypt().encrypt(); // ERROR: .encrypt() can only be applied to string literalsThe encryption and decryption operations are controlled by two macros:
NEVERC_STRING_ENCRYPT_BYTE(byte, key, idx) // compile-time: plaintext → ciphertext
NEVERC_STRING_DECRYPT_BYTE(byte, key, idx) // runtime: ciphertext → plaintextENCRYPT_BYTE defaults to XOR:
((char)((unsigned char)(byte) ^ (unsigned char)((key) >> (8 * ((idx) % sizeof(size_t))))))DECRYPT_BYTE defaults to an XOR-free arithmetic decomposition — it computes a ^ b as (a + b) - (a & b) - (b & a) using volatile intermediates to prevent LLVM from optimizing back to a xor instruction. This makes the decryption code invisible to simple disassembly XOR pattern matchers. The decomposition can be further strengthened with MBA (Mixed Boolean-Arithmetic) obfuscation passes.
To use a non-XOR algorithm, define both macros before the string prelude is included. They must be mathematical inverses — DECRYPT(ENCRYPT(b, k, i), k, i) == b for all inputs:
// Example: add/subtract algorithm
#define NEVERC_STRING_ENCRYPT_BYTE(byte, key, idx) \
((char)((unsigned char)(byte) + (unsigned char)((key) >> (8 * ((idx) % sizeof(size_t))))))
#define NEVERC_STRING_DECRYPT_BYTE(byte, key, idx) \
((char)((unsigned char)(byte) - (unsigned char)((key) >> (8 * ((idx) % sizeof(size_t))))))Important: The compiler's Sema layer uses a C++ function that mirrors
NEVERC_STRING_ENCRYPT_BYTE's default (XOR) behavior for compile-time encryption. If you overrideNEVERC_STRING_ENCRYPT_BYTEto a non-XOR algorithm, the compile-time encryption will still use XOR. To work around this, defineNEVERC_STRING_ENCRYPT_BYTEas XOR composed with your custom transform, ensuring the compile-time XOR step and your runtime transform combine correctly.
.encrypt() works inside aggregate initializers. Owned string members are auto-released on scope exit (see Composite Type Cleanup):
typedef struct { string user; string pass; } creds;
creds login = {.user = "admin".encrypt(), .pass = "s3cret".encrypt()};
string routes[] = {"/api/v1".encrypt(), "/api/v2".encrypt()};
string grid[2][2] = {
{"a".encrypt(), "b".encrypt()},
{"c".encrypt(), "d".encrypt()}
};
// Zero-allocation comparisons still apply
if (login.user == "admin".encrypt()) { /* ... */ }String encryption works in all compilation modes including dyncode (-fdyncode):
// DynCode with encrypted strings — compiles to position-independent flat binary
int main(int a, int b) {
string secret = "dyncode_secret".encrypt();
if (secret.starts_with("shell".encrypt())) {
// ...
}
return 0;
}In dyncode mode, the encrypted string decryption uses the dyncode-local arena allocator instead of malloc. All zero-allocation comparison patterns (decrypt-and-compare, decrypt-and-search) work identically. Both user-mode and kernel-mode dyncode contexts (-mdyncode-context=kernel) are supported.
| Flag | Description |
|---|---|
-fstring-encrypt-key=<hex> |
Override the full 64-bit protection seed; 0 uses fresh OS entropy |
| Macro | Default | Description |
|---|---|---|
NEVERC_STRING_ENCRYPT_BYTE(byte, key, idx) |
XOR with rotating key bytes | Per-byte encryption operation (compile-time) |
NEVERC_STRING_DECRYPT_BYTE(byte, key, idx) |
XOR with rotating key bytes | Per-byte decryption operation (runtime); must be inverse of ENCRYPT_BYTE |
All methods returning string support chaining. Intermediate values are auto-released with no leaks:
string result = input.to_upper().trim().replace_all(" ", "_");
string encoded = payload.to_base64_url();
string cleaned = raw.url_decode().from_base64().trim();| Mode | Description | String Function Body Source | Symbol Visibility |
|---|---|---|---|
| Hosted (default) | Normal executables | Precompiled LLVM bitcode merge | 0 symbols under LTO |
| DynCode | Position-independent flat binary | Full source prelude injection + arena rewrite | 0 symbols (AlwaysInliner) |
| LTO | Link-time optimization | Same as Hosted, LTO DCE further prunes | 0 symbols |
The final output binary exposes no neverc_string_* symbols.
| Flag | Description |
|---|---|
-fbuiltin-string |
Enable builtin string type (off by default) |
-fdyncode |
Enable dyncode mode (auto-enables builtin string) |
-DNEVERC_STRING_ALLOC=xxx |
Custom allocator (triggers full source prelude) |
-DNEVERC_STRING_FREE=xxx |
Custom free function |
| Macro | Default | Description |
|---|---|---|
NEVERC_STRING_ALLOC |
__builtin_malloc |
Owned buffer allocator |
NEVERC_STRING_FREE |
__builtin_free |
Corresponding free function |
NEVERC_STRING_NPOS |
(size_t)-1 |
"Not found" sentinel value |
NEVERC_STRING_MAX_LEN |
(size_t)-2 |
Payload length ceiling |
NEVERC_STRING_INT_BUF |
24 |
Stack buffer size for from_int / from_uint |
NEVERC_STRING_USER_ARENA_SIZE |
64 KB |
DynCode user-mode arena size |
NEVERC_STRING_KERNEL_ARENA_SIZE |
4 KB |
DynCode kernel-mode arena size |
At compiler build time, string runtime functions are precompiled into LLVM bitcode and embedded in the compiler binary. When compiling user code, only a thin header (struct + macros + extern declarations) is injected, and the bitcode is merged after CodeGen via llvm::Linker::linkModules().
prelude .inc (11 fragments)
│
├──→ gen_thin_header.py ──→ extern declarations
│
└──→ gen_string_runtime.py ──→ BuiltinStringRuntime.c
│
▼ neverc -c -emit-llvm
│
.bc (bitcode)
│
▼ bin2c.py
│
BuiltinStringBitcode.h
(embedded in compiler binary)
user.c
│
▼ FrontendAction: inject thin header
│ struct string { data, len, cap }
│ macros + inline functions
│ extern declarations
│
▼ Lexer → Parser → Sema → CodeGen
│
▼ StringRuntimeLinkerPass (PipelineStartEP)
│ 1. Parse embedded bitcode → Module
│ 2. Stamp kRuntimeFnAttr (zero name matching)
│ 3. Compute user-referenced functions + call-graph BFS transitive closure
│ 4. Pre-merge prune unreferenced functions
│ 5. llvm::Linker::linkModules() merge
│ 6. InternalizePass → all symbols become internal
│ 7. Mark-and-sweep DCE → remove unreachable functions
│ 8. llvm.used cleanup
│
▼ Optimization pipeline (incl. AlwaysInliner / GlobalDCE)
│
▼ Output .o / bitcode
StringRuntimeLinkerPass is the only "name → attribute" conversion point in the entire codebase. All downstream IR passes identify runtime functions via F.hasFnAttribute("neverc-string-runtime") (a single bit check), eliminating string prefix matching.
Benefit: Obfuscation passes can safely rename runtime functions without breaking the identification chain.
Bitcode generation uses a two-stage bootstrap:
ninja neverc # stage 1 (empty bitcode placeholder)
ninja neverc-bootstrap-string-bc # compile string runtime with neverc itself → .bc
ninja neverc # stage 2 (embed real bitcode)| Condition | Reason |
|---|---|
-fdyncode |
StringRuntimePass needs source-level function bodies |
-DNEVERC_STRING_ALLOC=xxx |
Custom allocator is baked into bitcode; must recompile |
| Empty embedded bitcode | First build (bootstrap stage 1) has no bitcode |
Does not use bitcode merge; instead injects the full source prelude:
FrontendAction: inject full prelude
│
▼ Lexer → Parser → Sema → CodeGen
│
▼ StringRuntimeLinkerPass ← legacy branch: stamp kRuntimeFnAttr, skip merge
▼ ZeroRelocPass
▼ IndirectBrPass
▼ MemIntrinPass
▼ StringRuntimePass ← rewrite malloc → stack arena
▼ AlwaysInliner ← inline all functions
▼ Data2TextPass
│
▼ dyncode.bin
StringRuntimePass rewrites __builtin_malloc/__builtin_free into a stack arena allocator:
- All memory allocation happens on the stack, no external library linkage
- Arena uses
{size, next, self, tag}per-allocation headers for validation - User-mode arena defaults to 64 KB, kernel-mode to 4 KB
AlwaysInlinerultimately inlines all functions — zero standalone symbols in the dyncode
Sema rewrites dot-call syntax into C function calls via buildNeverCStringRuntimeCall():
// User writes
string result = s.find("hello");
// Sema rewrites to
string result = neverc_string_find(s, __neverc_string_make_view("hello", 5));When the user writes s.method(args...), Sema searches for the target function in this priority order:
- Char overload (
BuiltinStringMethodCharOverloads.def): Matches when first argument has integer/char type - Arity overload (
BuiltinStringMethodOverloads.def): Matches specialized versions by argument count - Default argument completion (
BuiltinStringMethodDefaults.def): Appends default values (e.g.,s.substr(pos)→s.substr(pos, NPOS)) - Default mapping (
BuiltinStringMethodNames.def): General method → runtime function mapping
Most methods pass the receiver by string value. A few require pointer semantics:
| Method | Receiver | Reason |
|---|---|---|
s.assign(t) |
string * |
Needs in-place modification of target |
s.swap(t) |
string *, string * |
Both sides must be addressable |
s.capacity() |
const string * |
Avoids retain copy that would flatten true capacity |
| Compilation Mode | neverc_string_* Symbols in Final Binary |
|---|---|
| LTO (default) | 0 — internalize + LTO DCE fully eliminates |
| Non-LTO -O2 | Few t (internal) — GlobalDCE removes unused |
| Non-LTO -O0 | All t (internal) — DCE not run but symbols remain internal |
| DynCode | 0 — AlwaysInliner inlines everything |
neverc/
├── include/neverc/Foundation/Builtin/
│ ├── BuiltinString.h # API declarations
│ ├── BuiltinStringNames.h # Compile-time function name constants + prefix invariant static_assert
│ ├── BuiltinStringRoster.def # All functions X-macro registry (single source of truth)
│ ├── BuiltinStringRuntimeNames.def # Roster's IsPublic=1 filtered view
│ ├── BuiltinStringMethodNames.def # dot-call method name → runtime function mapping
│ ├── BuiltinStringMethodOverloads.def # Arity-based method overload table
│ ├── BuiltinStringMethodCharOverloads.def # Char-typed argument method overloads
│ ├── BuiltinStringMethodDefaults.def # Default argument completion rules
│ ├── BuiltinStringMethodDefaultArgKinds.def # Default argument type enum
│ ├── BuiltinStringMethodReceiverKinds.def # Pointer-receiver method list
│ ├── BuiltinStringMethodReceiverKindsRoster.def # Receiver type enum
│ ├── BuiltinStringBorrowedViewHelpers.def # Borrowed view helper list (CStr/Data)
│ ├── BuiltinStringLValueDirectHelpers.def # Lvalue-direct helper whitelist
│ ├── BuiltinStringWptrProducers.def # Wide pointer producer list
│ ├── BuiltinStringPreludeFragments.def # Prelude fragment ordering
│ └── BuiltinStringPrelude/ # 12 .inc function implementation fragments
│ ├── Type.inc # struct definition + base operations
│ ├── Allocation.inc # Memory allocation/release/assignment
│ ├── Accessors.inc # c_str, data, at, front, back
│ ├── Capacity.inc # reserve, shrink_to_fit, capacity
│ ├── Compare.inc # eq, compare + case-insensitive family
│ ├── Search.inc # find, rfind, contains, starts/ends_with + charset search + _ic family
│ ├── Mutation.inc # append, insert, erase, replace, pad, clear, swap, resize
│ ├── Utility.inc # clone, substr, trim, repeat, reverse, hash, count, split/join, numeric conversion
│ ├── Encoding.inc # UTF-8/16/32, Latin-1, Base64/Base32/Hex, URL/Form encoding
│ ├── WebCodec.inc # HTML/JSON/CSV escape/unescape
│ ├── EncryptDecrypt.inc # NC_XORSTR encrypt/decrypt helpers
│ └── Format.inc # printf-style format
│
├── include/neverc/DynCode/IR/
│ ├── StringRuntimeABI.h # Cross-layer ABI: kRuntimeFnAttr, arena constants
│ └── StringRuntimePass.h # DynCode arena rewrite pass declaration
│
├── lib/Foundation/Builtin/
│ ├── BuiltinString.cpp # Core implementation (method dispatch table + thin header + bitcode API)
│ ├── BuiltinStringThinHeaderPrologue.inc # Thin header fixed part
│ ├── gen_thin_header.py # Extract extern declarations from prelude
│ ├── gen_string_runtime.py # Generate standalone compilation unit
│ └── bin2c.py # .bc → C header embedding
│
├── lib/Analyze/Core/
│ └── Sema.cpp # NeverCStringFnKinds DenseMap
│
├── lib/Emit/Backend/
│ ├── StringRuntimeLinker.h # Bitcode merge pass declaration
│ ├── StringRuntimeLinker.cpp # Bitcode merge + kRuntimeFnAttr stamp
│ └── BackendUtil.cpp # Register linker pass in all modes
│
└── lib/DynCode/IR/
└── StringRuntimePass.cpp # DynCode arena rewrite pass
- Add a row to
BuiltinStringRoster.def:NEVERC_BUILTIN_STRING_FN(NameId, "neverc_string_xxx", 1) - Implement the function body in the corresponding
BuiltinStringPrelude/*.inc - (Optional) Add a dot-call mapping in
BuiltinStringMethodNames.def - (Optional) Add arity overloads in
BuiltinStringMethodOverloads.def - (Optional) Add char overloads in
BuiltinStringMethodCharOverloads.def
No other declaration sites need to change.