Десериализовать файл с помощью serde_json во время компиляции

В начале своей программы я читаю данные из файла:

let file = std::fs::File::open("data/games.json").unwrap();
let data: Games = serde_json::from_reader(file).unwrap();

Я хотел бы знать, как это можно сделать во время компиляции по следующим причинам:

  1. Производительность: нет необходимости десериализовать во время выполнения
  2. Переносимость: программу можно запускать на любой машине без необходимости иметь файл json, содержащий данные.

Мне также может быть полезно упомянуть, что данные могут быть доступны только для чтения, что означает, что решение может хранить их как статические.


person Nils André    schedule 12.10.2019    source источник
comment
1 - Я не думаю, что производительность десериализации будет проблемой (для огромного набора данных можно использовать более быстрый метод сериализации, такой как bincode) 2 - Для переносимости используйте _ 1_, тогда вы можете десериализовать необходимые байты (избегайте использования json, это приведет к увеличению двоичного размера зря)   -  person Asya Corbeau    schedule 13.10.2019
comment
@AsyaCorbeau есть ли причина использовать include_bytes! вместо _ 2_?   -  person Nils André    schedule 13.10.2019
comment
@ NilsAndré none, поскольку JSON подразумевает допустимый utf8, который является единственным требованием, отделяющим массив байтов от строки в ржавчине.   -  person Sébastien Renauld    schedule 13.10.2019
comment
Верно, но, как я уже сказал, при двоичном кодировании размер исполняемого файла будет уменьшен (без каких-либо накладных расходов на сжатие), JSON бесполезен, когда он не читается людьми / нет ограничений на кодирование и есть накладные расходы: десериализация JSON требует гораздо больше работы, чем оптимизированное двоичное кодирование, при необходимости это может избежать всех ограничений JSON за счет совместимости (которую вы можете восстановить, используя JSON в качестве ресурсов и тему перекодирования во время компиляции)   -  person Asya Corbeau    schedule 15.10.2019


Ответы (2)


Это просто, но приводит к некоторым потенциальным проблемам. Во-первых, нам нужно кое-что разобраться: хотим ли мы загрузить дерево объектов из файла или проанализировать его во время выполнения?

В 99% случаев людям достаточно синтаксического анализа static ref при загрузке, поэтому я дам вам это решение; В конце я укажу вам на «другую» версию, но она требует много дополнительной работы и зависит от предметной области.

Макрос (потому что это должен быть макрос), который вы ищете для включения файла во время компиляции, находится в стандартной библиотеке: _ 2_. Как следует из названия, он берет ваш файл во время компиляции и генерирует из него &'static str для использования. После этого вы можете делать с ним все, что захотите (например, анализировать).

Оттуда легко использовать lazy_static! для создания static ref для наш JSON Value (или что бы вы там ни выбрали) для каждой части программы. В вашем случае, например, это могло бы выглядеть так:

const GAME_JSON: &str = include_str!("my/file.json");

#[derive(Serialize, Deserialize, Debug)]
struct Game {
    name: String,
}

lazy_static! {
    static ref GAMES: Vec<Game> = serde_json::from_str(&GAME_JSON).unwrap();
}

При этом нужно помнить о двух вещах:

  1. Это значительно приведет к увеличению размера вашего файла, поскольку &str никоим образом не сжимается. Рассмотрим gzip
  2. Вам нужно будет беспокоиться об обычных проблемах, связанных с многопоточным доступом к одному и тому же static ref, но, поскольку он не изменяемый, вам действительно нужно беспокоиться только о его части.

Другой способ требует динамической генерации ваших объектов во время компиляции с использованием процедурного макроса. Как уже говорилось, я бы не рекомендовал его, если у вас действительно нет действительно высоких начальных затрат при синтаксическом анализе этого JSON; большинство людей этого не сделает, и в последний раз у меня это было, когда я имел дело с глубоко вложенными файлами JSON размером в несколько ГБ.

Крейты, на которые вы хотите обратить внимание, - это proc_macro2 и syn для генерации кода; остальное очень похоже на то, как вы бы написали обычный метод.

person Sébastien Renauld    schedule 12.10.2019
comment
Могу ли я использовать serde для генерации объектов во время компиляции или мне придется использовать свой собственный синтаксический анализатор? - person Nils André; 13.10.2019
comment
@ NilsAndré Выбор за вами. В последний раз, когда мне приходилось делать это, у меня было то преимущество, что я смог преобразовать структуру во что-то, что занимало меньше места и убивало промежуточный этап синтаксического анализа, и я использовал протокольные буферы (в сочетании с serde) вместе с этапом предварительной обработки в качестве макроса proc. Я бы не рекомендовал этот подход, за исключением случаев крайней необходимости. - person Sébastien Renauld; 13.10.2019
comment
@ NilsAndré, чтобы повторить: то, что вы получаете, если не выполняете шаг синтаксического анализа JSON при загрузке вашего приложения, вы теряете в значительной степени, имея определенный вами шаг синтаксического анализа, потому что данные будут анализироваться независимо от того, что, чем-то, чтобы вы могли его использовать. Приросты небольшие, а головная боль сильная. - person Sébastien Renauld; 13.10.2019
comment
Сценарий сборки будет выполнять те же функции, что и ваш процедурный макрос, и будет проще. - person Shepmaster; 18.11.2019

Когда вы десериализуете что-то во время выполнения, вы, по сути, строите некоторое представление в памяти программы из другого представления на диске. Но во время компиляции еще нет понятия «программная память» - где эти данные тоже будут десериализоваться?

Однако то, чего вы пытаетесь достичь, на самом деле возможно. Основная идея такая: чтобы создать что-то в памяти программы, вы должны написать код, который будет создавать данные. Что, если вы можете автоматически сгенерировать код на основе сериализованных данных? Это то, что делает uneval crate (отказ от ответственности: я автор, поэтому вам рекомендуется просмотреть источник, чтобы узнать, сможете ли вы сделать лучше).

Чтобы использовать этот подход, вам нужно создать build.rs примерно со следующим содержанием:

// somehow include the Games struct with its Serialize and Deserialize implementations
fn main() {
    let games: Games = serde_json::from_str(include_str!("data/games.json")).unwrap();
    uneval::to_out_dir(games, "games.rs");
}

И в вашем коде инициализации у вас будет следующее:

let data: Games = include!(concat!(env!("OUT_DIR"), "/games.rs"));

Обратите внимание, однако, что это может быть довольно сложно сделать с точки зрения эргономики, поскольку необходимые определения структур теперь должны совместно использоваться build.rs и самим ящиком, как я упоминал в комментарии. Было бы немного проще, если бы вы разделили свой ящик на две части, сохранив определения структур (и только их) в одном ящике, а логику, которая их использует, - в другом. Есть и другие способы - с помощью include! обмана или использования того факта, что сценарий сборки является обычным двоичным файлом Rust и может включать в себя также другие модули, - но это еще больше усложнит ситуацию.

person Cerberus    schedule 01.05.2020