serial -- 직렬화 (JSON / XML / HTML / CSV / YAML / Markdown / Protobuf)
std/serial/은 형식에 구애받지 않는 Value 트리와, 이 트리를 읽고 쓸 수 있는 다섯 가지 형식 백엔드(JSON, XML, HTML, CSV, YAML)를 제공합니다. SafeC에는 필드 이름 리플렉션이 없으므로(오직 개수만 알려주는 fieldcount(T)만 있고 이름은 알 수 없습니다), 구조체를 직렬화 가능하게 만들려면 자동 #[derive] 스타일 매크로 대신 to_value() 메서드를 직접 작성해야 합니다.
#include <std/serial/value.h>
#include <std/serial/json.h>
#include <std/serial/xml.h>
#include <std/serial/html.h>
Value 트리
#define VAL_NULL 0
#define VAL_BOOL 1
#define VAL_INT 2
#define VAL_FLOAT 3
#define VAL_STRING 4
#define VAL_ARRAY 5
#define VAL_OBJECT 6
struct Value {
int kind; // one of the VAL_* constants above
// Construction.
static struct Value null();
static struct Value from_bool(int v);
static struct Value from_int(long long v);
static struct Value from_float(double v);
static struct Value from_str(const &char s); // copies s
static struct Value array(); // empty; grow with v.array_push
static struct Value object(); // empty; grow with v.object_set
int kind_of() const;
int is_null() const;
int as_bool() const;
long long as_int() const;
double as_float() const;
const &char as_string() const;
// Array/Object element access -- read-only views (the returned
// reference aliases storage owned by this Value, don't free it
// separately). 'array_at' trusts 'idx' is in range (see array_len());
// 'object_get' is the one nullable lookup -- no entry for 'key' is a
// real, expected outcome, not a caller error.
unsigned long array_len() const;
&Value array_at(unsigned long idx) const;
unsigned long object_len() const;
?&Value object_get(const &char key) const; // empty (null) if the key isn't present
const &char object_key_at(unsigned long idx) const; // idx번째 항목, 삽입 순서
&Value object_value_at(unsigned long idx) const;
// Building and lifetime.
void array_push(struct Value v); // takes ownership of v
void object_set(const &char key, struct Value v); // takes ownership of v
struct Value clone() const; // deep copy
void free() consume; // releases the whole tree
// Text formats (any Format, see below).
generic<F: Format> struct String encode(F f) const; // a fresh String
generic<F: Format> static struct Value decode(F f, const &char text, ?&int ok); // *ok: 1, or 0
}
namespace std 밖에서는 생성자를 std::Value::object(), std::Value::from_str("...")처럼 호출합니다. 각 포맷 모듈은 자체 자유 함수(json_write / value_to_json / json_parse, xml_parse, yaml_parse, csv_parse, html_parse)도 그대로 유지하며, encode / decode는 Format 트레이트를 통해 같은 코드에 도달합니다.
소유권
array_push/object_set은 전달받은 Value의 소유권을 가져갑니다 — 그 이후에는 직접 해제하면 안 됩니다. 자신만의 복사본이 여전히 필요하다면 먼저 .clone()을 호출합니다. .free()는 트리 전체(문자열, 배열 요소, 객체 항목)를 재귀적으로 해제합니다.
object_get은 널이 될 수 있는 참조를 반환합니다 — 필드나 메서드로 바로 체이닝하지 말고 match(또는 .is_null()/.default())로 언래핑합니다. 이는 SafeC의 모든 ?&T에 적용되는 것과 동일한 규칙입니다(안전성 참고).
long long age = 0;
match (parsed.object_get("age")) {
case null: break;
case some(a): age = a.as_int();
}
array_at은 널이 될 수 없으므로(array_len()으로 확인된 범위 내 인덱스를 신뢰합니다), v.array_at(0).as_string()처럼 바로 체이닝할 수 있습니다.
객체는 항목을 삽입 순서대로 보관합니다. object_key_at / object_value_at은 그 순서대로 항목을 순회합니다 — 파싱한 맵의 순서가 중요할 때(YAML 설정의 섹션, 다시 내보낼 JSON 객체) 필요한 기능입니다:
for (unsigned long i = 0UL; i < cfg.object_len(); i = i + 1UL) {
std::print(cfg.object_key_at(i));
std::print(" = ");
std::println(cfg.object_value_at(i).as_string());
}
구조체를 직렬화 가능하게 만들기
구조체 필드로부터 Value 트리를 만드는 to_value() 메서드를 작성합니다.
#include <std/serial/value.h>
#include <std/serial/json.h>
#include <std/collections/string.h>
extern int printf(const char* fmt, ...);
struct Point { double x; double y; struct Value to_value() const; };
struct Value Point::to_value() const {
struct Value v = Value::object();
v.object_set("x", Value::from_float(self.x));
v.object_set("y", Value::from_float(self.y));
return v;
}
int main() {
struct Point p; p.x = 1.5; p.y = 2.5;
struct Value pv = p.to_value();
struct String json = value_to_json(&pv);
unsafe { printf("%s\n", json.as_ptr()); } // {"x":1.5,"y":2.5}
json.free();
pv.free();
return 0;
}
from_value(const &Value) 메서드는 동일한 방식으로 디코딩 방향을 담당합니다 — object_get/array_at/as_int/등을 사용해 필드를 다시 읽어냅니다.
트레이트로서의 포맷
모든 텍스트 백엔드는 해당 포맷의 작성기와 파서를 두 메서드로 가진 작은 구조체이기도 하며, value.h의 Format 트레이트가 그 형태에 이름을 붙입니다:
trait Format {
void write(const &Value v, &String out) const; // appends v's text to out
struct Value parse(const &char text, ?&int ok) const; // *ok: 1, or 0 on malformed input
}
| 백엔드 | 헤더 | 생성 | 동등한 함수 |
|---|---|---|---|
Json |
json.h |
Json::new() |
json_write / json_parse |
Yaml |
yaml.h |
Yaml::new() |
yaml_write / yaml_parse |
Csv |
csv.h |
Csv::new() |
csv_write / csv_parse |
Xml |
xml.h |
Xml::new("root") |
xml_write(v, root, out) / xml_parse; 루트 요소의 태그를 담으며, 태그 문자열은 Xml보다 오래 살아야 합니다 |
Html |
html.h |
Html::new() |
html_write / html_parse |
v.encode(f)는 트리를 포맷 f로 담은 새 String을 반환하고, Value::decode(f, text, &ok)는 그것을 파싱합니다. 둘 다 Format으로 바운드된 자기 타입 매개변수를 가진 메서드이므로, 포맷은 컴파일 타임에 정해지고 호출은 직접 호출입니다 — 함수 포인터 테이블이 없습니다. 제네릭 코드도 같은 방식으로 포맷을 받으며, 이런 형태의 write와 parse 메서드를 가진 직접 만든 구조체도 Format이 됩니다:
#include <std/serial/value.h>
#include <std/serial/json.h>
#include <std/serial/yaml.h>
#include <std/serial/xml.h>
#include <std/io.h>
// Written once, works for every format: encode, then decode the text back.
generic<F: Format>
long long round_trip_age(F f, const &Value v) {
struct String text = v.encode(f);
int ok = 0;
struct Value back = std::Value::decode(f, text.as_ptr(), &ok);
long long age = -1LL;
match (back.object_get("age")) {
case some(a): age = a.as_int();
case null: { }
}
back.free();
text.free();
return ok ? age : -1LL;
}
int main() {
struct Value v = std::Value::object();
v.object_set("name", std::Value::from_str("Ada"));
v.object_set("age", std::Value::from_int(36LL));
struct String json = v.encode(std::Json::new());
std::println(json.as_ptr()); // {"name":"Ada","age":36}
long long a1 = round_trip_age(std::Json::new(), &v);
long long a2 = round_trip_age(std::Yaml::new(), &v);
long long a3 = round_trip_age(std::Xml::new("person"), &v);
json.free();
v.free();
return a1 == 36LL && a2 == 36LL && a3 == 36LL ? 0 : 1;
}
Markdown과 Protobuf는 Format이 아닙니다: Markdown은 Value 트리 대신 자체 AST를 가지며, Protobuf는 스키마에 맞춰 필드 단위로 작성하는 바이너리 인코딩입니다.
JSON
void json_write(const &Value v, &String out); // 추가만 함; 'out'을 먼저 비우지 않음
struct String value_to_json(const &Value v); // 편의 함수: 새 String 반환
struct Value json_parse(const &char text, ?&int ok);
// 성공하면 *ok = 1과 파싱된 트리; 실패하면 *ok = 0과 Value::null().
// 첫 완전한 값 이후의 후행 내용은 무시됩니다.
JSON은 다섯 가지 형식 중 구조적 모호함이 없는 유일한 형식입니다 — 문법이 타입을 명시적으로 표시하므로, 숫자처럼 보이는 문자열이나 빈 문자열을 포함해 정확히 왕복 변환됩니다.
struct Value v = Value::object();
v.object_set("name", Value::from_str("Ada"));
v.object_set("age", Value::from_int(36));
struct String json = value_to_json(&v);
printf("%s\n", json.as_ptr()); // {"name":"Ada","age":36}
int ok = 0;
struct Value parsed = json_parse(json.as_ptr(), &ok);
long long age = 0;
match (parsed.object_get("age")) {
case null: break;
case some(a): age = a.as_int(); // 36
}
json.free();
v.free();
parsed.free();
XML
void xml_write(const &Value v, const &char tag, &String out);
struct String value_to_xml(const &Value v, const &char root_tag);
struct Value xml_parse(const &char text, ?&int ok); // 루트 태그 자체는 반환되지 않고, 그 내용만 반환됨
매핑:
Value 종류 |
XML 형태 (태그 T 아래) |
|---|---|
VAL_OBJECT |
<T><key1>...</key1><key2>...</key2></T> — 각 키는 이름 있는 자식 엘리먼트가 됨 |
VAL_ARRAY |
<T><item>...</item><item>...</item></T> — 배열 요소는 자체 이름을 갖지 않음 |
VAL_STRING/INT/FLOAT/BOOL |
<T>텍스트 내용</T> |
VAL_NULL |
<T/> |
struct Value v = Value::object();
v.object_set("name", Value::from_str("Ada <Countess>"));
v.object_set("age", Value::from_int(36));
struct String xml = value_to_xml(&v, "person");
printf("%s\n", xml.as_ptr());
// <person><name>Ada <Countess></name><age>36</age></person>
int ok = 0;
struct Value parsed = xml_parse(xml.as_ptr(), &ok);
// parsed.object_get("age")를 위 JSON 예제와 같은 방식으로 언래핑
xml.free();
v.free();
parsed.free();
xml_parse/json_parse는 이 모듈 자체가 만들어낸 출력 형태만 이해합니다 — 속성도, CDATA도, xml_write가 내보내는 5개(& < > " ') 외의 엔터티도 없습니다.
HTML
void html_escape(&String out, const &char s); // 단독 사용 가능, Value 렌더러와 무관
void html_unescape(&String out, const &char s); // < ' — — ... -> UTF-8 텍스트
void html_strip_tags(&String out, const &char s); // HTML 조각의 텍스트 내용
void html_write(const &Value v, &String out);
struct String value_to_html(const &Value v);
struct Value html_parse(const &char text, ?&int ok);
JSON/XML과 달리 HTML은 데이터 교환 형식이 아닙니다 — html_write는 Value 트리를 사람이 읽을 수 있는 문서 조각으로 렌더링합니다.
Value 종류 |
HTML 형태 |
|---|---|
VAL_OBJECT |
<dl><dt>key</dt><dd>...</dd>...</dl> (정의 목록) |
VAL_ARRAY |
<ul><li>...</li>...</ul> |
VAL_STRING/INT/FLOAT/BOOL |
이스케이프된 텍스트, 감싸는 태그 없음 |
VAL_NULL |
<em>null</em> |
struct Value v = Value::object();
v.object_set("comment", Value::from_str("<script>alert(1)</script>"));
struct String html = value_to_html(&v);
printf("%s\n", html.as_ptr());
// <dl><dt>comment</dt><dd><script>alert(1)</script></dd></dl>
html.free();
v.free();
html_parse는 이와 동일한 <dl>/<ul>/<em>null</em>/맨 텍스트 형태를 다시 Value 트리로 읽어들입니다 — 이는 범용 HTML 파서가 아니며, (xml_parse와 마찬가지로) 임의의 HTML 문서가 아니라 이 모듈 자체의 출력을 왕복시키기 위한 것입니다.
html_escape, html_unescape, html_strip_tags는 Value 렌더러와 무관하게 모든 텍스트에 사용할 수 있습니다. html_unescape는 숫자 참조(', —)와 자주 쓰이는 이름 참조(amp lt gt quot apos nbsp copy reg trade hellip mdash ndash, 곡선 따옴표, laquo raquo middot bull times divide deg, 화살표, ne le ge)를 디코딩하며, 그 밖의 것은 그대로 복사합니다. html_strip_tags는 태그와 주석을 제거하고, <script>/<style> 내용을 버리며, 인접한 블록의 단어가 붙지 않도록 블록 경계(</p>, <br>, </li>, </td>, </h2>, ...)마다 공백 하나를 넣습니다. 참조는 인코딩된 채로 두므로, 평문이 필요하면 이어서 html_unescape를 호출하십시오:
struct String tags = String::new();
html_strip_tags(&tags, "<h2>Title</h2><p>Fish & chips</p>");
struct String text = String::new();
html_unescape(&text, tags.as_ptr()); // "Title Fish & chips"
CSV
#include <std/serial/csv.h>
CSV 문서는 행(row)들의 VAL_ARRAY이며, 각 행 자체는 VAL_STRING 필드들의 VAL_ARRAY입니다 — CSV는 고유한 타입 체계가 없으므로(모든 필드가 텍스트), json_parse와 달리 int/float/bool 추론이 없습니다. 어떤 열이 숫자를 담고 있다고 알고 있다면 각 필드를 직접 변환합니다(String::parse_int()/parse_float()).
void csv_write(const &Value v, &String out); // 'v'는 반드시 VAL_ARRAY의 VAL_ARRAY여야 함
struct String value_to_csv(const &Value v);
struct Value csv_parse(const &char text, ?&int ok);
콤마, 큰따옴표, 개행을 포함하는 필드는 RFC 4180에 따라 따옴표로 묶이며, 내장된 큰따옴표는 이중으로 표기됩니다(""). 파싱 시 "\n"과 "\r\n" 줄바꿈 모두 허용되며, 후행 개행은 선택 사항입니다. 잘못된 형식의 따옴표 필드(닫는 "가 없는 여는 ")를 만나면 *ok는 0으로 설정되고 반환값은 그때까지 파싱된 내용이 됩니다.
int ok = 0;
struct Value rows = csv_parse("name,age\nAda,36\n", &ok);
&Value row0 = rows.array_at(0);
&Value name = row0.array_at(0); // VAL_STRING "name"
printf("%s\n", name.as_string());
rows.free();
YAML
#include <std/serial/yaml.h>
설정 파일과 프런트매터를 다루는 YAML 1.1의 서브셋입니다: 블록 스칼라(평문, 각자의 이스케이프 규칙을 따르는 단일/이중 따옴표), 블록 시퀀스(- item, 맵 목록의 축약형인 - key: value 포함), 블록 매핑(key: value, 들여쓰기로 중첩), 한 줄짜리 플로우 컬렉션([a, b], {k: v, n: [1, 2]}, 중첩 가능, 따옴표 또는 평문 항목), 리터럴/폴디드 블록 스칼라(|, >, - 제거와 + 유지 chomping 지원), # 주석(줄 맨 앞 또는 공백 뒤 — a#b는 평문 스칼라), ~/null/true/false/숫자 코어 스키마 스칼라 형태.
지원하지 않는 것(완전한 YAML 1.2 프로세서는 훨씬 더 큰 작업입니다): 여러 줄에 걸친 플로우 컬렉션, 앵커/앨리어스, 태그, 다중 문서 스트림, 병합 키. 이들 중 하나를 사용하는 문서는 평문 스칼라 텍스트로 파싱되거나(닫히지 않은 [1, 2는 문자열 "[1, 2"로 남습니다), 아래 yaml_parse의 ok 계약에 따라 실패합니다.
void yaml_write(const &Value v, &String out);
struct String value_to_yaml(const &Value v);
struct Value yaml_parse(const &char text, ?&int ok);
// 구조적 오류(들여쓰기 불일치, 종료되지 않은 따옴표 스칼라, 시퀀스/매핑
// 항목도 아니고 맨 스칼라가 기대되는 위치의 맨 스칼라도 아닌 줄)가
// 있으면 *ok = 0 -- 반환값은 오류 전까지 파싱된 내용입니다.
struct Value v = Value::object();
v.object_set("name", Value::from_str("Ada"));
v.object_set("age", Value::from_int(36));
struct String yaml = value_to_yaml(&v);
printf("%s", yaml.as_ptr());
// name: Ada
// age: 36
yaml.free();
v.free();
페이지 프런트매터에서처럼 플로우 컬렉션과 블록 스칼라를 쓰는 예:
int ok = 0;
struct Value fm = yaml_parse(
"outline: [2, 4]\n"
"hero: {name: SafeC, actions: [start, docs]}\n"
"summary: >\n"
" Folded lines become\n"
" one paragraph.\n"
"code: |\n"
" int x;\n"
" int y;\n", &ok);
// outline -> [2, 4] (VAL_INT의 VAL_ARRAY)
// summary -> "Folded lines become one paragraph.\n"
// code -> "int x;\nint y;\n"
fm.free();
Markdown
#include <std/serial/markdown.h>
Markdown은 키/값 데이터가 아니라 문서 마크업이므로 Value 트리 대신 자체 AST를 가집니다. MdNode::parse는 CommonMark를 읽고, MdNode::parse_opts는 GitHub 방식(GFM)과 문서 사이트용 확장을 더합니다. 트리는 HTML(예: HTTP 응답 본문)이나 다시 Markdown으로 렌더링하거나, 직접 순회할 수 있습니다.
CommonMark(항상 켜짐): ATX(#)와 setext(=== / --- 밑줄) 제목, 문단, 구분선, 느슨한 이어쓰기(lazy continuation)를 지원하는 인용 블록, 글머리표(- * +)와 번호(1. / 1)) 목록 — 중첩, tight/loose, 항목 안에 어떤 블록(코드, 인용, 목록)이든 포함 가능 — 정보 문자열이 있는 펜스 코드(``` / ~~~), 들여쓰기 코드. 인라인은 CommonMark의 구분자 규칙을 따릅니다: *em* / _em_, **strong** / __strong__(단어 안의 _는 강조가 아니므로 snake_case_name은 텍스트로 남습니다), `code`(강조보다 우선), 백슬래시 이스케이프, 엔티티/숫자 참조(©, —), 제목(title)이 있는 링크와 이미지(링크 텍스트 안에 어떤 인라인이든 가능: [`code` *and* em](/url "title")), <https://...>와 <user@host> 자동 링크, 강제 줄바꿈(끝의 공백 두 개 또는 \). 지원하지 않는 것: 참조 스타일 링크([x][ref]와 [ref]: url), 각주.
확장(MdOptions 필드. MdOptions::gfm()은 앞의 세 가지를 켭니다):
| 옵션 | 문법 |
|---|---|
tables |
GFM 표: 헤더 행, 구분 행(|:--|:-:|--:|가 왼쪽/가운데/오른쪽 정렬을 지정), 본문 행. 셀 안의 파이프는 |로 이스케이프 |
strikethrough |
~~text~~ |
autolinks |
맨 https://…, http://…, www.… URL이 링크가 됨(끝의 문장 부호는 제외) |
html |
원시 HTML 블록과 인라인 태그/주석을 이스케이프 없이 통과 — 기본값은 꺼짐이라 신뢰할 수 없는 입력은 무해하게 남음 |
containers |
::: kind [title] … ::: 블록(tip, warning, details, … — 아무 단어나 가능), 중첩 가능. <div class="custom-block kind">와 <p class="custom-block-title">(제목, 없으면 kind를 대문자로)로 렌더링되며, details는 <details>/<summary> |
heading_ids |
모든 제목에 id 부여: ## Title {#custom-id}, 없으면 텍스트의 md_slugify 결과(중복 시 -1, -2, …) |
#define MD_DOC 0
#define MD_HEADING 1 // level 1-6; id (heading_ids)
#define MD_PARAGRAPH 2
#define MD_LIST 3 // ordered, start, tight
#define MD_LIST_ITEM 4
#define MD_CODE_BLOCK 5 // text = code, info = info string ("c", "bash")
#define MD_BLOCKQUOTE 6
#define MD_HR 7
#define MD_TEXT 8 // inline text in 'text' (references decoded)
#define MD_BOLD 9
#define MD_ITALIC 10
#define MD_CODE_SPAN 11 // inline code in 'text'
#define MD_LINK 12 // children = link text; text = URL; title
#define MD_TABLE 13 // rows; the first, 'header' row is the thead
#define MD_TABLE_ROW 14
#define MD_TABLE_CELL 15 // inline children; align = MD_ALIGN_*
#define MD_CONTAINER 16 // info = kind; first child MD_CONTAINER_TITLE when titled
#define MD_HTML_BLOCK 17 // raw HTML in 'text'
#define MD_HTML_INLINE 18
#define MD_IMAGE 19 // children = alt text; text = src; title
#define MD_STRIKE 20
#define MD_BREAK 21 // hard line break
#define MD_CONTAINER_TITLE 22
struct MdOptions {
int tables, html, containers, heading_ids, strikethrough, autolinks;
static struct MdOptions commonmark(); // all off (what parse() uses)
static struct MdOptions gfm(); // tables, strikethrough, autolinks
};
struct MdNode {
int kind;
int level; // MD_HEADING
int ordered; // MD_LIST
int start; // MD_LIST: first number of an ordered list
int tight; // MD_LIST: no blank lines between items/blocks (no <p> in items)
int align; // MD_TABLE_CELL: MD_ALIGN_NONE/LEFT/CENTER/RIGHT
int header; // MD_TABLE_ROW/CELL: in the header row
struct String text, info, id, title;
struct Vec<&heap MdNode> children;
static &heap MdNode parse(const &char text); // CommonMark
static &heap MdNode parse_opts(const &char text, const &MdOptions opts); // with extensions
struct String render_html() const; // text escaped; raw HTML only with the html option
struct String render_markdown() const; // round-trips: parse(render_markdown()) renders the same
void plain_text(&String out) const; // text content, no markup
void free() consume; // frees the subtree and the node itself
};
void md_slugify(const &char text, &String out); // "2. Temporal Safety" -> "_2-temporal-safety"
generic<C>
struct String md_render_html_with(const &MdNode node,
fn int(?&C, const &MdNode, int, &String) hook, ?&C ctx);
#include <std/serial/markdown.h>
#include <std/io.h>
int main() {
&heap MdNode doc = std::MdNode::parse("# Title\n\nSome **bold** text.\n");
struct String html = doc.render_html();
std::println(html.as_ptr()); // <h1>Title</h1>\n<p>Some <strong>bold</strong> text.</p>
html.free();
doc.free();
return 0;
}
md_slugify는 GitHub/VitePress 방식의 앵커를 만듭니다: 소문자로 바꾸고, 공백·ASCII 문장 부호·제어 문자(및 곡선 따옴표)의 연속을 - 하나로 합치며, 앞뒤의 -를 없애고, 숫자로 시작하면 앞에 _를 붙입니다. 비ASCII 문자는 그대로 둡니다.
훅으로 렌더링하기
md_render_html_with는 각 노드의 기본 HTML을 쓰기 전에 훅을 호출하므로, 나머지를 다시 구현하지 않고도 링크 대상을 바꾸거나, 제목에 앵커를 붙이거나, 코드에 구문 강조를 적용할 수 있습니다. 진입 시(entering = 1) 훅은 노드를 평소대로 렌더링하려면 0, 노드의 HTML 전체를 직접 썼으면 1, 여는 부분만 썼으면 2를 반환합니다. 2인 경우 자식은 평소대로 렌더링되고, 이어서 entering = 0으로 훅이 다시 호출되며, 닫는 부분도 직접 썼으면 1(기본 닫기를 쓰려면 0)을 반환합니다.
struct Site { const char* base; };
int site_hook(?&Site site, const &MdNode n, int entering, &String out) {
if (n.kind == MD_LINK && entering) { // prefix every absolute link
out.push("<a href=\"");
match (site) { case some(s): { unsafe { out.push(s.base); } } case null: { } }
html_escape(out, n.text.as_ptr());
out.push("\">");
return 2; // children + default </a>
}
if (n.kind == MD_HEADING && !entering) { // anchor before </hN>
out.push(" <a class=\"anchor\" href=\"#");
out.push(n.id.as_ptr());
out.push("\">#</a></h");
out.push_int((long long)n.level);
out.push(">\n");
return 1;
}
if (n.kind == MD_HEADING) {
out.push("<h"); out.push_int((long long)n.level);
out.push(" id=\""); out.push(n.id.as_ptr()); out.push("\">");
return 2;
}
return 0;
}
struct MdOptions o = MdOptions::gfm();
o.heading_ids = 1;
&heap MdNode doc = MdNode::parse_opts(text, &o);
struct Site site;
site.base = "/docs";
struct String html;
unsafe { html = md_render_html_with(doc, site_hook, (?&Site)&site); }
Protobuf
#include <std/serial/protobuf.h>
Protocol Buffers proto3 바이너리 와이어 포맷입니다 — 바린트, 태그, 지그재그, 길이 구분, fixed32/fixed64 — protoc이 생성한 코드와 정확히 같으므로 다른 언어의 protobuf와 상호 운용됩니다. .proto 컴파일러는 없습니다: 스키마의 필드 번호를 써서 to_proto()/from_proto()를 직접 작성합니다. RPC와 함께 씁니다.
#define PB_WIRE_VARINT 0 // int32/int64/uint32/uint64/sint32/sint64/bool/enum
#define PB_WIRE_FIXED64 1 // fixed64/sfixed64/double
#define PB_WIRE_LEN 2 // string/bytes/embedded messages/packed repeated
#define PB_WIRE_FIXED32 5 // fixed32/sfixed32/float
// Group wire types 3/4 are not supported (proto3 never emits them).
// Writer: each *_field call appends its own tag, then the value.
void pb_write_varint(&String out, unsigned long long v);
void pb_write_tag(&String out, int field_num, int wire_type);
void pb_write_int64_field(&String out, int field_num, long long v); // negative values take 10 bytes, like protoc
void pb_write_uint64_field(&String out, int field_num, unsigned long long v);
void pb_write_sint64_field(&String out, int field_num, long long v); // zigzag
void pb_write_bool_field(&String out, int field_num, int v);
void pb_write_double_field(&String out, int field_num, double v);
void pb_write_float_field(&String out, int field_num, float v);
void pb_write_string_field(&String out, int field_num, const &char s);
void pb_write_bytes_field(&String out, int field_num, []const unsigned char data); // also nested messages
// Reader: a zero-copy cursor over an existing buffer.
struct PbReader {
const &unsigned char data;
unsigned long len;
unsigned long pos;
static struct PbReader new([]const unsigned char data); // at data[0]
int has_more() const;
// Each returns 1 on success, 0 on truncated or malformed input.
int read_varint(&unsigned long long out);
int read_tag(&int field_num, &int wire_type);
int read_int64(&long long out);
int read_uint64(&unsigned long long out);
int read_sint64(&long long out);
int read_bool(&int out);
int read_double(&double out);
int read_float(&float out);
// A slice of the reader's own buffer (zero-copy) — copy it if it must outlive
// that buffer. null on a truncated encoding.
?[]const unsigned char read_length_delimited();
int skip_field(int wire_type); // skip an unknown field (forward compatibility)
};
쓰기 함수들은 자유 함수로 남아 있습니다: 어떤 String에든 덧붙이므로, 메서드를 둘 protobuf 전용 구조체가 없기 때문입니다.
read_length_delimited는 리더 자신의 버퍼를 가리키는 슬라이스를 반환합니다(인코딩이 잘렸으면 null) — 버퍼를 별칭으로 가리키므로, 버퍼보다 오래 살아야 하면 바이트를 복사합니다(예: String::from_bytes).
#include <std/serial/protobuf.h>
// .proto: message Point { int32 x = 1; int32 y = 2; }
struct Point { int x; int y; };
int main() {
struct String buf = std::String::new();
std::pb_write_int64_field(&buf, 1, 3LL);
std::pb_write_int64_field(&buf, 2, -4LL);
struct Point p;
p.x = 0; p.y = 0;
struct PbReader r;
r = std::PbReader::new(buf.as_bytes()); // []const char is a byte slice
while (r.has_more()) {
int field = 0; int wire = 0;
if (!r.read_tag(&field, &wire)) break;
long long v = 0LL;
if (wire == PB_WIRE_VARINT && r.read_int64(&v)) {
if (field == 1) p.x = (int)v;
if (field == 2) p.y = (int)v;
} else if (!r.skip_field(wire)) break;
}
buf.free();
return (p.x == 3 && p.y == -4) ? 0 : 1;
}
std/serial/serial.h는 value.h, json.h, xml.h, html.h만 포함합니다. csv.h, yaml.h, markdown.h, protobuf.h는 직접 include합니다.
왕복 변환 충실도
JSON은 구조적 모호함이 없습니다 — 임의의 문자열 내용을 정확히 왕복시키는 것이 중요하다면 항상 JSON을 우선합니다. XML과 HTML은 모든 Value 종류를 평문 태그 텍스트로 매핑하는 데서 비롯된 두 가지 고유한 모호함을 공유합니다.
- 빈 문자열 대 빈 컨테이너.
<t></t>(자식도 텍스트도 없음)는 빈 문자열일 수도, 빈 객체/배열일 수도 있습니다. 두 파서 모두 항상 이를VAL_STRING ""으로 해석합니다. - 숫자/불린처럼 보이는 텍스트. 숫자나
true/false처럼 보이는 텍스트 내용은, 실제로 그렇게 직렬화되었을 때와 마찬가지로VAL_INT/VAL_FLOAT/VAL_BOOL로 다시 디코딩됩니다 — 값이 우연히"42"인 문자열 필드는 XML/HTML을 거쳐 문자열"42"가 아니라 정수42로 왕복됩니다.
CSV는 타입 추론을 아예 하지 않음으로써 이 문제를 피합니다(모든 필드는 예외 없이 VAL_STRING으로 다시 파싱됩니다). YAML은 XML/HTML과 마찬가지로 null/불린/숫자처럼 보이는 스칼라를 해당 타입의 Value 종류로 추론합니다 — 같은 모호함이 형식 하나만큼 더 늘어난 셈입니다.
성능 참고 사항
각 형식의 문자열 이스케이프 함수(json_append_escaped_, xml_append_escaped_, html_escape)는 이스케이프가 필요 없는 바이트 구간을 바이트 하나당 push_char() 호출 하나가 아니라 단일 String::push_n() 호출로 일괄 처리합니다 — 실제 문자열 내용 대부분에는 특수 문자가 없으므로, 이렇게 하면 일반적인 경우 문자열당 경계 검사가 포함된 버퍼 증가 한 번과 memcpy 한 번만으로 처리되며, 바이트당 함수 호출이 발생하지 않습니다.