Как создать приложение Rust с нуля за 16 часов

Rust
Как создать приложение Rust с нуля за 16 часов

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

Некоторые из нас просто хотят развлечься, некоторые хотят узнать что-то новое, а некоторые из нас могут просто захотеть доказать какую-то концепцию или идею.

Последние несколько недель я пассивно собирал информацию о Rust или работал с кодом на Rust, поэтому я подумал, что хакатоны — отличное время для изучения Rust.

Нехватка времени на хакатоне позволяет мне учиться быстрее и одновременно решать реальные проблемы.

Почему ржавчина

Getting a chance to peek under the hood again

В первые 10 лет своей карьеры я использовал C и C++ в течение 8 лет.

С другой стороны, мне нравятся такие языки, как C++, которые обеспечивают статическую типизацию, потому что они обнаруживают ошибки на ранней стадии компиляции.

Вот некоторые из моих личных мнений о C++:

  • Инженеры могут запросто выстрелить себе в ногу
  • Как язык программирования он уже очень раздутый и сложный
  • Отсутствие хорошей, стандартной, широко применимой системы управления пакетами

С тех пор, как я переключился на веб-приложения, я занимаюсь разработкой на Python и JavaScript, используя такие фреймворки, как Django, Flask и Express.

Мой опыт разработки на Python и JavaScript до сих пор показывает, что они могут обеспечить хорошую итерацию программы и скорость доставки, но иногда могут занимать много ресурсов ЦП и памяти, даже когда служба относительно простаивает.

Я часто ловлю себя на том, что пишу программы на C++, которым не хватает безопасности, скорости и простоты.

Я ищу минимальный язык программирования на чистом железе, такой как Rust, для разработки веб-приложений.

Нет времени выполнения, нет сборки мусора. Загрузите двоичный код напрямую и передайте его ядру для выполнения.

Цель

Моя цель — завершить приложение, в котором бэкенд написан на Rust, а фронтенд — JavaScript+React, по аналогии с S3 в качестве графа.Пользователи могут сделать следующее:

  • Просмотрите все изображения в картинной кроватке (нумерация страниц не обязательна)
  • загрузить изображение
  • Вы можете добавлять теги к изображениям при загрузке изображений
  • Запрос или фильтр по имени

У всех интересных проектов хакатона есть название, поэтому я решил назвать этот:

RustIC -> Rust + Image Contents

Let’s hack something great

Я считаю этот хакатон успешным лично для меня, если я сделаю следующее:

  • Иметь базовое представление о Rust, включая его систему типов и модель памяти.
  • Изучите возможности предварительно подписанных ссылок S3 для файлов и произвольных тегов
  • Напишите проверяемое функциональное приложение

Так как моя основная цель — развивать фичи с учетом обучения. Много кода я написал, пока учился, поэтому организация и эффективность кода могут быть не оптимальными, поскольку это второстепенные цели.

Принципы ржавчины

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

Вопреки тому, что я читал во многих блогах, в Rust есть вероятность утечек памяти (циклические ссылки) и небезопасных операций (в небезопасных блоках), как подробно описано в FAQ выше.

«Мы [создатели языка] не собираемся [для Rust] быть на 100% статичным, на 100% безопасным, на 100% отражающим».

Dazzling, intricate, sophisticated

Начните с бэкенда

Поиск в Google по запросу "Rust web framework" далRocket. Я зашел на этот сайт и обнаружил, что все примеры документации понятны с первого взгляда.

Следует отметить, что для Rocket требуется ночная версия Rust, но это незначительная проблема на хакатоне.

ГитхабБиблиотека кодаЕсть очень богатые примеры. Идеально!

я используюCargoСоздал новый проект, добавил зависимости Rocket в файл TOML и следовал указаниям Rocket.Руководство по началу работы, написал первый фрагмент кода:

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

fn main() {
    rocket::ignite().mount("/", routes![index]).launch();
}

Для студентов, знакомых с такими фреймворками, как Django, Flask, Express и т. д., этот код очень легко читается. Как пользователь Rocket, вы можете использовать макросы в качестве декораторов для сопоставления маршрутов с соответствующими обработчиками.

Во время компиляции макрос будет развернут. Это полностью прозрачно для разработчика. Если вы хотите увидеть расширенный код, вы можете использоватьcargo-expand.

Вот некоторые интересные или сложные моменты моей работы по созданию приложений на Rust:

Укажите ответ маршрута

Я хочу вернуть список всех файлов в S3 в формате данных JSON.

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

Настроить структуру ответа очень легко, если вы хотите вернуть данные в формате JSON, и каждое поле имеет свою структуру и тип, он соответствует Rust'овскомуstruct.

Итак, вы должны сначала определить структуруstruct(S)чтобы принять ответ и должен быть аннотирован:

#[derive(Serialize)]

структуры отмечены#[derive(Serialize)], поэтому его можно преобразовать в JSON с помощью `rocket_contrib::json::Json.

#[derive(Serialize)]
struct BucketContents {
    data: Vec<S3Object>,
}

#[derive(Serialize)]
struct S3Object {
    file_name: String,
    presigned_url: String,
    tags: String,
    e_tag: String, // AWS generated MD5 checksum hash for object
    is_filtered: bool,
}

#[get("/contents?<filter>")]
fn get_bucket_contents(
    filter: Option<&RawStr>
) -> Result<Json<BucketContents>, Custom<String>> {
    // Returns either Ok(Json(BucketContents)) or,
    // a Custom error with a reason
}

Обработка многокомпонентных загрузок

Когда я понял, что есть большая вероятность, что мой интерфейс использует метод POST для загрузки формата какmultipart/form-dataform data, я начал копаться в том, как использовать Rocket для создания программ.

К сожалению, Rocket 0.4 не поддерживает multipart, похоже будет в 0.5.

Это означает, что мне нужно использоватьmultipartящик и интегрировать в Rocket. Окончательный код будет работать нормально, но если Rocket поддерживает multipart, код будет более лаконичным.

#[post("/upload", data = "<data>")]
// signature requires the request to have a `Content-Type`. The preferred way to handle the incoming
// data would have been to use the FromForm trait as described here: https://rocket.rs/v0.4/guide/requests/#forms
// Unfortunately, file uploads are not supported through that mechanism since a file upload is performed as a
// multipart upload, and Rocket does not currently (As of v0.4) support this. 
// https://github.com/SergioBenitez/Rocket/issues/106
fn upload_file(cont_type: &ContentType, data: Data) -> Result<Custom<String>, Custom<String>> {
    // this and the next check can be implemented as a request guard but it seems like just
    // more boilerplate than necessary
    if !cont_type.is_form_data() {
        return Err(Custom(
            Status::BadRequest,
            "Content-Type not multipart/form-data".into()
        ));
    }

    let (_, boundary) = cont_type.params()
                                 .find(|&(k, _)| k == "boundary")
                                 .ok_or_else(
        || Custom(
            Status::BadRequest,
            "`Content-Type: multipart/form-data` boundary param not provided".into()
        )
    )?;

    // The hot mess that ensues is some weird combination of the two links that follow
    // and a LOT of hackery to move data between closures.
    // https://github.com/SergioBenitez/Rocket/issues/106
    // https://github.com/abonander/multipart/blob/master/examples/rocket.rs
    let mut d = Vec::new();
    data.stream_to(&mut d).expect("Unable to read");
    let mut mp = Multipart::with_body(Cursor::new(d), boundary);

    let mut file_name = String::new();
    let mut categories_string = String::new();
    let mut raw_file_data = Vec::new();

    mp.foreach_entry(|mut entry| {
        if *entry.headers.name == *"fileName" { 
            let file_name_vec = entry.data.fill_buf().unwrap().to_owned();
            file_name = from_utf8(&file_name_vec).unwrap().to_string()
        } else if *entry.headers.name == *"tags" {
            let tags_vec = entry.data.fill_buf().unwrap().to_owned();
            categories_string = from_utf8(&tags_vec).unwrap().to_string();
        } else if *entry.headers.name == *"file" {
            raw_file_data = entry.data.fill_buf().unwrap().to_owned()
        }
    }).expect("Unable to iterate");

    let s3_file_manager = s3_interface::S3FileManager::new(None, None, None, None);
    s3_file_manager.put_file_in_bucket(file_name.clone(), raw_file_data);

    let tag_name_val_pairs = vec![("tags".to_string(), categories_string)];
    s3_file_manager.put_tags_on_file(file_name, tag_name_val_pairs);

    return Ok(
        Custom(Status::Ok, "Image Uploaded".to_string())
    );
}

Настроить CORS

После того, как маршруты написаны, я начинаю тестировать с помощью curl или Postman, и теперь пришло время заняться интеграцией фронтенда. Мне нужно правильно установить заголовки ответов, чтобы избежать междоменных проблем.

Rocket по-прежнему не поддерживает эту функцию.

Затем я нашел несколько решений в репозитории GitHub:

// CORS Solution below comes from: https://github.com/SergioBenitez/Rocket/issues/25
extern crate rocket;

use std::io::Cursor;
use rocket::fairing::{Fairing, Info, Kind};
use rocket::{Request, Response};
use rocket::http::{Header, ContentType, Method};

struct CORS();

impl Fairing for CORS {
    fn info(&self) -> Info {
        Info {
            name: "Add CORS headers to requests",
            kind: Kind::Response
        }
    }

    fn on_response(&self, request: &Request, response: &mut Response) {
        if request.method() == Method::Options || 
           response.content_type() == Some(ContentType::JSON) || 
           response.content_type() == Some(ContentType::Plain) {

            response.set_header(Header::new("Access-Control-Allow-Origin", "http://localhost:3000"));
            response.set_header(Header::new("Access-Control-Allow-Methods", "POST, GET, OPTIONS"));
            response.set_header(Header::new("Access-Control-Allow-Headers", "Content-Type"));
            response.set_header(Header::new("Access-Control-Allow-Credentials", "true"));
        }

        if request.method() == Method::Options {
            response.set_header(ContentType::Plain);
            response.set_sized_body(Cursor::new(""));
        }
    }
}

fn main() {
    
    rocket::ignite().attach(
        CORS()
    ).mount(
        "/", 
        routes![get_bucket_contents, upload_file]
    ).launch();
}

Через некоторое время я нашелrocket_cors, это помогло мне резко сократить объем кода.

fn main() -> Result<(), Error> {
    let allowed_origins = AllowedOrigins::some_exact(&["http://localhost:3000"]);

    let cors = rocket_cors::CorsOptions {
        allowed_origins,
        allowed_methods: vec![Method::Get, Method::Post].into_iter().map(From::from).collect(),
        allowed_headers: AllowedHeaders::some(&["Content-Type", "Authorization", "Accept"]),
        allow_credentials: true,
        ..Default::default()
    }
    .to_cors()?;


    rocket::ignite().attach(cors)
                    .mount("/", routes![get_bucket_contents, upload_file])
                    .launch();

    Ok(())
}

и работает

Нам просто нужен простойcargo runкоманда для запуска программы

output

Монитор активности на моей машине сообщает мне, что эта программа работает и потребляет всего 2,7 МБ памяти.

И это просто не оптимизированные отладочные сборки. Использование проекта- releaseЕсли тег упакован, во время выполнения требуется только 1,6 МБ памяти.

memory

Бэкенд-сервер на основе Rust, где мы запрашиваем/contentsЭтот маршрут получит следующий ответ:

