Парсим JSON и XML на ассемблере
Отобрал для вас несколько крайне интересных, но малоизвестных библиотек, реализующих работу с XML и JSON. Кроссплатформенных и без зависимостей. На чистом С и ассемблере.
Зачем
Вообще говоря форматы XML и JSON уже из новых времен победившей педерастии и скотоложества толерантности и забивания на инженерную культуру.
Реальные пацаны используют чистые сокеты и бинарные форматы. И считают контрольные суммы вручную.
Проекты, описанные в этой статье уже наверное не несут никакого практического смысла в современных реалиях — зачем вам сверхбыстрая обработка данных, если все остальное у вас в проекте является тормозящим говном?
Так что стоит воспринимать их лишь как образцы хорошего вкуса и остатков инженерной культуры.
Тестовый стенд
Понта ради чтобы подсветить возможные проблемы совместимости, в качестве стенда использовалась FreeBSD 14, а не более обыденный Linux.
В зависимости от типа проекта, сборка осуществлялась как с помощью обычного gcc так и с clang и/или nasm, jasm.
XML.C
Начнем наше (очередное) погружение в мир ультрахардкора высоких достижений с довольно простого проекта:
Simple XML subset parser comparable to glib's Markup parser, but without any dependencies in one self contained file.
Реализован этот парсер XML на чистом современном С, разумеется без зависимостей, сборка происходит с помощью cmake.
Как-то так выглядит тестовое приложение с использованием этого парсера:
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <xml.h>
int main(int argc, char** argv) {
/* XML source, could be read from disk
*/
uint8_t* source = ""
"<Root>"
"<Hello>World</Hello>"
"<This>"
"<Is>:-)</Is>"
"<An>:-O</An>"
"<Example>:-D</Example>"
"</This>"
"</Root>";
/* Parse the document
*/
struct xml_document* document = xml_parse_document(source,
strlen(source));
if (!document) {
printf("Could parse document\n");
exit(EXIT_FAILURE);
}
struct xml_node* root = xml_document_root(document);
/* Say Hello World :-)
*/
struct xml_node* root_hello = xml_node_child(root, 0);
struct xml_string* hello = xml_node_name(root_hello);
struct xml_string* world = xml_node_content(root_hello);
uint8_t* hello_0 = calloc(xml_string_length(hello) + 1, sizeof(uint8_t));
uint8_t* world_0 = calloc(xml_string_length(world) + 1, sizeof(uint8_t));
xml_string_copy(hello, hello_0, xml_string_length(hello));
xml_string_copy(world, world_0, xml_string_length(world));
printf("%s %s\n", hello_0, world_0);
free(hello_0);
free(world_0);
/* Extract amount of Root/This children
*/
struct xml_node* root_this = xml_node_child(root, 1);
printf("Root/This has %lu children\n",
(unsigned long)xml_node_children(root_this));
xml_document_free(document, false);
}Специфика FreeBSD
В исходном коде есть вот такой блок:
#ifdef XML_PARSER_VERBOSE #include <alloca.h> #endif
Заголовочный файл alloca.h это часть проекта ядра Linux и поставляется вместе с ним, т.е. это не какая-то дополнительная библиотека а часть заголовков ядра.
В FreeBSD его разумеется нет, поэтому для сборки я вставил этот простой макрос:
#if defined(__FreeBSD__) #include <stdlib.h> #else #include <alloca.h> #endif
JSON65
https://github.com/mignon-p/json65
Следущий проект — натуральный бальзам для души уставшего от современного технологического п#здеца программиста:
A JSON parser written in 6502 assembly language.
Относительно большой проект, реализующий парсер JSON на чистом ассемблере (и немного С) для.. 8-битных микропроцессоров серии 6502.
Понимаю, что скорее всего для молодого поколения разработчиков, название 6502 само по себе мало о чем говорит, поэтому ниже в качестве иллюстрации список систем, поддерживаемых компилятором cc65, с помощью которого собирается этот проект:
- the following Commodore machines:
- the Apple ][+ and successors.
- the Atari 8-bit machines.
- the Atari 2600 console.
- the Atari 5200 console.
- GEOS for the C64, C128 and Apple //e.
- the Bit Corporation Gamate console.
- the NEC PC-Engine (aka TurboGrafx-16) console.
- the Nintendo Entertainment System (NES) console.
- the Watara Supervision console.
- the VTech Creativision console.
- the Oric Atmos.
- the Oric Telestrat.
- the Lynx console.
- the Ohio Scientific Challenger 1P.
- the Commander X16.
- the Synertek Systems Sym-1.
Atari, Commodore, Apple II — думаю хотя-бы о части этих знаменитых компьютеров вы слышали:
Так вот данный проект реализует парсер JSON для всех этих замечательных машин из далекого прошлого.
Для сборки нужен уже упомянутый кросс-компилятор cc65, исходники которого находятся на Github.
Специфика FreeBSD
Компилятор сс65 присутствует в портах, поэтому его можно установить в систему без заморочек со сборкой из исходников:
pkg install cc65
В пакете cc65 идет также симулятор sim65, позволяющий запустить собранное приложение для 6502 процессора.
Так выглядит тестовый пример использования этой библиотеки:
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
#include <cc65.h>
#include "json65.h"
#include "json65-file.h"
#include "json65-tree.h"
#include "json65-print.h"
static uint8_t scratch[2048];
static j65_tree tree;
static void my_exit (int code) __attribute__ ((noreturn)) {
if (doesclrscrafterexit ()) {
cursor (true);
cgetc ();
}
exit (code);
}
int main (int argc, char **argv) {
const char *filename;
FILE *f;
int8_t ret;
j65_node *banana, *banana_value;
uint8_t width, height;
if (argc >= 2) {
filename = argv[1];
} else {
filename = "input.json";
}
/* Find out the width of the screen. (This will be used
** when formatting error messages.)
*/
screensize (&width, &height);
/* Initialize the tree data structure. */
j65_init_tree (&tree);
/* Open the input file. */
f = fopen (filename, "r");
if (!f) {
perror (filename);
my_exit (EXIT_FAILURE);
}
/* Call j65_parse_file() to parse the file, and it will in turn
** call j65_tree_callback() to build up the tree.
*/
ret = j65_parse_file (f, /* file to parse */
scratch, /* pointer to a scratch buffer */
sizeof (scratch), /* length of scratch buffer */
&tree, /* "context" for callback */
j65_tree_callback, /* the callback function */
0, /* 0 means use max nesting depth */
stderr, /* where to print errors */
width, /* width of screen (for errors) */
filename, /* used in error messages */
NULL); /* no custom error func */
if (ret < 0) {
/* Don't need to print any error message here, because
** j65_parse_file() already printed an error message before
** returning a negative number.
*/
fclose (f);
my_exit (EXIT_FAILURE);
}
/* We're done reading the file, so we can close it now. */
fclose (f);
/* Look for a key named "banana". */
banana = j65_find_key (&tree, tree.root, "banana");
if (banana == NULL) {
printf ("Could not find banana.\n");
j65_free_tree (&tree);
my_exit (EXIT_FAILURE);
}
/* The variable "banana" now points to the key "banana" in
** the tree. If we want to know the value associated with
** the key "banana", we need to look at the key's child node.
*/
banana_value = banana->child;
/* Now print the value associated with the key "banana". */
printf ("Here are some fun facts about bananas, from line %lu of %s:\n",
banana_value->location.line_number + 1, filename);
ret = j65_print_tree (banana_value, stdout);
if (ret < 0) {
perror ("error printing tree");
j65_free_tree (&tree);
my_exit (EXIT_FAILURE);
}
/* j65_print_tree() prints everything on one line (so, not
** particularly human readable) without a newline at the
** end, so we must print the newline ourselves.
*/
printf ("\n");
/* We are done, so we can free the tree now. */
j65_free_tree (&tree);
my_exit (EXIT_SUCCESS);
}Jsonx64
https://github.com/mmoeller86/jsonx64/
Следующий мозговыносящий проект:
A fast JSON-parser written in x86-64 assembly
Тестовое приложение из этого проекта (вместе с частью исходников) вы можете наблюдать на заглавном скриншоте к статье.
It supports 64-bit strings, 64-bit memory and 3 input sources: memory, files and FDs.
Для сборки необходимо использовать довольно редкий ассемблер JWasm.
Специфика FreeBSD
К великой радости, jwasm также присутствует в пакетах FreeBSD, поэтому можно обойтись без ручной сборки:
pkg install jwasm
Сам факт сборки проекта на чистом ассемблере под FreeBSD — железобетонный пруф переносимости в пределах одной архитектуры и отсутствия каких-либо зависимостей от ОС.
MSXJSON
https://github.com/ricbit/msxjson
Наверное самый отбитый проект из всей подборки:
MSX BASIC это такая версия языка БЕЙСИК из примерно 1983 года.
Парсер написан на ассемблере для знаменитого микропроцессора Z80 и собирается с помощью специального компилятора sjasmplus:
Command-line cross-compiler of assembly language for Z80 CPU.
Так выглядит запуск тестов обработки JSON.. в эмуляторе MSX (!):
Специфика FreeBSD
И sjasmplus и openmsx — эмулятор MSX присутствуют в пакетах FreeBSD в готовом виде:
asmjson
https://github.com/MKuranowski/asmjson
Еще одна реализация парсера JSON на чистом ассемблере, на этот раз под nasm:
JSON parser written in nasm for x86-64 Linux
Несмотря на описание, отлично собирается и работает на FreeBSD.
Один единственный файл с исходником на ассемблере и еще один заголовочный, с описанием функций — вот и весь проект.
Часть исходного кода и тестовое приложение в работе:
AsmXml
Следущий крышесносный проект — реализация парсера XML на чистом ассемблере:
AsmXml is a very fast XML parser and decoder for x86 platforms (Windows, Linux, BSD and Mac OS X).
Проект старый (разработка идет с 2008 года) и довольно известный.
Как-то так выглядит запуск тестового приложения с использованием этой библиотеки:
У проекта есть рекорды скорости обработки:
To give an idea of the relative speed of AsmXml, the fastest open source XML parsers process between 10 and 30 MBs of XML per seconds while AsmXml processes around 200 MBs per seconds (on an Athlon XP 1800+).
Если вы задаетесь вопросом зачем я это читаю использовать чистый ассемблер когда есть Java и .NET — то вот за этим:
Специфика FreeBSD
Как ни странно, но эта библиотека присутствует в готовом виде в пакетах FreeBSD:
Поэтому можно не заморачиваться сборкой и сразу использовать в своих проектах.
Вот так выглядит пример использования (на С), причем пример кроссплатформенный и собирается как на BSD/Linux так и в Windows:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "asm-xml.h"
#ifdef WIN32
#define SEP "\\"
#else
#define SEP "/"
#endif
static const int chunkSize = 16*1024*1024; // 16M
static const char schemaFilename[] = "data" SEP "employees-schema.xml";
static const char xmlFilename[] = "data" SEP "employees.xml";
char buffer[65536];
const char* asString(AXAttribute* attr)
{
const char* start = attr->begin;
const char* limit = attr->limit;
size_t size = limit - start;
memcpy(buffer, start, size);
buffer[size] = 0;
return buffer;
}
void printAsmXmlError(AXParseContext* context)
{
fprintf(stderr, "Error (%d,%d): %d\n", context->line,
context->column, context->errorCode);
}
AXElementClass* readClass(const char* filename, AXClassContext* classContext)
{
FILE* f;
size_t size;
f = fopen(filename, "rb");
if( f == NULL )
{
fprintf(stderr, "can't open schema '%s'\n", filename);
return NULL;
}
size = fread(buffer, 1, 65535, f);
buffer[size] = 0;
fclose(f);
// Parse the string and build the class
return ax_classFromString(buffer, classContext);
}
AXElement* readDocument(const char* filename,
AXParseContext* parseContext,
AXElementClass* clazz)
{
FILE* f;
size_t size;
f = fopen(filename, "rb");
if( f == NULL )
{
fprintf(stderr, "can't open file '%s'\n", filename);
return NULL;
}
size = fread(buffer, 1, 65535, f);
buffer[size] = 0;
fclose(f);
// Parse the string and build the class
return ax_parse(parseContext, buffer, clazz, 1);
}
int main(int argc, char *argv[])
{
int res;
AXClassContext classContext;
AXParseContext parseContext;
AXElementClass* employeeClass;
AXElement* employees;
AXElement* employee;
ax_initialize(malloc, free);
res = ax_initializeClassParser(&classContext);
// An error while initialization means that allocation failed.
// It should never happen since it allocates only 4K.
if( res != 0 )
return 1;
// Read the schema and compile it
//
employeeClass = readClass(schemaFilename, &classContext);
if( employeeClass == NULL )
return 1;
res = ax_initializeParser(&parseContext, chunkSize);
// An error while initialization means that initial allocation failed.
if( res != 0 )
return 1;
// Read the file and parse it
//
employees = readDocument(xmlFilename, &parseContext, employeeClass);
if( employees == NULL )
{
printAsmXmlError(&parseContext);
return 1;
}
// Enumerate child elements
employee = employees->firstChild;
while( employee )
{
printf("================================\n");
printf("Employee id.: %s\n", asString(&employee->attributes[0]));
printf("Manager id..: %s\n", asString(&employee->attributes[1]));
printf("First Name..: %s\n", asString(&employee->attributes[2]));
printf("Last Name...: %s\n", asString(&employee->attributes[3]));
printf("Email.......: %s\n", asString(&employee->attributes[4]));
printf("Position....: %s\n", asString(&employee->attributes[5]));
employee = employee->nextSibling;
}
// Release the document and its class
ax_releaseParser(&parseContext);
ax_releaseClassParser(&classContext);
return 0;
}Как раз этот пример в запущенном виде можно видеть на скриншоте выше.
json-parser
https://github.com/json-parser/json-parser
Не смотря на столь банальное название, перед нами действительно эпический проект:
Very low footprint DOM-style JSON parser written in portable C89 (sometimes referred to as ANSI C).
Понимаю, что для современного разработчика сложно будет понять масштаб и всю эпичность этого чуда, поэтом процитирую:
ANSI C also referred to as C89, was the first C Programming Language Standard defined by the American National Standards Institute (ANSI) in 1989. It is officially designated as ANSI X3.159-1989.
Фактически это означает, что сей замечательный парсер JSON можно собрать из говна и палок вообще любым существующим компилятором С и в самых экзотичных системах.
Так выглядит в работе тестовое приложение:
Еще от этого же автора есть проект генератора JSON — для сериализации в JSON:
https://github.com/json-parser/json-builder
..
json_value * arr = json_array_new(0);
json_array_push(arr, json_string_new("Hello world!"));
json_array_push(arr, json_integer_new(128));
char * buf = malloc(json_measure(arr));
json_serialize(buf, arr);
printf("%s\n", buf);
..Simple JSON Parser in C
https://github.com/whyisitworking/C-Simple-JSON-Parser
Еще один интересный проект, состоящий (как и положено) из одного файла исходника на С и еще одного — с заголовком:
Extremely lightweight, easy-to-use & blazing fast JSON parsing library written in pure C
Проект относительно новый и я не стал бы тратить на него время, если бы не одно «но»:
- Fully RFC-8259 compliant
А этот RFC описывает стандарт JSON и при этом является свежим и актуальным — от декабря 2017 года.
Так что если автор проекта не врет — перед нами полная реализация парсера JSON на чистом С и без зависимостей.
Зависимостей нет до такой степени, что в проекте нет даже скрипта сборки и тестовое приложение собирается ручками:
gcc -I. json.c example.c -o example
Вот как выглядит собранное тестовое приложение в действии:
Вне рамок
Ниже еще несколько проектов, выходящих за рамки статьи, но настолько отбитых интересных, что просто не мог их пропустить.
json.sh
https://github.com/rcrowley/json.sh
Pure-shell JSON parser
«Pure shell» означает что оно работает далеко не только в любимом bash, но и например в ksh.
. "lib/json.sh" json <"tests/mixed.json"
OJG
https://github.com/ohler55/ojg
Optimized JSON for Go is a high performance parser with a variety of additional JSON tools. OjG is optimized to processing huge data sets where data does not necessarily conform to a fixed structure.
Быстрый парсер и валидатор JSON для Golang:
Из интересного, есть поддержка выборки по выражениям JSONPath — некий аналог XPath для XML:
x, err := jp.ParseString("a[?(@.x > 1)].y")
ys := x.Get(obj)
// returns [4]Есть готовый пакет в официальном репозитории Golang и судя по истории коммитов — проект активно развивается.
simdjson
https://github.com/simdjson/simdjson
Напоследок расскажу про один большой и достаточно известный проект, написанный на C++ 17 и нашедший широкое применение в самых разнообразных движках СУБД, веб-серверах и фреймворках:
Parsing gigabytes of JSON per second : used by Facebook/Meta Velox, the Node.js runtime, ClickHouse, WatermelonDB, Apache Doris, Milvus, StarRocks
Да, теперь вы тоже знаете одну из причин высокой производительности ClickHouse:
У этой библиотеки есть широчайший набор биндингов для самых разных языков, а также несколько интересных портов на другие языки, например на Zig и Java.
И все это активно развивается.
#include <iostream>
#include "simdjson.h"
using namespace simdjson;
int main(void) {
ondemand::parser parser;
padded_string json = padded_string::load("twitter.json");
ondemand::document tweets = parser.iterate(json);
std::cout << uint64_t(tweets["search_metadata"]["count"])
<< " results." << std::endl;
}Разумеется эта библиотека есть в пакетах FreeBSD: