Почему ваша первая реализация FizzBuzz на Rust может не работать

в 20:01, , рубрики: c++, fizzbuzz, lifetime, ownership, python, Rust, ненормальное программирование, системное программирование

Полное оригинальное название статьи: «Why your first FizzBuzz implementation may not work: an exploration into some initially surprising but great parts of Rust (though you still might not like them)»

tl;dr;-версия: На первый взгляд некоторые аспекты Rust могут показаться странными и даже неудобными, однако, они оказываются весьма удачными для языка, который позиционируется как системный. Концепции владения (ownership) и времени жизни (lifetime) позволяют привнести в язык сильные статические гарантии и сделать программы на нём эффективными и безопасными, как по памяти, так и по времени.

Лицензия: CC-BY, автор Chris Morgan.

Почему ваша первая реализация FizzBuzz может не работать: исследование некоторых особенностей Rust, которые изначально шокируют, но в действительности являются его лучшими сторонами (хотя они всё равно могут вам не понравиться)

http://chrismorgan.info/media/images/rust-fizzbuzz.svgFizzBuzz предлагается как простое задание для новичка, но в Rust присутствуют несколько подводных камней, о которых лучше знать. Эти подводные камни не являются проблемами Rust, а, скорее, отличиями от того, с чем знакомо большиство программистов, ограничениями, которые на первый взгляд могут показаться очень жёсткими, но в действительности дают громадные преимущества за малой ценой.

Rust это движущаяся цель, тем не менее, язык становится стабильней. Код из статьи работает с версией 0.12. Если что-то сломается, пожалуйста, свяжитесь со мной. Касательно кода на Python, он будет работать как в двойке, так и в тройке.

Простая реализация

Ок, я сказал в заголовке, что ваша первая реализация FizzBuzz может не работать, чтож, она может и работать. Вы могли бы написать её как в примере ниже. Для краткости опустим fn main() { … }. Если вас беспокоит то, что код на Python оказывается короче, чем на Rust, то для вас есть специальная форма кода на Python, она доступна по нажатию на чекбокс. (прим. пер. в оригинальной статье есть чекбокс, который переключает код на Python в «специальную форму» from __future__ import braces, что-то вроде пасхалки от автора).

Реализация FizzBuzz с отдельными инструкциями print: всё просто работает.
Python Rust
for i in range(1, 101):
    if i % 15 == 0:
        print('FizzBuzz')
    elif i % 5 == 0:
        print('Buzz')
    elif i % 3 == 0:
        print('Fizz')
    else:
        print(i)

for i in range(1i, 101) {
    if i % 15 == 0 {
        println!("FizzBuzz");
    } else if i % 5 == 0 {
        println!("Buzz");
    } else if i % 3 == 0 {
        println!("Fizz");
    } else {
        println!("{}", i);
    }
}

Обе программы производят желаемый результат, и они, очевидно, очень схожи. Главное, что здесь стоит упомянуть, это то, что в Rust println!()[1] требует строковой литерал в качестве первого аргумента, строку формата; соответствующий код на Python будет выглядеть так: print('{}'.format(i)).

Но что если мы хотим избавиться от дублирования вызова print в коде? Вот как это могло бы выглядеть:

FizzBuzz с одной инструкцией print.
Python Rust (не скомпилируется)
for i in range(1, 101):
    if i % 15 == 0:
        result = 'FizzBuzz'
    elif i % 5 == 0:
        result = 'Buzz'
    elif i % 3 == 0:
        result = 'Fizz'
    else:
        result = i
    print(result)

for i in range(1i, 101) {
    let result = if i % 15 == 0 {
        "FizzBuzz"
    } else if i % 5 == 0 {
        "Buzz"
    } else if i % 3 == 0 {
        "Fizz"
    } else {
        i
    };
    println!("{}", result);
}

Обратите внимание, как в Rust мы можем использовать целый блок if как выражение. Даже результат присваивания в действительности не нужен, мы могли бы просто запихнуть весь блок в ту конструкцию, где он используется. Это очень знакомый подход для рубистов, но не для питонистов, потому что в Python всё является инструкцией, а не выражением. Если вы скептически относитесь к этому подходу, я понимаю; когда я начал знакомство с Rust, его уклон в выражения и возможность опускать инструкцию return показались мне чудн ́ыми. Но, используя Rust, я осознал, что это совсем не так. На самом деле это здорово.

Python был моим любимым языком в течении пяти лет, но, несмотря на то, что я продолжаю профессионально писать на этом языке (правда, хотел бы переключиться на Rust), я обнаружил, что мне всё чаще не хватает фишек из Rust. При работе с Rust я не чувствую такого же недостатка чего-либо из Python, если не считать нужды в библиотеках, которых ещё нет в Rust. Вообще, Rust очень сильно принизил Python в моих глазах.

Код на Rust выглядит хорошо, но в действительности он не работает из-за строгих правил типизации в этом языке. Так какой же тип переменной result? Первые три ветви if возвращают строки, а четвёртая — целое число:

f.rs:7:12: 11:6 error: if and else have incompatible types: expected `&'static str`, found `int` (expected &-ptr, found int)
f.rs:7     } else if i % 3 == 0 {
f.rs:8         "Fizz"
f.rs:9     } else {
f.rs:10         i
f.rs:11     };
error: aborting due to previous error

Это не работает. Как насчёт того, чтобы превратить число в строку?

for i in range(1i, 101) {
    let result = if i % 15 == 0 {
        "FizzBuzz"
    } else if i % 5 == 0 {
        "Buzz"
    } else if i % 3 == 0 {
        "Fizz"
    } else {
        i.to_string()
    };
    println!("{}", result);
}

Здесь мы утянули фишку, с которой многие знакомы из других языков программирования (to_string) и применили её в той области, в которой не так много кто разбирается. В общем, это не работает.

f.rs:7:12: 11:6 error: if and else have incompatible types: expected `&'static str`, found `collections::string::String` (expected &-ptr, found struct collections::string::String)
f.rs:7     } else if i % 3 == 0 {
f.rs:8         "Fizz"
f.rs:9     } else {
f.rs:10         i.to_string()
f.rs:11     };
error: aborting due to previous error

«Что?» Я так и слышу, как вы говорите «разве теперь они не все строки? В чём дело с этим &'static str (да как это вообще, чёрт возьми, произнести?) и collections::string::String?». На этом этапе нам стоит более тщательно подойти к анализу типов значений, производимых ветвями: первые три ветви не производят просто какую-то «строку», они производят &'static str, а четвёртая ветвь не производит просто «целое», а int. В языках навроде Python, Ruby и JavaScript типы целых объединены (а JS пошёл ещё дальше, и вовсе объединил все числовые типы), в то время как C#, Java, Go имеют множество типов целых, различающихся размером. Но даже языки типа C#, Java, Go имеют всего один тип для строки.

А Rust — нет. У него их два.

Два типа строк? Это что вообще?

Здесь мы могли бы ограничиться простым объяснением и пойти дальше, но раз уж мы спустились так глубоко, то почему бы не спуститься до конца, и понять, что именно было сделано, и почему оно абсолютно того стоит. Так почему же C♯, Java и Go смогли удовлетвориться одним строковым типом, а Rust нет? Чтобы ответить на этот вопрос мы должны спуститься на уровень управления памятью.

Как C♯, так Java и Go — все они являются управляемыми языками[2] (также известные как языки со сборкой мусора). То есть, у них в рантайме есть механизм, который управляет выделением и освобождением памяти в соответствующее время: когда никто больше не использует строку, она может быть особождена. Таким образом, они могут возвращать ссылку на строку, не волнуясь о её времени жизни: строки, которые ещё используются, не будут особождены.

Для этих языков здесь также есть одна уступка. Как правило, они имеют неизменяемые (иммутабельные) строки — если сконкатенировать две строки, произойдёт выделение памяти (аллокация) под новую строку нужного размера. (Это также означает, что для решения, которое будет конкатенировать Fizz и Buzz в соответствующих случаях, будет происходить две аллокации для чисел, делящихся на 15. Правда, некоторые языки могут немного сгладить этот негативный эффект, применяя то, что называется пулом строк или интернированием. Успех работы этого механизма зависит от оптимизатора и того, как написан код) Я полагаю, строки иммутабельны потому, что в качестве альтернативы у нас будет большее из зол — изменение строки может повлиять на другие строки, зависящие от неё. Это сильно бьёт по корректности программы и может вести к состояниям гонки в том, что по сути является примитивным типом.
Также, для unicode-строк это может вести к возникновению некорректных срезов строк. Конечно, эти проблемы возникают также и в других местах, но иметь их и в строках может быть намного хуже. (Я сказал, что эти языки имеют один строковой тип, но это не совсем так — есть также и специализированные строковые типы, например, как Java, так и .NET имеют механизм, называемый StringBuilder).

Модель Rust отличается от используемой в языках со сборкой мусора и основана на понятии владения (ownership). В этой модели каждый объект имеет одного владельца (прим. пер. is owned) в одном месте, в один момент времени, а в других местах можно безопасно получать указатель на него, одалживать (borrow).

collections::string::String это тип с владением. Это значит, что он имеет исключительное право владения содержимым стоки. Когда объект такого типа покидает свою область видимости (scope), строка освобождается. Поэтому, любая подстрока не может иметь тип String, потому что между строкой и подстрокой не будет связи, и когда первая покинет свою область видимости, вторая станет некорректной. Вместо этого, подстроки (или строковые срезы) используют тип, который является ссылкой на объект, которым владеет кто-то другой — &str. Rust, благодаря концепции времени жизни объекта, способен гарантировать, что ни один строковой срез не переживёт свою исходную строку, таким образом безопасность памяти сохраняется.

В гайде по времени жизни есть более детальное объяснение. Здесь же, если вы видите конструкцию 'такого_вида после ссылочного типа, знайте, это что так определяется время жизни ссылки. Есть специальное время жизни 'static, которое означает, что объект существует в течение всей работы программы. Такие объекты запекаются прямо в исполняемый файл, также как и строковые литералы, которые встречаются в коде — то есть тип строкового литерала &'static str.

Ранее, когда тип ~T был тем, чем сейчас является Box<T>, а str был фейковым, тип ~str представлял собой строковой тип изменяемого размера. Он хранил текущий (size) и максимальный (capacity) размер — как нынешний тип String (который заменил ~str). Предполагалось, что все типы-обёртки будут работать таким образом. Сейчас, Box<T> это простое обёрнутое значение. Вот почему он не используется — не имея дополнительной ёмкости, ему бы потребовалось перевыделять память каждый раз при дописывании в строку. String умеет перевыделять память и делает это по умолчанию. Поэтому разница между Box<str> и &str существенна.

Могу добавить, что во время этого изменения новый тип носил имя StrBuf. На самом деле ситуация не сильно отличается от таковой в других языках. В действительности, это влияние отсутствия обязательной сборки мусора, которая делает некоторые применения &str бестолковыми. В Rust вам придётся обращаться к строковому буферу несколько чаще, чем в других языках, просто потому что другие языки позволяют вам обращаться со своим основным строковым типом более легкомысленно.

Вернёмся к FizzBuzz

То есть, проблема в том, что в одной ветке мы имеем строку с владением, а строки в трёх других являются просто статическими строковыми срезами (ссылками на статически определённые строки). Как же нам разрешить эту проблему? Может попробуем сделать их все строковыми срезами (да-да, тип &str любого времени жизни 'a мы можем неявно привести к 'b, если 'a дольше, чем 'b. Так как 'static дольше, чем что-либо, компилятор может свободно преобразовать его к подходящему времени жизни):

for i in range(1i, 101) {
    let result = if i % 15 == 0 {
        "FizzBuzz"
    } else if i % 5 == 0 {
        "Buzz"
    } else if i % 3 == 0 {
        "Fizz"
    } else {
        i.to_string().as_slice()
    };
    println!("{}", result);
}

Выглядит как хорошая идея, да? Простите, это тоже не сработает:

f.rs:10:9: 10:22 error: borrowed value does not live long enough
f.rs:10         i.to_string().as_slice()
                ^~~~~~~~~~~~~
f.rs:2:25: 13:2 note: reference must be valid for the block at 2:24...
f.rs:2 for i in range(1i, 101) {
f.rs:3     let result = if i % 15 == 0 {
f.rs:4         "FizzBuzz"
f.rs:5     } else if i % 5 == 0 {
f.rs:6         "Buzz"
f.rs:7     } else if i % 3 == 0 {
       ...
f.rs:9:12: 11:6 note: ...but borrowed value is only valid for the expression at 9:11
f.rs:9     } else {
f.rs:10         i.to_string().as_slice()
f.rs:11     };
error: aborting due to previous error

Здесь мы упираемся во время жизни: строка, порождённая в i.to_string() не хранится достаточное время и освобождается в конце блока. Таким образом, ссылка на неё также не может покинуть блок. Это потенциальный баг, связанный с ссылкой на невалидную память, который компилятор Rust успешно поймал. В некоторых языках это называется «висящий указатель» и это Очень Плохо.

Здесь мы можем просто поднять строковую переменную за блок, нам достаточно, чтобы строка была валидной в течение тела цикла. Иногда вы будете сталкиваться с ситуациями, в которых этого будет достаточно, но зачастую — нет.

for i in range(1i, 101) {
    let x;
    let result = if i % 15 == 0 {
        "FizzBuzz"
    } else if i % 5 == 0 {
        "Buzz"
    } else if i % 3 == 0 {
        "Fizz"
    } else {
        x = i.to_string();
        x.as_slice()
    };
    println!("{}", result);
}

Размещаем ссылку в охватывающем блоке, это работает.

Как насчёт того, чтобы сделать всё типом String?

Мы можем пойти и в обратном направлении, обязав все ветви возвращать строки с владением:

for i in range(1i, 101) {
    let result = if i % 15 == 0 {
        "FizzBuzz".to_string()
    } else if i % 5 == 0 {
        "Buzz".to_string()
    } else if i % 3 == 0 {
        "Fizz".to_string()
    } else {
        i.to_string()
    };
    println!("{}", result);
}

Делаем всё строками, но не бесплатно для рантайма.

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

Напишем функцию

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

Начнём с такого кода:

Функция fizz_buzz, возвращающая String.
Python Rust
def fizz_buzz(i):
    if i % 15 == 0:
        return 'FizzBuzz'
    elif i % 5 == 0:
        return 'Buzz'
    elif i % 3 == 0:
        return 'Fizz'
    else:
        return i
 
for i in range(1, 101):
    print(fizz_buzz(i))

fn fizz_buzz(i: int) -> String {
    if i % 15 == 0 {
        "FizzBuzz".to_string()
    } else if i % 5 == 0 {
        "Buzz".to_string()
    } else if i % 3 == 0 {
        "Fizz".to_string()
    } else {
        i.to_string()
    }
}
 
for i in range(1i, 101) {
    println!("{}", fizz_buzz(i));
}

Теперь у нас есть дополнительный уровень инкапсуляции. Он демонстрирует как раз тот случай, когда решение с вынесением переменной на уровень выше не будет работать, потому что переменная будет покидать саму функцию.
(Можете попробовать сами; возвращаемое значение функции нельзя представить в системе типов Rust, так как нет подходящего времени жизни — x не получит время жизни 'static, и нет ничего, к чему мы могли бы его привязать.)

Также, так как мы поместили код в функцию, мы выделяем новые строки для тех случаев, когда это не нужно.

Введём SendStr

К счастью, Rust поддерживает алгебраические типы данных (также известные как enum). А также, в стандартной библиотеке есть подходящий тип, который может описать объект, являющийся либо строковым срезом, либо строкой с владением.

Ниже приведено определение такого типа (без описания методов, которые делают его ещё более полезным):

pub enum MaybeOwned<'a> {
    Slice(&'a str),
    Owned(String)
}

pub type SendStr = MaybeOwned<'static>;

Определения MaybeOwned и SendStr из std::str.

Send это ограничение, которое указывает, что объект можно безопасно пересылать между задачами (то есть между потоками, при этом не теряя безопасность по памяти); это также подразумевает, что объект самодостаточен, и может быть возвращён из функции. Пусть есть строка типа &'static str, как в определении SendStr; она не содержит ссылок на какие-либо объекты внутри функции, не так ли? Следовательно, она может существовать столько, сколько потребуется. То же самое верно и для String. Поэтому любой из этих двух объектов может быть захвачен внутри enum-типа, который говорит, что мы владеем одним или другим объектом. Следовательно SendStr удовлетворяет условию Send. Этот тип хранит в себе некоторое значение и пользователь может выполнять над ним разные операции. Сейчас самый примечательный факт в том, что мы можем извлекать строковой срез из этого типа с помощью as_slice(). Данный тип также реализует std::fmt::Show, что означает, что мы можем использовать его в форматированном выводе напрямую, указывая {} (типаж Show это прямой аналог __str__() в Python или to_s(), toString(), &c в других языках, но он работает напрямую с объектом writer, что позволяет избавиться от промежуточного строкового объекта. Вызов to_string() на любом типе, реализующем Show также вызывает этот механизм).

Вот как выглядит применение:

use std::str::SendStr;
 
fn fizz_buzz(i: int) -> SendStr {
    if i % 15 == 0 {
        "FizzBuzz".into_maybe_owned()
    } else if i % 5 == 0 {
        "Buzz".into_maybe_owned()
    } else if i % 3 == 0 {
        "Fizz".into_maybe_owned()
    } else {
        i.to_string().into_maybe_owned()
    }
}

for i in range(1i, 101) {
    println!("{}", fizz_buzz(i));
}

Функция fizz_buzz возвращает SendStr. Это работает.
(.into_maybe_owned() взята из IntoMaybeOwned и доступна по умолчанию)

Круто! Теперь мы уменьшили количество работы, которую нужно выполнять компьютеру и сделали наш общеизвестный пример быстрей.
Но можем ли мы пойти дальше?

Написание собственного enum-типа и реализация типажа std::fmt::Show

Конечно, то, что мы передаём на самом деле не является «строкой», это некоторые значения «Fizz», «Buzz», «FizzBuzz», либо число. Мы просто преобразовали все варианты в строку заранее; мы можем запросто сделать это лениво, избегая лишних аллокаций (в действительности, всех аллокаций здесь можно избежать).

Давайте сделаем собственный enum.
В процессе также реализуем std::fmt::Show для него, что позволит выводить его напрямую в stdout, без необходимости в промежуточной строке.

Применение изолированного типа данных для эффективного представления возможных вариантов значения.
аналог на Python (чрезвычайно натянутый) Rust
class FizzBuzzItem:
 
    def __init__(self, value):
        self._value = value
 
    def __str__(self):
        if self is Fizz:
            return "Fizz"
        elif self is Buzz:
            return "Buzz"
        elif self is FizzBuzz:
            return "FizzBuzz"
        else:
            return str(self._value)
 
# притворимся, что эти типы непрозрачны
Fizz = FizzBuzzItem(object())
Buzz = FizzBuzzItem(object())
FizzBuzz = FizzBuzzItem(object())
 
def Number(number):
    return FizzBuzzItem(number)
 
def fizz_buzz(i):
    if i % 15 == 0:
        return FizzBuzz
    elif i % 5 == 0:
        return Buzz
    elif i % 3 == 0:
        return Fizz
    else:
        return Number(i)
 
for i in range(1, 101):
    print(fizz_buzz(i))

use std::fmt;
 
enum FizzBuzzItem {
    Fizz,
    Buzz,
    FizzBuzz,
    Number(int),
}
 
impl fmt::Show for FizzBuzzItem {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            Fizz => f.write(b"Fizz"),
            Buzz => f.write(b"Buzz"),
            FizzBuzz => f.write(b"FizzBuzz"),
            Number(num) => write!(f, "{}", num),
        }
    }
}
 
fn fizz_buzz(i: int) -> FizzBuzzItem {
    if i % 15 == 0 {
        FizzBuzz
    } else if i % 5 == 0 {
        Buzz
    } else if i % 3 == 0 {
        Fizz
    } else {
        Number(i)
    }
}
 
for i in range(1i, 101) {
    println!("{}", fizz_buzz(i));
}

Обратите внимание, что это действительно хороший способ представления данных, хотя мы могли не заморачиваться так сильно в этом случае и просто заменить первые три ветви типом Word(&'static str): Word("FizzBuzz") итд. (По правде, это была первая версия, которую я написал на этом шаге. Даже я был повёрнут на использовании строк там, где этого не требуется!)

Мы могли бы пойти и дальше, написав отдельный итератор, но, учитывая то, как работают итераторы в Rust, это совсем необязательно — можно просто написать range(1, 101).map(fizz_buzz). Это даст намного больше гибкости. Как только где-то будет реализован Iterator<int>, можно будет просто дописать .map(fizz_buzz) в конец и вы получите тип, реализующий Iterator<FizzBuzzItem>.

Цикл можно переписать в этом стиле на раз-два:

Применяем функцию fizz_buzz по итератору целых чисел.
Python Rust
for f in map(fizz_buzz, range(1, 101)):
    print(f)

for f in range(1, 101).map(fizz_buzz) {
    println!("{}", f);
}

Какой бы из способов мы не выбрали, в результате мы получим старый-добрый выхлоп программы FizzBuzz.

Заключение

Теперь вы знаете почему ваша первая реализация FizzBuzz на Rust могла бы не работать. Некоторые из затруднений, описанных в статье, типичны для статически-типизированных языков, некоторые относятся к специфике Rust. (В действительности, ситуация аналогична той же в C++, за той разницей, что C++ позволит вам сделать кучу глупых ошибок и не даёт каких-либо гарантий работы с памятью. Не спорьте со мной по этому поводу, здесь я лишь цитирую других людей, я не знаю C++ в должной степени.)

Мы прошлись по теме модели владения в Rust, и тому как она может помешать вам писать в том стиле, к которому вы привыкли, и почему так (правда, без описания конкретных преимуществ). Также мы упомянули эффективную концепцию enum-типов (алгебраических типов данных), которая позволяет описывать данные более строго и эффективно.

Надеюсь, вы увидели силу всех этих вещей, и она вас заинтересовала.

Является ли описанное дополнительной смысловой нагрузкой? Да.

Это неприятно? Периодически. (Мой опыт говорит, что всё это спасает от затруднений также часто, как и создаёт их.)

Позволяет ли это улучшить эффективность ваших программ? Безусловно, и с полной уверенностью. Раньше эти вещи требовали потери безопасности и корректности, теперь в Rust, вам не требуются подобные компромиссы.

Позволяет ли это упростить пронимание кода? В простых случаях, как этот, особой разницы не видно, но в сложных эти механизмы становятся реальной помощью. (Мне правда недостаёт их в Python.)

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

Следует ли вам использовать Rust? Чтож, я предлагаю хотя бы попробовать его. Вы можете найти его сырым или непригодным для ваших целей из-за его акцента на системном программировании. Для многих высокоуровневых задач он может оказаться несколько громоздким. Но я верю, что придёт время, и это будет классный инструмент для вещей, вроде веб-программирования, о чём я говорил на докладе в StrangeLoop (можете также посмотреть слайды, 2MB SVG).

Наконец, если вы слабо знакомы с Rust или не поняли какую-то часть статьи, я предлагаю вам ознакомиться с официальной документацией; тридцатиминутное введение в Rust описывает концепцию владения достаточно хорошо, а в Гайде хорошо раскрыты enum-типы и многое другое. Также есть более детализованные гайды по конкретным вопросам. Если у вас всё ещё остались вопросы, места вроде канала #rust на irc.mozilla.org могут здорово помочь — я подолгу нахожусь там, мой ник ChrisMorgan.

Ну а если вы по-настоящему любите возиться с оптимизацией FizzBuzz

Да пожалуйста. Это финальная версия, с минимальными поправками, необходимыми, чтобы компилироваться с соверменной версией Rust, и строковой версией OUT для улучшенной читаемости (!?):

#![no_std]
#![feature(asm, lang_items)]
 
extern crate libc;
 
static OUT: &'static [u8] = b"
    1n2nFizzn4nBuzznFizzn7n8nFizznBuzzn11nFizzn13n14nFizzBuzzn
    16n17nFizzn19nBuzznFizzn22n23nFizznBuzzn26nFizzn28n29nFizzBuzzn
    31n32nFizzn34nBuzznFizzn37n38nFizznBuzzn41nFizzn43n44nFizzBuzzn
    46n47nFizzn49nBuzznFizzn52n53nFizznBuzzn56nFizzn58n59nFizzBuzzn
    61n62nFizzn64nBuzznFizzn67n68nFizznBuzzn71nFizzn73n74nFizzBuzzn
    76n77nFizzn79nBuzznFizzn82n83nFizznBuzzn86nFizzn88n89nFizzBuzzn
    91n92nFizzn94nBuzznFizzn97n98nFizznBuzzn";
 
#[start]
fn start(_argc: int, _argv: *const *const u8) -> int {
    unsafe {
        asm!(
            "
            mov $$1, %rax
            mov $$1, %rdi
            mov $0, %rsi
            mov $$0x19d, %rdx
            syscall
            "
            :
            : "r" (&OUT[0])
            : "rax", "rdi", "rsi", "rdx"
            :
        );
    }
    0
}
 
#[lang = "stack_exhausted"] extern fn stack_exhausted() {}
#[lang = "eh_personality"] extern fn eh_personality() {}
#[lang = "fail_fmt"] extern fn fail_fmt() {}

Примечания переводчика:
1. Rust имеет развитую систему макросов, в данном случае println! в compile-time разворачивается в специализированный под конкретный тип вызов println.
2. При первом прочтении оригинала может сложиться впечатление, что идёт речь об управляемом коде, однако, здесь имеется ввиду управляемая память. Несмотря на различные формулировки внутри скобок и вне, речь идёт об одном и том же.

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

Автор: StreetStrider

Источник

Поделиться

* - обязательные к заполнению поля