{
    "data": [
        {
            "file_name": "Duck.gif",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/Duck.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=1369c003b2f54510882bf9982ab56d024d6c9d2655a4d86f8907313c7499b56d&X-Amz-SignedHeaders=host",
            "tags": "animal",
            "e_tag": "\"93c570cadd6b8b2f85b47c2f14fd82a1\"",
            "is_filtered": false
        },
        {
            "file_name": "GIZMO.png",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/GIZMO.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050353Z&X-Amz-Expires=1800&X-Amz-Signature=040e76c2df5a9a54ed4fbc8490378cf732b32bae78f628448536fc610018c0c3&X-Amz-SignedHeaders=host",
            "tags": "robots",
            "e_tag": "\"2cde221a0c7a72c0a7a60cffce29a0bc\"",
            "is_filtered": false
        },
        {
            "file_name": "GreenSmile.gif",
            "presigned_url": "https://s3.amazonaws.com/rustic-images/GreenSmile.gif?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIARDWJNDW3U8329UDNJ%2F20200107%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20200107T050354Z&X-Amz-Expires=1800&X-Amz-Signature=d115b107de530ce15b3590abdbab355c2a9481a81131f88bf4ad2a59ca11bbac&X-Amz-SignedHeaders=host",
            "tags": "smile-face",
            "e_tag": "\"86854a599540f50bdc5e837d30ca34f9\"",
            "is_filtered": false
        }
    ]
}

Работа с фронтендом относительно проста, мы используем:

  • React
  • React Bootstrap
  • react-grid-gallery
  • react-tags-input

Пользователи могут просматривать изображения на нашей странице, а также искать или фильтровать их по имени файла или тегу.

images

Пользователи также могут загружать файлы, перетаскивая их, и могут помечать их перед отправкой на загрузку.

upload

Причины, по которым я люблю создавать приложения на Rust

  • Уровень зависимости Cargo и управления приложениями просто потрясающий
  • Компилятор очень помогает нам бороться с ошибками компиляции, блогер вблогОписывает, как он писал код в соответствии с большими рекомендациями компилятора. Мой опыт очень похож.
  • Я был приятно удивлен, что для каждой функции, которая мне нужна, есть ящик.

Crates galore on crates.io!

  • онлайнRust Playground, что позволяет мне запускать небольшие фрагменты кода.
  • Языковой сервер Rust, хорошо интегрированный в Visual Studio Code, обеспечивает проверку ошибок в реальном времени, форматирование, поиск символов и многое другое. Это позволило мне добиться приличного прогресса за несколько часов без компиляции.

Неудобства, неожиданности и неприятности

Хотя документация Rust великолепна, мне пришлось полагаться на документацию и примеры некоторых ящиков. В некоторых ящиках есть отличные интеграционные тесты, которые дают некоторые подсказки о том, как их использовать. Конечно, мне очень помогли Stack Overflow и Reddit.

“Where’s the documentation?”

Также обратите внимание, что:

  • Понимание прав собственности, жизненного цикла и заимствования прав собственности может усложнить обучение, особенно при попытке реализовать функциональность во время двухдневного хакатона. Я сравниваю их с C++ и разбираюсь, но иногда все равно путаюсь.
  • Во всем,Stringsостановил меня на несколько минут, особенноStringа также&strРазница еще более запутанная - я не знал, пока я не потратил некоторое время на понимание владения, жизни и заимствования права собственности.

некоторые другие наблюдения

  • В Rust нет настоящего нулевого типа, обычно нулевые значения нужно использовать сOptionТипNoneПредставлять
  • Сопоставление с образцом — это круто, это одна из моих любимых функций в Scala, то же самое и в Rust. Этот код выглядит выразительно и позволяет компилятору помечать необработанные случаи.
match bucket_contents {
    Err(why) => match why {
        S3ObjectError::FileWithNoName => Err(Custom(
            Status::InternalServerError,
            "Encountered bucket objects with no name".into()
        )),
        S3ObjectError::MultipleTagsWithSameName => Err(Custom(
            Status::InternalServerError,
            "Encountered a file with a more than one tag named 'tags'".into()
        ))
    },
    Ok(s3_objects) => {
        let visible_s3_objects: Vec<S3Object> = s3_objects.into_iter()
                                                          .filter(|obj| !obj.is_hidden())
                                                          .collect();
        Ok(Json(BucketContents::new(visible_s3_objects)))
    }
}
  • Говоря о безопасных и небезопасных режимах, вы все еще можете программировать на более низком уровне, например, взаимодействовать с кодом C через интерфейсы в небезопасном режиме. Несмотря на то, что в Rust есть много проверок правильности, вы все равно можете делать некоторые трюки в небезопасных модулях, например, разыменовывать. Человек, читающий код, также может получить много информации из незащищенного модуля.
  • пройти черезBoxВыделять память в куче вместоnewа такжеdelete. Сначала это казалось странным, но понять было легко. Другие также определены в стандартной библиотеке.умный указатель, вы можете использовать его напрямую, если вам нужно использовать количество ссылок или слабых ссылок.
  • Исключения в Rust также интересны тем, что в нем нет исключений. вы можете использоватьResult<T, E>Указывает на исправимые ошибки, которые также можно использовать сpanic!Макросы представляют неисправимые ошибки.
// This code:
// 1. Takes a vector of objects representing S3 contents
// 2. Uses filter to remove entries we don't care about
// 3. Uses map to transform each object into another type, but terminates iteration
// .  if the lambda passed to map returns an Err. 
// 4. If all iterations produced an Ok(S3Object) result, these are collected into a Vec<S3Object>
let bucket_contents: Result<Vec<S3Object>, S3ObjectError> = bucket_list
        .into_iter()
        .filter(|bucket_obj| bucket_obj.size.unwrap_or(0) != 0) // Eliminate folders
        .map(|bucket_obj| {
            if let None = bucket_obj.key {
                return Err(S3ObjectError::FileWithNoName);
            }

            let file_name = bucket_obj.key.unwrap();
            let e_tag = bucket_obj.e_tag.unwrap_or(String::new());
            let tag_req_output = s3_file_manager.get_tags_on_file(file_name.clone());
            let tags_with_categories: Vec<Tag> = tag_req_output.into_iter()
                                                            .filter(|tag| tag.key == "tags")
                                                            .collect();
            if tags_with_categories.len() > 1 {
                return Err(S3ObjectError::MultipleTagsWithSameName);
            }

            let tag_value = if tags_with_categories.len() == 0 {
                "".to_string()
            } else {
                tags_with_categories[0].value.clone()
            };

            let presigned_url = s3_file_manager.get_presigned_url_for_file(
                file_name.clone()
            );
            Ok(S3Object::new(
                file_name,
                e_tag,
                tag_value,
                presigned_url,
                false,
            ))
        })
        .collect();

В инструкции это описано так:

В большинстве случаев Rust требует, чтобы вы как можно лучше понимали ошибки и соответствующим образом обрабатывали их перед компиляцией. Это требование делает вашу программу более надежной, гарантируя, что вы сможете отловить и обработать ошибки в ней до того, как она будет выпущена.

Очки и уроки

  • Джон Кармак однажды назвал опыт написания Rust «очень полезным». Я согласен с этим чувством, этот хакатон ощущается как открытие двери в новый мир и открытие множества новых вещей, и эти достижения не только на уровне кода.
  • Оглядываясь назад, я должен был быть более строгим в выборе веб-фреймворка. Если подумать, я мог бы пойти другим путем. Я мог бы выбрать в следующий разiron,actix-web, илиtiny-http.
  • Я только поверхностно познакомился с Rust, за 16 часов полностью стать рустацем невозможно, хотя мне любопытен язык, и я получил кое-какие глубокие знания. Я воодушевлен будущим Rust, я думаю, что он дает много спецификаций для создания приложений, это очень выразительный язык, и он дает нам скорость работы и производительность памяти, сравнимую с производительностью C++.

ресурс

Серверный код RustIC

Интерфейсный код RustIC

Rusoto: AWS SDK для Rust

Оригинальная ссылка

medium.com/better-pro Страна…