В предыдущей статье мы говорили о том, как избежать переписывания библиотеки на Rust, когда вам это не нужно. Но как насчёт тех случаев, когда это действительно необходимо?
В большинстве языков вам придётся переписывать всю библиотеку с нуля, а первые результаты появятся ближе к концу проекта. Такие порты, как правило, довольно дороги и подвержены ошибкам, а часто они выходят из строя на полпути. Джоэл Сполски гораздо лучше меня объясняет это в статье о том, почему полные переделки проектов — плохая идея.
Однако у Rust есть убийственная особенность, когда речь заходит о таких вещах. Он может вызывать код на языке С без накладных расходов (т. е. среда P/Invoke в C#), и он выставляет функции, которые можно использовать в C так же, как и любые другие функции на языке С. Это открывает дверь для альтернативного подхода:
Портировать библиотеки на Rust по одной функции.
Прежде чем что-то сделать, нужно создать новый проект. У меня есть шаблон, который устанавливает CI и лицензии для cargo-generate.
Наша первая реальная задача — собрать библиотеку, которую мы хотим портировать, и немного в ней разобраться.
В данном случае мы портируем jakogut/tinyvm,
Чтобы проще ссылаться на неё в будущем, добавим репозиторий в качестве подмодуля в наш проект.
Теперь посмотрим на исходный код. Для начала,
Ладно, давайте заглянем в каталог
Мне очень нравится, когда библиотеки C компилируются прямо из коробки без необходимости устанавливать случайные пакеты
К сожалению, библиотека не содержит никаких тестов, поэтому мы не сможем (сразу) убедиться, что отдельные функции переведены правильно, но она содержит пример интерпретатора, который мы можем использовать для изучения функциональности высокого уровня.
Таким образом, мы знаем, что можем без особых хлопот собрать её из командной строки. Теперь нужно убедиться, что наш крейт
Вот тут-то и появляются сценарии сборки. Наша стратегия заключается в том, чтобы крейт Rust использовал скрипт сборки
Нужно будет добавить крейт
А также убедиться, что
Нам также нужен способ сообщить Rust, какие функции он может вызвать из
Сгенерируем привязки из
Нужно будет добавить в наш крейт модуль
Глядя на каталог
Это невероятно просто. Что очень приятно, учитывая, что мы будем использовать этот интерпретатор в качестве одного из наших примеров.
А пока давайте переведём его непосредственно в Rust и вставим в каталог
В качестве проверки также можем запустить виртуальную машину и убедиться, что всё это работает.
Класс!
Когда вы начинаете с чего-то вроде этого, возникает соблазн погрузиться в самые важные функции и перенести их первыми. Попытайтесь сопротивляться этому порыву. Вы легко откусите больше, чем можете прожевать, и в конечном итоге либо потеряете время, либо это вас деморализует и заставит сдаться.
Вместо этого давайте поищем самое простое.
Этот файл
Посмотрим на заголовочный файл
Выглядит достаточно легко для реализации. Единственная проблема заключается в том, что определение
Мы можем проверить, имеет ли что-нибудь доступ к внутренним элементам хэш-таблицы, временно переместив определения структуры в
А затем ещё раз запускаем
Похоже, всё по-прежнему работает, теперь приступаем ко второй фазе; создаём идентичный набор функций, которые под капотом используют
Ограничившись заглушкой с самым минимумом, получаем:
Также нужно объявить модуль
Теперь нужно убедиться, что оригинальный
Исправление на самом деле довольно простое.
И попытка запустить пример
При добавлении поддержки FFI для нового типа проще всего начать с конструктора и деструктора.
Добавление элементов в хэш-карту почти так же просто реализовать.
Две функции
Единственное, где нужно быть осторожным, — это убедиться, что правильное значение возвращается, когда элемент не может быть найден. В данном случае, посмотрев на
Теперь, когда мы фактически реализовали функции заглушки, всё должно снова работать.
Самый простой способ это проверить — запустить наш пример.
И чтобы перепроверить, мы можем запустить его через
Успех!
Виртуальная машина
Такого рода манипуляции с текстом намного проще выполнить с помощью типов
Использование
Код C просто использует выходные параметры для замены строки
Прежде чем сделать что-то ещё, нужно написать тест для
Мы взаимодействуем с файловой системой, поэтому нужно будет вытащить крейт tempfile.
Нам также понадобится крейт
Глядя на исходный код, мы видим, что функция
Сначала давайте создадим тест, чтобы убедиться, что препроцессор обрабатывает
45 строк это намного больше, чем мне обычно нравится в тестах, но для преобразования туда и обратно между строками C требуется изрядное количество дополнительного кода.
Нам также нужно проверить, включая ещё один файл.
Итак, теперь у нас несколько тестов, так что можем начать реализовывать
Во-первых, нужно определить тип ошибки.
Глядя на функции process_includes() и process_derives(), кажется, что они сканируют строку в поисках определённой директивы, а затем заменяют эту строку чем-то другим (либо содержимым файла, либо ничем, если строка должна быть удалена).
Мы должны быть в состоянии извлечь эту логику в хелпер и избежать ненужного дублирования.
Теперь у нас есть хелпер
К сожалению, парсер %define немного сложнее.
Чтобы получить доступ к тексту в нашей хэш-таблице, нужно будет дать элементу
На этом этапе неплохо добавить ещё несколько тестов.
На этом этапе мы воспроизвели большую часть логики предварительной обработки, так что теперь просто нужна функция, которая будет продолжать расширять операторы
Конечно, эта функция
Теперь функция
Ой, линкер жалуется, что и
Попробуем ещё раз.
Гораздо лучше!
Помните тот прошлый пример, где
Если вы все еще читаете статью, поздравляю, мы только что портировали два модуля с
К сожалению, эта статья уже довольно длинная. Но надеюсь, что к настоящему моменту вы уловили общую картину.
Самое лучшее в этом методе заключается в том, что вы постепенно улучшаете кодовую базу, приложение сохраняет работоспособность, а вы не переписываете всё от начала до конца.
Это всё равно что менять колесо на ходу.
Предпочтительный способ переноса приложения с C на Rust
В большинстве языков вам придётся переписывать всю библиотеку с нуля, а первые результаты появятся ближе к концу проекта. Такие порты, как правило, довольно дороги и подвержены ошибкам, а часто они выходят из строя на полпути. Джоэл Сполски гораздо лучше меня объясняет это в статье о том, почему полные переделки проектов — плохая идея.
Однако у Rust есть убийственная особенность, когда речь заходит о таких вещах. Он может вызывать код на языке С без накладных расходов (т. е. среда P/Invoke в C#), и он выставляет функции, которые можно использовать в C так же, как и любые другие функции на языке С. Это открывает дверь для альтернативного подхода:
Портировать библиотеки на Rust по одной функции.
Примечание
Код из этой статьи доступен на GitHub. Не стесняйтесь заходить, чтобы заимствовать код или вдохновение.
Если вы нашли статью полезной или заметили ошибку, дайте знать в баг-трекере блога!
Приступая к работе
Прежде чем что-то сделать, нужно создать новый проект. У меня есть шаблон, который устанавливает CI и лицензии для cargo-generate.
$ cargo generate --git https://github.com/Michael-F-Bryan/github-template --name tinyvm-rs
$ cd tinyvm-rs && tree
tree -I 'vendor|target'
.
├── Cargo.toml
├── LICENSE_APACHE.md
├── LICENSE_MIT.md
├── README.md
├── .travis.yml
└── src
└── lib.rs
1 directory, 6 files
Наша первая реальная задача — собрать библиотеку, которую мы хотим портировать, и немного в ней разобраться.
В данном случае мы портируем jakogut/tinyvm,
TinyVM — это небольшая, быстрая, лёгкая виртуальная машина, написанная на чистом языке ANSI C.
Чтобы проще ссылаться на неё в будущем, добавим репозиторий в качестве подмодуля в наш проект.
$ git submodule add https://github.com/jakogut/tinyvm vendor/tinyvm
Теперь посмотрим на исходный код. Для начала,
README.md
инструкций по сборке.TinyVM — это виртуальная машина минимального размера. Малое использование памяти, небольшое количество кода и небольшой двоичный код.(выделение добавлено)
Сборка выполняется на UNIX-подобных системах с make и GCC.
Внешних зависимостей нет, сохраните стандартную библиотеку С.
Сборка выполняется с помощью make или make rebuild.
Чтобы собрать отладочную версию, добавьте DEBUG=yes после make. Чтобы собрать двоичный файл с включённым профилированием, добавьте PROFILE=yes после make.
Со мной можно связаться по адресу joseph.kogut(at)gmail.com
Ладно, давайте заглянем в каталог
tinyvm
и посмотрим, будет ли сборка просто работать.$ cd vendor/tinyvm
$ make
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_program.c -o libtvm/tvm_program.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_lexer.c -o libtvm/tvm_lexer.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm.c -o libtvm/tvm.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_htab.c -o libtvm/tvm_htab.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_memory.c -o libtvm/tvm_memory.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_preprocessor.c -o libtvm/tvm_preprocessor.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_parser.c -o libtvm/tvm_parser.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_file.c -o libtvm/tvm_file.o
ar rcs lib/libtvm.a libtvm/tvm_program.o libtvm/tvm_lexer.o libtvm/tvm.o libtvm/tvm_htab.o libtvm/tvm_memory.o libtvm/tvm_preprocessor.o libtvm/tvm_parser.o libtvm/tvm_file.o
clang src/tvmi.c -ltvm -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -Llib/ -o bin/tvmi
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c tdb/main.c -o tdb/main.o
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c tdb/tdb.c -o tdb/tdb.o
clang tdb/main.o tdb/tdb.o -ltvm -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -Llib/ -o bin/tdb
Мне очень нравится, когда библиотеки C компилируются прямо из коробки без необходимости устанавливать случайные пакеты
*-dev
или возиться с системой сборки.К сожалению, библиотека не содержит никаких тестов, поэтому мы не сможем (сразу) убедиться, что отдельные функции переведены правильно, но она содержит пример интерпретатора, который мы можем использовать для изучения функциональности высокого уровня.
Таким образом, мы знаем, что можем без особых хлопот собрать её из командной строки. Теперь нужно убедиться, что наш крейт
tinyvm
способен собрать всё программно.Вот тут-то и появляются сценарии сборки. Наша стратегия заключается в том, чтобы крейт Rust использовал скрипт сборки
build.rs
и крейт cc
для вызова эквивалентных команд к нашему вызову make
. Оттуда мы можем подключиться к libtvm
из Rust точно так же, как и к любой другой родной библиотеке.Нужно будет добавить крейт
cc
в качестве зависимости.$ cargo add --build cc
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding cc v1.0.47 to build-dependencies
А также убедиться, что
build.rs
компилирует исходный код libtvm
.// build.rs
use cc::Build;
use std::path::Path;
fn main() {
let tinyvm = Path::new("vendor/tinyvm");
let include = tinyvm.join("include");
let src = tinyvm.join("libtvm");
Build::new()
.warnings(false)
.file(src.join("tvm_file.c"))
.file(src.join("tvm_htab.c"))
.file(src.join("tvm_lexer.c"))
.file(src.join("tvm_memory.c"))
.file(src.join("tvm_parser.c"))
.file(src.join("tvm_preprocessor.c"))
.file(src.join("tvm_program.c"))
.file(src.join("tvm.c"))
.include(&include)
.compile("tvm");
}
Примечание
Если вы просмотрели документацию крейтаcc
, то, возможно, заметили методBuild::files()
, который принимает итератор путей. Мы могли бы программно обнаружить все файлы*.c
внутриvendor/tinyvm/libtvm
, но поскольку мы портируем код по одной функции, гораздо проще удалить отдельные вызовы.file()
по мере портирования.
Нам также нужен способ сообщить Rust, какие функции он может вызвать из
libtvm
. Обычно это делается путём записи определений для каждой функции в блоке extern, но, к счастью, существует инструмент под названием bindgen, который может читать заголовочный файл в стиле C и генерировать определения для нас.Сгенерируем привязки из
vendor/tinyvm/include/tvm/tvm.h
.$ cargo install bindgen
$ bindgen vendor/tinyvm/include/tvm/tvm.h -o src/ffi.rs
$ wc --lines src/ffi.rs
992 src/ffi.rs
Нужно будет добавить в наш крейт модуль
ffi
.// src/lib.rs
#[allow(non_camel_case_types, non_snake_case)]
pub mod ffi;
Глядя на каталог
src/
в tinyvm
, мы находим исходный код интерпретатора tinyvm
.// vendor/tinyvm/src/tvmi.c
#include <stdlib.h>
#include <stdio.h>
#include <tvm/tvm.h>
int main(int argc, char **argv)
{
struct tvm_ctx *vm = tvm_vm_create();
if (vm != NULL && tvm_vm_interpret(vm, argv[1]) == 0)
tvm_vm_run(vm);
tvm_vm_destroy(vm);
return 0;
}
Это невероятно просто. Что очень приятно, учитывая, что мы будем использовать этот интерпретатор в качестве одного из наших примеров.
А пока давайте переведём его непосредственно в Rust и вставим в каталог
examples/
.// examples/tvmi.rs
use std::{env, ffi::CString};
use tinyvm::ffi;
fn main() {
let filename = CString::new(env::args().nth(1).unwrap()).unwrap();
// cast away the `const` because that's what libtvm expects
let filename = filename.as_ptr() as *mut _;
unsafe {
let vm = ffi::tvm_vm_create();
if !vm.is_null() && ffi::tvm_vm_interpret(vm, filename) == 0 {
ffi::tvm_vm_run(vm);
}
ffi::tvm_vm_destroy(vm);
}
}
В качестве проверки также можем запустить виртуальную машину и убедиться, что всё это работает.
$ cargo run --example tvmi -- vendor/tinyvm/programs/tinyvm/fact.vm
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm`
1
2
6
24
120
720
5040
40320
362880
3628800
Класс!
Низко висящие фрукты
Когда вы начинаете с чего-то вроде этого, возникает соблазн погрузиться в самые важные функции и перенести их первыми. Попытайтесь сопротивляться этому порыву. Вы легко откусите больше, чем можете прожевать, и в конечном итоге либо потеряете время, либо это вас деморализует и заставит сдаться.
Вместо этого давайте поищем самое простое.
$ ls libtvm
tvm.c tvm_file.c tvm_htab.c tvm_lexer.c tvm_memory.c tvm_parser.c
tvm_preprocessor.c tvm_program.c
Этот файл
tvm_htab.с
выглядит многообещающе. Я почти уверен, что htab
расшифровывается как «хэш-таблица», а стандартная библиотека Rust уже содержит высококачественную реализацию. Мы должны быть в состоянии поменять это достаточно легко.Посмотрим на заголовочный файл
tvm_htab.h
и проверим, с чем имеем дело.// vendor/tinyvm/include/tvm/tvm_htab.h
#ifndef TVM_HTAB_H_
#define TVM_HTAB_H_
#define KEY_LENGTH 64
#define HTAB_SIZE 4096
struct tvm_htab_node {
char *key;
int value;
void *valptr;
struct tvm_htab_node *next;
};
struct tvm_htab_ctx {
unsigned int num_nodes;
unsigned int size;
struct tvm_htab_node **nodes;
};
struct tvm_htab_ctx *tvm_htab_create();
void tvm_htab_destroy(struct tvm_htab_ctx *htab);
int tvm_htab_add(struct tvm_htab_ctx *htab, const char *key, int value);
int tvm_htab_add_ref(struct tvm_htab_ctx *htab,
const char *key, const void *valptr, int len);
int tvm_htab_find(struct tvm_htab_ctx *htab, const char *key);
char *tvm_htab_find_ref(struct tvm_htab_ctx *htab, const char *key);
#endif
Выглядит достаточно легко для реализации. Единственная проблема заключается в том, что определение
tvm_htab_ctx
и tvm_htab_node
включены в заголовочный файл, а это означает, что какой-то код может обращаться непосредственно к внутренним элементам хэш-таблицы, а не проходить через опубликованный интерфейс.Мы можем проверить, имеет ли что-нибудь доступ к внутренним элементам хэш-таблицы, временно переместив определения структуры в
tvm_htab.c
и посмотреть, всё ли ещё компилируется.diff --git a/include/tvm/tvm_htab.h b/include/tvm/tvm_htab.h
index 9feb7a9..e7346b7 100644
--- a/include/tvm/tvm_htab.h
+++ b/include/tvm/tvm_htab.h
@@ -4,18 +4,8 @@
#define KEY_LENGTH 64
#define HTAB_SIZE 4096
-struct tvm_htab_node {
- char *key;
- int value;
- void *valptr;
- struct tvm_htab_node *next;
-};
-
-struct tvm_htab_ctx {
- unsigned int num_nodes;
- unsigned int size;
- struct tvm_htab_node **nodes;
-};
+struct tvm_htab_node;
+struct tvm_htab_ctx;
struct tvm_htab_ctx *tvm_htab_create();
void tvm_htab_destroy(struct tvm_htab_ctx *htab);
А затем ещё раз запускаем
make
:$ make
make
clang -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -c libtvm/tvm_htab.c -o libtvm/tvm_htab.o
ar rcs lib/libtvm.a libtvm/tvm_program.o libtvm/tvm_lexer.o libtvm/tvm.o libtvm/tvm_htab.o libtvm/tvm_memory.o libtvm/tvm_preprocessor.o libtvm/tvm_parser.o libtvm/tvm_file.o
clang src/tvmi.c -ltvm -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -Llib/ -o bin/tvmi
clang tdb/main.o tdb/tdb.o -ltvm -Wall -pipe -Iinclude/ -std=gnu11 -Werror -pedantic -pedantic-errors -O3 -Llib/ -o bin/tdb
Похоже, всё по-прежнему работает, теперь приступаем ко второй фазе; создаём идентичный набор функций, которые под капотом используют
HashMap<K, V>
.Ограничившись заглушкой с самым минимумом, получаем:
// src/htab.rs
use std::{
collections::HashMap,
ffi::CString,
os::raw::{c_char, c_int, c_void},
};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct HashTable(pub(crate) HashMap<CString, Item>);
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Item {
// not sure what to put here yet
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_create() -> *mut HashTable {
unimplemented!()
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_destroy(htab: *mut HashTable) {
unimplemented!()
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_add(
htab: *mut HashTable,
key: *const c_char,
value: c_int,
) -> c_int {
unimplemented!()
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_add_ref(
htab: *mut HashTable,
key: *const c_char,
value_ptr: *mut c_void,
length: c_int,
) -> c_int {
unimplemented!()
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_find(
htab: *mut HashTable,
key: *const c_char,
) -> c_int {
unimplemented!()
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_find_ref(
htab: *mut HashTable,
key: *const c_char,
) -> *mut c_char {
unimplemented!()
}
Также нужно объявить модуль
htab
и реэкспортировать его функции из lib.rs
.// src/lib.rs
mod htab;
pub use htab::*;
Теперь нужно убедиться, что оригинальный
tvm_htab.c
не компилируется и не связывается с окончательной библиотекой, иначе компоновщик встретит нас стеной ошибок повторяющихся символов.
Стена ошибок повторяющихся символов
error: linking with `/usr/bin/clang` failed: exit code: 1
|
= note: "/usr/bin/clang" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-L" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.17q5thi94e1eoj5i.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.19e8sqirbm56nu8g.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.1g6ljku8dwzpfvhi.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.1h5e5mxmiptpb7iz.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.1herotdop66zv9ot.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.1qbfxpvgd885u6o.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.21psdg8ni4vgdrzk.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.2albhpxlxxvc0ccu.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.2btm2dc9rhjhhna1.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.2kct5ftnkrqqr0mf.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.2lwgg3uosup4mkh0.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.2xduj46e9sw5vuan.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.35h8y7f23ua1qnz0.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.3cgfdtku63ltd8oc.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.3ot768hzkzzy7r76.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.3u2xnetcch8f2o02.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.4ldrdjvfzk58myrv.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.4omnum6bdjqsrq8b.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.4s8ch4ccmewulj22.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.4syl3x2rb8328h8x.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.532awiysf0h9r50f.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.dfjs079cp9si4o5.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.qxp6yb2gjpj0v6n.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.xz7ld20yvprst1r.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.z35ukhvchmmby1c.rcgu.o" "-o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.1d7wvlwdjap8p3g4.rcgu.o" "-Wl,--gc-sections" "-pie" "-Wl,-zrelro" "-Wl,-znow" "-nodefaultlibs" "-L" "/home/michael/Documents/tinyvm-rs/target/debug/deps" "-L" "/home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out" "-L" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,-Bstatic" "-Wl,--whole-archive" "-ltvm" "-Wl,--no-whole-archive" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libtest-a39a3e9a77b17f55.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libterm-97a69cd310ff0925.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libgetopts-66a42b1d94e3e6f9.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunicode_width-dd7761d848144e0d.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_std-f722acdb78755ba0.rlib" "-Wl,--start-group" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-974c3c08f6def4b3.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-eb49676f33a2c8a6.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libhashbrown-7ae0446feecc60f2.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_alloc-2de299b65d7f5721.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace-64514775bc06309a.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace_sys-1ed8aa185c63b9a5.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_demangle-a839df87f563fba5.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunwind-8e726bdc2018d836.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcfg_if-5285f42cbadf207d.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liblibc-b0362d20f8aa58fa.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liballoc-f3dd7051708453a4.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_core-83744846c43307ce.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-d5565a3a0f4cfe21.rlib" "-Wl,--end-group" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-ea790e85415e3bbf.rlib" "-Wl,-Bdynamic" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil" "-fuse-ld=lld"
= note: ld.lld: error: duplicate symbol: tvm_htab_create
>>> defined at htab.rs:14 (src/htab.rs:14)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_create)
>>> defined at tvm_htab.c:23 (vendor/tinyvm/libtvm/tvm_htab.c:23)
>>> tvm_htab.o:(.text.tvm_htab_create+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
ld.lld: error: duplicate symbol: tvm_htab_destroy
>>> defined at htab.rs:17 (src/htab.rs:17)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_destroy)
>>> defined at tvm_htab.c:35 (vendor/tinyvm/libtvm/tvm_htab.c:35)
>>> tvm_htab.o:(.text.tvm_htab_destroy+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
ld.lld: error: duplicate symbol: tvm_htab_add_ref
>>> defined at htab.rs:29 (src/htab.rs:29)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_add_ref)
>>> defined at tvm_htab.c:160 (vendor/tinyvm/libtvm/tvm_htab.c:160)
>>> tvm_htab.o:(.text.tvm_htab_add_ref+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
ld.lld: error: duplicate symbol: tvm_htab_add
>>> defined at htab.rs:20 (src/htab.rs:20)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_add)
>>> defined at tvm_htab.c:147 (vendor/tinyvm/libtvm/tvm_htab.c:147)
>>> tvm_htab.o:(.text.tvm_htab_add+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
ld.lld: error: duplicate symbol: tvm_htab_find
>>> defined at htab.rs:39 (src/htab.rs:39)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_find)
>>> defined at tvm_htab.c:189 (vendor/tinyvm/libtvm/tvm_htab.c:189)
>>> tvm_htab.o:(.text.tvm_htab_find+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
ld.lld: error: duplicate symbol: tvm_htab_find_ref
>>> defined at htab.rs:47 (src/htab.rs:47)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-599d57f523fdb1a4.5b2qwmmtc5pvnbh.rcgu.o:(tvm_htab_find_ref)
>>> defined at tvm_htab.c:199 (vendor/tinyvm/libtvm/tvm_htab.c:199)
>>> tvm_htab.o:(.text.tvm_htab_find_ref+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: aborting due to previous error
error: could not compile `tinyvm`.
Исправление на самом деле довольно простое.
diff --git a/build.rs b/build.rs
index 6f274c8..af9d467 100644
--- a/build.rs
+++ b/build.rs
@@ -9,7 +9,6 @@ fn main() {
Build::new()
.warnings(false)
.file(src.join("tvm_file.c"))
- .file(src.join("tvm_htab.c"))
.file(src.join("tvm_lexer.c"))
.file(src.join("tvm_memory.c"))
.file(src.join("tvm_parser.c"))
И попытка запустить пример
tvmi
снова терпит крах, как и следовало ожидать от программы, полной unimplemented!()
.$ cargo run --example tvmi -- vendor/tinyvm/programs/tinyvm/fact.vm
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm`
thread 'main' panicked at 'not yet implemented', src/htab.rs:14:57
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
При добавлении поддержки FFI для нового типа проще всего начать с конструктора и деструктора.
Информация
Код C может получить доступ к нашей хэш-таблице только через указатель, поэтому нужно выделить один из них в куче, а затем передать право собственности на этот выделенный кучей объект вызывающему объекту.
// src/htab.rs
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_create() -> *mut HashTable {
let hashtable = Box::new(HashTable::default());
Box::into_raw(hashtable)
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_destroy(htab: *mut HashTable) {
if htab.is_null() {
// nothing to free
return;
}
let hashtable = Box::from_raw(htab);
// explicitly destroy the hashtable
drop(hashtable);
}
Предупреждение
Важно, чтобы вызывающие абоненты уничтожалиHashTable
только с помощью функцииtvm_htab_destroy ()
!
Если они не сделают этого и вместо этого попытаются вызватьfree()
напрямую, у нас почти наверняка возникнет плохая ситуация. В лучшем случае это приведет к большой утечке памяти, но также вполне возможно, что нашBox
в Rust не использует ту же кучу, чтоmalloc()
иfree ()
, а это означает, что освобождение объекта Rust от C может повредить кучу и оставить всё в сломанном состоянии.
Добавление элементов в хэш-карту почти так же просто реализовать.
// src/hmap.rs
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Item {
/// An integer value.
value: c_int,
/// An opaque value used with [`tvm_htab_add_ref()`].
///
/// # Safety
///
/// Storing the contents of a `void *` in a `Vec<u8>` *would* normally
/// result in alignment issues, but we've got access to the `libtvm` source
/// code and know it will only ever store `char *` strings.
opaque_value: Vec<u8>,
}
impl Item {
pub(crate) fn integer(value: c_int) -> Item {
Item {
value,
opaque_value: Vec::new(),
}
}
pub(crate) fn opaque<V>(opaque_value: V) -> Item
where
V: Into<Vec<u8>>,
{
Item {
value: 0,
opaque_value: opaque_value.into(),
}
}
pub(crate) fn from_void(pointer: *mut c_void, length: c_int) -> Item {
// we need to create an owned copy of the value
let opaque_value = if pointer.is_null() {
Vec::new()
} else {
unsafe {
std::slice::from_raw_parts(pointer as *mut u8, length as usize)
.to_owned()
}
};
Item::opaque(opaque_value)
}
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_add(
htab: *mut HashTable,
key: *const c_char,
value: c_int,
) -> c_int {
let hashtable = &mut *htab;
let key = CStr::from_ptr(key).to_owned();
hashtable.0.insert(key, Item::integer(value));
// the only time insertion can fail is if allocation fails. In that case
// we'll abort the process anyway, so if this function returns we can
// assume it was successful (0 = success).
0
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_add_ref(
htab: *mut HashTable,
key: *const c_char,
value_ptr: *mut c_void,
length: c_int,
) -> c_int {
let hashtable = &mut *htab;
let key = CStr::from_ptr(key).to_owned();
hashtable.0.insert(key, Item::from_void(value_ptr, length));
0
}
Примечание
Важно убедиться, что мы здесь используемCString
, а не обычныйString
, в качестве ключа хэш-таблицы, потому что*const c_char
может содержать любые ненулевые байты, тогда какString
в Rust требует, чтобы строка была валидной UTF-8.
Вероятно, нам сойдет с рук преобразованиеCStr
в&str
, а затем вString
с владением, потому что большинство входных данных будут ASCII, но учитывая, что нам понадобится один или дваunwrap()
, проще просто сделать всё правильно и сохранитьCString
.
Две функции
*_find()
можно делегировать прямо во внутренний HashMap<CString, Item>
.Единственное, где нужно быть осторожным, — это убедиться, что правильное значение возвращается, когда элемент не может быть найден. В данном случае, посмотрев на
tvm_htab.c
мы видим, что tvm_htab_find()
возвращает −1
, а tvm_htab_find_ref()
возвращает NULL
.// src/hmap.rs
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_find(
htab: *mut HashTable,
key: *const c_char,
) -> c_int {
let hashtable = &mut *htab;
let key = CStr::from_ptr(key);
match hashtable.get(key) {
Some(item) => item.value,
None => -1,
}
}
#[no_mangle]
pub unsafe extern "C" fn tvm_htab_find_ref(
htab: *mut HashTable,
key: *const c_char,
) -> *mut c_char {
let hashtable = &mut *htab;
let key = CStr::from_ptr(key);
match hashtable.0.get(key) {
Some(item) => item.value_ptr as *mut c_char,
None => ptr::null_mut(),
}
}
Теперь, когда мы фактически реализовали функции заглушки, всё должно снова работать.
Самый простой способ это проверить — запустить наш пример.
cargo run --example tvmi -- vendor/tinyvm/programs/tinyvm/fact.vm
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm`
1
2
6
24
120
720
5040
40320
362880
3628800
И чтобы перепроверить, мы можем запустить его через
valgrind
, чтобы убедиться, что нет утечек памяти или чего-то хитрого с указателями.$ valgrind target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm
==1492== Memcheck, a memory error detector
==1492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1492== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==1492== Command: target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm
==1492==
1
2
6
24
120
720
5040
40320
362880
3628800
==1492==
==1492== HEAP SUMMARY:
==1492== in use at exit: 0 bytes in 0 blocks
==1492== total heap usage: 270 allocs, 270 frees, 67,129,392 bytes allocated
==1492==
==1492== All heap blocks were freed -- no leaks are possible
==1492==
==1492== For lists of detected and suppressed errors, rerun with: -s
==1492== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)
Успех!
Реализация предварительной обработки данных
Виртуальная машина
tinyvm
использует упрощённую форму ассемблера, похожую на традиционный ассемблер Intel x86. Первым шагом при парсинге ассемблера tinyvm является запуск препроцессора, который интерпретирует операторы %include filename
и операторы %define identifier value
.Такого рода манипуляции с текстом намного проще выполнить с помощью типов
&str
в Rust, поэтому давайте посмотрим на интерфейс, который должен реализовать наш крейт.// vendor/tinyvm/include/tvm/tvm_preprocessor.h
#ifndef TVM_PREPROCESSOR_H_
#define TVM_PREPROCESSOR_H_
#include "tvm_htab.h"
int tvm_preprocess(char **src, int *src_len, struct tvm_htab_ctx *defines);
#endif
Использование
char **
и int *
для переменных src
и src_len
поначалу может показаться немного странным, но если бы вы написали эквивалент в Rust, то получили бы что-то вроде этого:fn tvm_preprocess(
src: String,
defines: &mut HashTable,
) -> Result<String, PreprocessorError> {
...
}
Код C просто использует выходные параметры для замены строки
src
на месте, потому что он не может возвращать как новую строку, так и код ошибки.Прежде чем сделать что-то ещё, нужно написать тест для
tvm_preprocess()
. Таким образом, мы можем гарантировать, что наша функция Rust функционально эквивалентна оригиналу.Мы взаимодействуем с файловой системой, поэтому нужно будет вытащить крейт tempfile.
$ cargo add --dev tempfile
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding tempfile v3.1.0 to dev-dependencies
Нам также понадобится крейт
libc
, потому что мы будем передавать строки libtvm
, которые ему, возможно, потребуется освободить.cargo add libc
Updating 'https://github.com/rust-lang/crates.io-index' index
Adding libc v0.2.66 to dev-dependencies
Глядя на исходный код, мы видим, что функция
tvm_preprocess()
будет продолжать разрешать %include
и %define
до тех пор, пока их не останется ни одного.Сначала давайте создадим тест, чтобы убедиться, что препроцессор обрабатывает
%define
. Мы знаем, что этот код уже работает (в конце концов, это код от tinyvm
), так что никаких сюрпризов быть не должно.// src/preprocessing.rs
#[cfg(test)]
mod tests {
use crate::ffi;
use std::{
ffi::{CStr, CString},
io::Write,
os::raw::c_int,
};
#[test]
fn find_all_defines() {
let src = "%define true 1\nsome random text\n%define FOO_BAR -42\n";
let original_length = src.len();
let src = CString::new(src).unwrap();
unsafe {
// get a copy of `src` that was allocated using C's malloc
let mut src = libc::strdup(src.as_ptr());
let mut len = original_length as c_int;
let defines = ffi::tvm_htab_create();
let ret = ffi::tvm_preprocess(&mut src, &mut len, defines);
// preprocessing should have been successful
assert_eq!(ret, 0);
// make sure the define lines were removed
let preprocessed = CStr::from_ptr(src).to_bytes();
let preprocessed =
std::str::from_utf8(&preprocessed[..len as usize]).unwrap();
assert_eq!(preprocessed, "\nsome random text\n\n");
// make sure the "true" and "FOO_BAR" defines were set
let true_define =
ffi::tvm_htab_find_ref(defines, b"true\0".as_ptr().cast());
let got = CStr::from_ptr(true_define).to_str().unwrap();
assert_eq!(got, "1");
let foo_bar =
ffi::tvm_htab_find_ref(defines, b"FOO_BAR\0".as_ptr().cast());
let got = CStr::from_ptr(foo_bar).to_str().unwrap();
assert_eq!(got, "-42");
// clean up our hashtable and copied source text
ffi::tvm_htab_destroy(defines);
libc::free(src.cast());
}
}
}
45 строк это намного больше, чем мне обычно нравится в тестах, но для преобразования туда и обратно между строками C требуется изрядное количество дополнительного кода.
Нам также нужно проверить, включая ещё один файл.
// src/preprocessing.rs
#[cfg(test)]
mod tests {
...
#[test]
fn include_another_file() {
const TOP_LEVEL: &str = "first line\n%include nested\nlast line\n";
const NESTED: &str = "nested\n";
// the preprocessor imports files from the filesystem, so we need to
// copy NESTED to a temporary location
let mut nested = NamedTempFile::new().unwrap();
nested.write_all(NESTED.as_bytes()).unwrap();
let nested_filename = nested.path().display().to_string();
// substitute the full path to the "nested" file
let top_level_src = TOP_LEVEL.replace("nested", &nested_filename);
std::fs::write(&nested, NESTED).unwrap();
unsafe {
let top_level_src = CString::new(top_level_src).unwrap();
// create a copy of the top_level_src which can be freed by C
let mut src = libc::strdup(top_level_src.as_ptr());
let mut len = libc::strlen(src) as c_int;
let defines = ffi::tvm_htab_create();
// after all that setup code we can *finally* call the preprocessor
let ret = ffi::tvm_preprocess(&mut src, &mut len, defines);
assert_eq!(ret, 0);
// make sure the define and import lines were removed
let preprocessed = CStr::from_ptr(src).to_bytes();
let got =
std::str::from_utf8(&preprocessed[..len as usize]).unwrap();
// after preprocessing, all include and define lines should have
// been removed
assert_eq!(got, "first line\nnested\nlast line\n");
ffi::tvm_htab_destroy(defines);
libc::free(src.cast());
}
}
Примечание
В качестве примечания, тест изначально был написан так, чтобы вложить всё в три слоя глубиной (например,top_level.vm
включает в себя nested.vm, которая включает в себя really_nested.vm), чтобы убедиться, что он обрабатывает более одного уровня%include
, но независимо от того, как он был написан, тест продолжал сегфолтить.
Затем я попробовал запустить исходный двоичный файл Ctvmi
…
$ cd vendor/tinyvm/ $ cat top_level.vm %include nested $ cat nested.vm %include really_nested $ cat really_nested.vm Hello World $ ./bin/tvmi top_level.vm [1] 10607 segmentation fault (core dumped) ./bin/tvmi top_level.vm
Оказывается, оригинальный tinyvm по какой-то причине падает, когда у вас несколько слоёвinclude
...
Итак, теперь у нас несколько тестов, так что можем начать реализовывать
tvm_preprocess()
.Во-первых, нужно определить тип ошибки.
// src/preprocessing.rs
#[derive(Debug)]
pub enum PreprocessingError {
FailedInclude {
name: String,
inner: IoError,
},
DuplicateDefine {
name: String,
original_value: String,
new_value: String,
},
EmptyDefine,
DefineWithoutValue(String),
}
Глядя на функции process_includes() и process_derives(), кажется, что они сканируют строку в поисках определённой директивы, а затем заменяют эту строку чем-то другим (либо содержимым файла, либо ничем, если строка должна быть удалена).
Мы должны быть в состоянии извлечь эту логику в хелпер и избежать ненужного дублирования.
// src/preprocessing.rs
/// Scan through the input string looking for a line starting with some
/// directive, using a callback to figure out what to replace the directive line
/// with.
fn process_line_starting_with_directive<F>(
mut src: String,
directive: &str,
mut replace_line: F,
) -> Result<(String, usize), PreprocessingError>
where
F: FnMut(&str) -> Result<String, PreprocessingError>,
{
// try to find the first instance of the directive
let directive_delimiter = match src.find(directive) {
Some(ix) => ix,
None => return Ok((src, 0)),
};
// calculate the span from the directive to the end of the line
let end_ix = src[directive_delimiter..]
.find('\n')
.map(|ix| ix + directive_delimiter)
.unwrap_or(src.len());
// the rest of the line after the directive
let directive_line =
src[directive_delimiter + directive.len()..end_ix].trim();
// use the callback to figure out what we should replace the line with
let replacement = replace_line(directive_line)?;
// remove the original line
let _ = src.drain(directive_delimiter..end_ix);
// then insert our replacement
src.insert_str(directive_delimiter, &replacement);
Ok((src, 1))
}
Теперь у нас есть хелпер
process_line_starting_with_directive()
, так что можно реализовать парсер %include
.// src/preprocessing.rs
fn process_includes(
src: String,
) -> Result<(String, usize), PreprocessingError> {
const TOK_INCLUDE: &str = "%include";
process_line_starting_with_directive(src, TOK_INCLUDE, |line| {
std::fs::read_to_string(line).map_err(|e| {
PreprocessingError::FailedInclude {
name: line.to_string(),
inner: e,
}
})
})
}
К сожалению, парсер %define немного сложнее.
// src/preprocessing.rs
n process_defines(
src: String,
defines: &mut HashTable,
) -> Result<(String, usize), PreprocessingError> {
const TOK_DEFINE: &str = "%define";
process_line_starting_with_directive(src, TOK_DEFINE, |line| {
parse_define(line, defines)?;
Ok(String::new())
})
}
fn parse_define(
line: &str,
defines: &mut HashTable,
) -> Result<(), PreprocessingError> {
if line.is_empty() {
return Err(PreprocessingError::EmptyDefine);
}
// The syntax is "%define key value", so after removing the leading
// "%define" everything after the next space is the value
let first_space = line.find(' ').ok_or_else(|| {
PreprocessingError::DefineWithoutValue(line.to_string())
})?;
// split the rest of the line into key and value
let (key, value) = line.split_at(first_space);
let value = value.trim();
match defines.0.entry(
CString::new(key).expect("The text shouldn't contain null bytes"),
) {
// the happy case, this symbol hasn't been defined before so we can just
// insert it.
Entry::Vacant(vacant) => {
vacant.insert(Item::opaque(value));
},
// looks like this key has already been defined, report an error
Entry::Occupied(occupied) => {
return Err(PreprocessingError::DuplicateDefine {
name: key.to_string(),
original_value: occupied
.get()
.opaque_value_str()
.unwrap_or("<invalid>")
.to_string(),
new_value: value.to_string(),
});
},
}
Ok(())
}
Чтобы получить доступ к тексту в нашей хэш-таблице, нужно будет дать элементу
Item
пару вспомогательных методов:// src/htab.rs
impl Item {
...
pub(crate) fn opaque_value(&self) -> &[u8] { &self.opaque_value }
pub(crate) fn opaque_value_str(&self) -> Option<&str> {
std::str::from_utf8(self.opaque_value()).ok()
}
}
На этом этапе неплохо добавить ещё несколько тестов.
// src/preprocessing.rs
#[cfg(test)]
mod tests {
...
#[test]
fn empty_string() {
let src = String::from("");
let mut hashtable = HashTable::default();
let (got, replacements) = process_defines(src, &mut hashtable).unwrap();
assert!(got.is_empty());
assert_eq!(replacements, 0);
assert!(hashtable.0.is_empty());
}
#[test]
fn false_percent() {
let src = String::from("this string contains a % symbol");
let mut hashtable = HashTable::default();
let (got, replacements) =
process_defines(src.clone(), &mut hashtable).unwrap();
assert_eq!(got, src);
assert_eq!(replacements, 0);
assert!(hashtable.0.is_empty());
}
#[test]
fn define_without_key_and_value() {
let src = String::from("%define\n");
let mut hashtable = HashTable::default();
let err = process_defines(src.clone(), &mut hashtable).unwrap_err();
match err {
PreprocessingError::EmptyDefine => {},
other => panic!("Expected EmptyDefine, found {:?}", other),
}
}
#[test]
fn define_without_value() {
let src = String::from("%define key\n");
let mut hashtable = HashTable::default();
let err = process_defines(src.clone(), &mut hashtable).unwrap_err();
match err {
PreprocessingError::DefineWithoutValue(key) => {
assert_eq!(key, "key")
},
other => panic!("Expected DefineWithoutValue, found {:?}", other),
}
}
#[test]
fn valid_define() {
let src = String::from("%define key value\n");
let mut hashtable = HashTable::default();
let (got, num_defines) = process_defines(src.clone(), &mut hashtable).unwrap();
assert_eq!(got, "\n");
assert_eq!(num_defines, 1);
assert_eq!(hashtable.0.len(), 1);
let key = CString::new("key").unwrap();
let item = hashtable.0.get(&key).unwrap();
assert_eq!(item.opaque_value_str().unwrap(), "value");
}
}
На этом этапе мы воспроизвели большую часть логики предварительной обработки, так что теперь просто нужна функция, которая будет продолжать расширять операторы
%include
и обрабатывать %define
до тех пор, пока их не останется.// src/preprocessing.rs
pub fn preprocess(
src: String,
defines: &mut HashTable,
) -> Result<String, PreprocessingError> {
let mut src = src;
loop {
let (modified, num_includes) = process_includes(src)?;
let (modified, num_defines) = process_defines(modified, defines)?;
if num_includes + num_defines == 0 {
return Ok(modified);
}
src = modified;
}
}
Конечно, эта функция
preprocess()
доступна только для Rust. Нужно создать extern "C" fn
, которая переводит аргументы из типов C во что-то, с чем может справиться Rust, а затем переводит обратно в C.// src/preprocessing.rs
#[no_mangle]
pub unsafe extern "C" fn tvm_preprocess(
src: *mut *mut c_char,
src_len: *mut c_int,
defines: *mut tvm_htab_ctx,
) -> c_int {
if src.is_null() || src_len.is_null() || defines.is_null() {
return -1;
}
// Safety: This assumes the tvm_htab_ctx is actually our ported HashTable
let defines = &mut *(defines as *mut HashTable);
// convert the input string to an owned Rust string so it can be
// preprocessed
let rust_src = match CStr::from_ptr(*src).to_str() {
Ok(s) => s.to_string(),
// just error out if it's not valid UTF-8
Err(_) => return -1,
};
match preprocess(rust_src, defines) {
Ok(s) => {
let preprocessed = CString::new(s).unwrap();
// create a copy of the preprocessed string that can be free'd by C
// and use the output arguments to pass it to the caller
*src = libc::strdup(preprocessed.as_ptr());
// the original C implementation didn't add a null terminator to the
// preprocessed string, so we're required to set the length as well.
*src_len = libc::strlen(*src) as c_int;
// returning 0 indicates success
0
},
// tell the caller "an error occurred"
Err(_) => -1,
}
}
Совет
Возможно, вы заметили, что наша функция tvm_preprocess() не имеет никакой логики предварительной обработки и больше похожа на адаптер для перевода аргументов и возвращаемых значений, а также для обеспечения правильного распространения ошибок.
Это не случайность.
Секрет кодирования FFI заключается в том, чтобы писать как можно меньше и избегать «умных» трюков. В отличие от большей части кода Rust, ошибки в таких функциях взаимодействия могут привести к ошибкам в логике и памяти.
Создание тонкой оболочки вокруг нашей функцииpreprocess()
также облегчает последующую задачу: когда большая часть кодовой базы написана на Rust, мы можем удалить оболочку и вызватьpreprocess()
напрямую.
Теперь функция
tvm_preprocess()
определена, и мы должны быть готовы к работе. Compiling tinyvm v0.1.0 (/home/michael/Documents/tinyvm-rs)
error: linking with `/usr/bin/clang` failed: exit code: 1
|
= note: "/usr/bin/clang" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-m64" "-L" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.13h6j6k0dzqf6zi2.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.13l2b4uvr7p3ht4k.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.14bdbjhozo3id49g.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.14fw2gyd6mrq5730.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.19xc7n0bb25uaxgk.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1duzy573vjvyihco.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1e0yejy24qufh7ie.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1k4xuir9ezt4vkzp.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1mqdnrarww1zjlt.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1ubflbxzxkx7grpn.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1vtvcpzzusyku3mk.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1wal3ebwyfg4qllf.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.235k75fk09i43ba3.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.253rt7mnjcp3n8ex.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.27phuscrye2lmkyq.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2bwv51h7gucjizh0.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2ghuai4hs88aroml.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2gqnd9h4nmhvgxbn.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2hjvtf620gtog0qz.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2hq7kc2w3vix8i5q.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2ibwag4iedx494ft.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2jdt9fes53g5mxlp.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2kv4bwega1wfr8z6.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2lja418hz58xlryz.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2o0foimqe73p8ujt.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2ouuhyii88vg8tqs.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2tnynvvdxge4sv9a.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2u1hzhj3v0d8kn4s.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2v1ii2legejcp3ir.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2vkkoofkb7zs04v1.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2w5mgql1gpr1f9uz.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2wdyioq7lxh9uxu7.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2wokgurbjsmgz12r.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.2wwcrmvusj07mx2n.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.310mxv7piqfbf4tr.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.3352aele91geo33m.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.36f4wrjtv0x5y00b.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.38f6o2m900r5q63j.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.3b67z5wg30f9te4l.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.3gyajmii4500y81t.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.3ovwslgcz03sp0ov.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.3vwhwp967j90qfpp.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.41ox17npnikcezii.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4472ut4qn508rg19.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4bbcvjauqmyr7tjc.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4c9lrc1xbvaru030.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4fzwdkjhjtwv5uik.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4gy2dy14zw2o60sh.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4i8qxpi0pmwn8d2e.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4isstj7ytb9d9yep.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4isz4o5d1flv8pme.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4lnnaom9zd4u3xmv.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4mgvbbhn4jewmy60.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4q7wf9d53jp9j6y6.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4qimnegzmsif2zbr.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4scm7492lh4yspgt.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4ten9b8okg10ap4i.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4vrj7dhlet4j6oe.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4wtf4i2ggbrvqt63.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4zsqxnhj8yusiplh.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.50o8i1bmvqwd5eg7.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.50urmck1r52hucuw.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.51w3uc6agh3gynn3.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.55o6ad6nlq4o2zyt.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.57gih8p2bu1jbo0l.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.57rpuf5wpgkfmf1z.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.5920w55mlosqy9aj.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.5c1ra5cheein740g.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.5cuuq0m7tzehyrti.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.5e85z18y46lhofte.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.6yu7c01lw47met2.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.cn69np51jgriev2.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.d224rq9cs4mbv0q.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.e0vaqgnhc25c4ox.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.edm0ce3nfzegp4d.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.elxjhifv4wlzkc2.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.ifqyaukx6gnbb0a.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.kr8s9rcy6ux2d02.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.ley637x8c2etn66.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.njyqsm0frvb1j4d.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.r9ttxk3s5kacz9k.rcgu.o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.xrorvssabbgfjqz.rcgu.o" "-o" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88" "/home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.1iplfu0pt8fy07e4.rcgu.o" "-Wl,--gc-sections" "-pie" "-Wl,-zrelro" "-Wl,-znow" "-nodefaultlibs" "-L" "/home/michael/Documents/tinyvm-rs/target/debug/deps" "-L" "/home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out" "-L" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib" "-Wl,-Bstatic" "-Wl,--whole-archive" "-ltvm" "-Wl,--no-whole-archive" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libtest-a39a3e9a77b17f55.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libterm-97a69cd310ff0925.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libgetopts-66a42b1d94e3e6f9.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunicode_width-dd7761d848144e0d.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_std-f722acdb78755ba0.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libtempfile-b08849d192e5c2e1.rlib"
"/home/michael/Documents/tinyvm-rs/target/debug/deps/librand-c85ceffb304c7385.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/librand_chacha-4e4839e3036afe89.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libc2_chacha-7555b62a53de8bdf.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libppv_lite86-0097c0f425957d6e.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/librand_core-de2208c863d15e9b.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libgetrandom-c696cd809d660e17.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/liblibc-d52d0b97a33a5f02.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libremove_dir_all-4035fb46dbd6fb92.rlib" "/home/michael/Documents/tinyvm-rs/target/debug/deps/libcfg_if-6adeb646d05b676c.rlib" "-Wl,--start-group" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libstd-974c3c08f6def4b3.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libpanic_unwind-eb49676f33a2c8a6.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libhashbrown-7ae0446feecc60f2.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_alloc-2de299b65d7f5721.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace-64514775bc06309a.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libbacktrace_sys-1ed8aa185c63b9a5.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_demangle-a839df87f563fba5.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libunwind-8e726bdc2018d836.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcfg_if-5285f42cbadf207d.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liblibc-b0362d20f8aa58fa.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/liballoc-f3dd7051708453a4.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/librustc_std_workspace_core-83744846c43307ce.rlib" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcore-d5565a3a0f4cfe21.rlib" "-Wl,--end-group" "/home/michael/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-gnu/lib/libcompiler_builtins-ea790e85415e3bbf.rlib" "-Wl,-Bdynamic" "-lutil" "-lutil" "-ldl" "-lrt" "-lpthread" "-lgcc_s" "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil" "-fuse-ld=lld"
= note: ld.lld: error: duplicate symbol: tvm_preprocess
>>> defined at preprocessing.rs:13 (src/preprocessing.rs:13)
>>> /home/michael/Documents/tinyvm-rs/target/debug/deps/tinyvm-8eca24ff9a1cde88.4mgvbbhn4jewmy60.rcgu.o:(tvm_preprocess)
>>> defined at tvm_preprocessor.c:135 (vendor/tinyvm/libtvm/tvm_preprocessor.c:135)
>>> tvm_preprocessor.o:(.text.tvm_preprocess+0x0) in archive /home/michael/Documents/tinyvm-rs/target/debug/build/tinyvm-3f1a2766f78b5580/out/libtvm.a
clang: error: linker command failed with exit code 1 (use -v to see invocation)
error: aborting due to previous error
error: could not compile `tinyvm`.
To learn more, run the command again with --verbose.
Ой, линкер жалуется, что и
preprocessing.rs
, и tvm_preprocessor.c
определяют функцию tvm_preprocess()
. Похоже, мы забыли удалить tvm_preprocessor.c
из сборки…diff --git a/build.rs b/build.rs
index 0ed012c..42b8fa0 100644
--- a/build.rs
+++ b/build.rs
@@ -14,6 +14,7 @@ fn main() {
.file(src.join("tvm_memory.c"))
.file(src.join("tvm_parser.c"))
.file(src.join("tvm_program.c"))
- .file(src.join("tvm_preprocessor.c"))
.file(src.join("tvm.c"))
.include(&include)
.compile("tvm");
(END)
Попробуем ещё раз.
cargo run --example tvmi -- vendor/tinyvm/programs/tinyvm/fact.vm
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/examples/tvmi vendor/tinyvm/programs/tinyvm/fact.vm`
1
2
6
24
120
720
5040
40320
362880
3628800
Гораздо лучше!
Помните тот прошлый пример, где
tvmi
падал, получая три уровня глубины кода? Как приятный побочный эффект, после переноса кода на Rust вложенные уровни просто работают.Примечание
Возможно, вы также заметили, что функцияpreprocess()
не использует ни одной функции хэш-таблицы изtvm_htab.h
. После портирования модуля на Rust мы просто используем типы Rust напрямую.
В этом вся прелесть этого процесса. Как только вы перенесли что-то на Rust, вы можете применить это, чтобы использовать типы/функции непосредственно — и мгновенно выиграть в обработке ошибок и эргономике.
Заключение
Если вы все еще читаете статью, поздравляю, мы только что портировали два модуля с
tinyvm
на Rust.К сожалению, эта статья уже довольно длинная. Но надеюсь, что к настоящему моменту вы уловили общую картину.
- Просмотреть заголовочные файлы приложения и найти простую функцию/модуль
- Написать несколько тестов, чтобы понять, как должна работать существующая функция
- Написать эквивалентные функции на Rust и убедиться, что они проходят те же тесты
- Создать тонкую прокладку, которая экспортирует функцию Rust с тем же интерфейсом C, не забывая удалить исходную функцию/модуль из сборки, чтобы компоновщик использовал код Rust вместо C
- Перейти к шагу 1
Самое лучшее в этом методе заключается в том, что вы постепенно улучшаете кодовую базу, приложение сохраняет работоспособность, а вы не переписываете всё от начала до конца.
Это всё равно что менять колесо на ходу.
Предпочтительный способ переноса приложения с C на Rust
См. также: