Обновить

Комментарии 21

Классика – неэффективное решение на Rust сравнивают с эффективным на другом языке.

> Компромисс Раста в том, что язык агрессивно диктует вам архитектуру

Бред.

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

А еще авторов zig (и ржавчины тоже) можно отправить почитать про memory pools and resource management, в такие проекты как APR, postgresql (там они называются Context, Resource Owners) и т.д.

Хотя зачем... как тогда отделять С от .. как там было сказано - "адекватных" :) его замен?

Защита от переполнения буфера - это не выравнивание и не оборачивание защитными страницами, а контроль размера записываемых в него данных.

И каким образом это делать в общем случае, контроль этот? Встроенные в язык и его runtime проверки это очень хорошо, но память в процессе общая, при желании кто угодно может писать куда угодно даже в случае Java, если только нет защиты памяти на запись на уровне OS (которую впрочем так-же легко выключить, как и включить).

Cуперзащищеннный на уровне языка и runtime буфер может быть поврежден просто вызовом сторонней функции, в новой версии которой, недавно полученной по dnf update вдруг внезапно появился вызов sprintf()

Ну если вы не знаете какие функции у вас в программе куда пишут, то я даже не знаю...

Наивно думать, что С программисты не контролируют память и свои буферы и прочие целочисленные переполнения. Большинство CVE построены на том, что пробивается лишь одна из функций, часто библиотечная, и дальше вся система контроля падает как карточный домик. Это справедливо для любого runtime, в любой среде.

Отличие подхода Zig, Rust и подобных от С лишь в том, что они принуждают контролировать диапазоны значений даже там, где это реально и не нужно.

К примеру изначальный код на С (изначально со всеми нужными проверками) будет в теории дописан иначе (второй кусок). Примеры ниже утрированные и наверняка там будет оптимизация, излишние проверки выбросит компилятор, как минимум когда-нибудь, но сути это не меняет - то что для Rust, Zig нормик, любой программист на C, мягко говоря, не поймет и заставит переписать подобный PR оптимально.

// "Неправильный" подход

#define N 12
char data[N] = "hello world";

for (int i = 0; i < N; i++) {
    if (data[i] == 'e') {
        data[i] = 'E';
    }
}


// А теперь мы покажем как на самом деле надо программировать

#define N 12
char data[N] = "hello world";

for (int i = 0; i < N; 
    IIF(i < MAX_INT; 
        i++; 
        panic("integer overflow"
    ) 
) { // да, но i никогда не cможет быть более N=12)
    if (
        IIF(
            data != NULL && i >= 0 && i < N; 
            data[i] == 'e'; 
            panic("buffer index out of range")
        )
    ) {
      // да,  но i никогда не cможет быть <0 и > N=12), data всегда != NULL, но нет, еще раз
        IIF(
            data != NULL && i >= 0 && i < N; 
            data[i] = 'E';
            panic ("buffer index out of range")
        )
    }
}
  • Люк, используй итераторы

  • И не используй знаковые типы для индексации

  • Проверки целочисленного переполнения by default есть только в дебаг версии. В gcc можно с тем же успехом -fsanitize добавить, только оно по умолчанию отключено. (Кстати, а в ваших проектах есть -fwrapv, или живёте в мире UB?)

  • Если data не является сырым указателем то он имеет NonNull тип.

  • Если вы готовы зуб отдать, гарантируя что инварианты не будут нарушены - напишите unsafe блок, с get_unchecked

  • Итераторы для С? внимательно слушаю

  • Знаковые типы для индексации? а иначе что? вот серьезно? Я могу привести примеры как раз обратного, что безнаковый size_t это опасное заблуждение.

  • Включать проверку на целочисленное переполнение для C по-умолчанию не верно, к примеру алгоритмы расчета MD5 на целочисленном переполнении и построены (для самого было сюрпризом). UB не UB, но из песни слов не выкинуть, они реализованы именно так

  • Ммм, честно говоря, не совсем понятно о чем речь.

  • Зачем? Я просто продолжу и дальше пользовать C, не расходуя зубы и нервы, все ходы известны и записаны. Его, если не ошибаюсь, уже 30-ть лет все пытаются заменить - C++, Java, C#, D, C--, Vala, Scala, Rust, Zig (да да, на этот раз точно получится), а он зараза, все никак не заменяется :) Парадокс? Отнюдь, там фундаментальная закономерность.

    RustOS и ZigOS кстати еще не написали? JavaOS уже была, C#.OS тоже (Singularity)

Как я понял, вторая часть вашего сниппета - это утрированный пример того, как выглядит аналог ржавому коду, если явно все проверки прописать. Так вот,

  • В расте итераторы есть и активно используются.

  • Как минимум - избавитесь от проверки i >= 0.

  • Да, значительная часть сишного кода такое не переживет. И да - реализации многих алгоритмов полагаются на то, что при переполнении будет выполнено приведение по модулю. И если они используют беззнаковый тип, то это не UB и 100% соответствие стандарту си.

  • Cистема типов и инварианты раста гарантируют, что data (из вашего примера) никогда не будет null. Соответственно - никаких rt проверок.

  • Просто так? Ничего против C или плюсов не имею. Но распробовав раст, возвращаться к ним не хочется.

Как минимум Redox написали…

И кстати, из любопытства погонял компилятор, так вот. Если первый пример кода на rust 1:1 переписать, то, при включенных оптимизациях

  • если N - константа, то цикл размотается вообще без проверок

  • если N - rt-переменная, то компилятор вынесет все проверки перед телом цикла

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

#define N 12
char data[N] = "hello world";

for (int i = 0; i < N; i++) {
    if (data[i] == 'e') {
        data[i] = 'E';
    }
}

и что здесь неправильного?

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

Ну так языки-альтернативы С/С++ появились как результат признания факта, что количество ошибок работы с памятью слабо коррелирует с квалификацией программиста. Потому что помимо пролома буфера есть use-after-free, double free, работа с неициализированными переменными в редко исполняющихся ветках программы и т.д. Да, какая-то часть таких ошибок предотвращается полезными привычками и дисциплиной самого программиста + разным тулингом в виде санитайзеров и статических анализаторов, но это не убирает главную причину таких ошибок - снижение внимательности и концентрации. Да и эволюционно человеческие мозги заточены на то, чтобы эффективно работать только с тем, что непосредственно перед глазами. Если в языке нет явного выражения владения и заимствования, а только неявное, в виде специфических "соглашений " и паттернов, то в какой-то момент кодовая база перестанет "влазить" в окно фокуса внимания разработчика и он так или иначе накосячит.

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

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

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

Какой хейт? Эта функциональность начиналась ещё с Паскаля. А может и раньше, не помню.

только языки со строгой статической типизацией признаются подходящими для промышленной разработки

Потому что у других проблемы с производительностью.

Потому что у других проблемы с производительностью.

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

И то - тот-же Facebook до сих пор пишет себе на PHP, и отказываться от него не собирается. Хотя Google с Yandex вполне себе использует C++ для ключевых сервисов, потенциальные пробои буферов, целочисленные переполнения, UB и утечки памяти их почему-то не смущают.

Информация прямо от совета директоров?

Или знакомые СТО рассказали?

Не обязательно впадать в крайности - плюсы или пхп, достаточно промежуточных нормальных вариантов - от Явы до Го. Это я про Веб, на дескопе тем более другой расклад

Потому что помимо пролома буфера есть use-after-free, double free, работа с неициализированными переменными в редко исполняющихся ветках программы

Неинициализированные переменные элементарно ловятся линтером или даже штатно диагностикой clang/gcc, упоминать про такое это не серъезно.

use-afte-free, double free - это тяжелое наследие работы с HEAP через malloc/free - я уже выше упоминал APR Pools или Contexts, они были как раз специально придуманы против этого, и в т.ч. с целью получить бенефиты от идей Garbage Collector в части скорости выделения памяти, и получить правильное освобождение памяти при завершении Запроса, Сессии, Процесса.

Про них правда мало кто знает, а напрасно. Взять любой проект с претензией на многодневный runtime, т.е. если это не короткоживущая консольная утилита, и поискать там вызовы free/malloc/new/delete, GC или ARC - то... все, пожалуй сразу пиши пропало, эти люди скорее всего просто пока еще не умеют писать действительно отказоустойчивый и защищенный код, бывают исключения конечно, да и перезапускать сервисы раз в день или даже каждый час тоже, наверное, можно.

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

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

Подход mtproto.zig: Epoll + State Machine ]Старт сервера -> Предвыделяем массив слотов в памяти один разНовый клиент -> Берем свободный слот (O(1)) -> Читаем асинхронноКлиент отвалился -> Возвращаем слот в массив

Что мешает сделать ровно это же на С?

Если клиенты не отваливаются и их становится больше предвыделенного массива слотов -- тогда что?

Если клиенты не отваливаются и их становится больше предвыделенного массива слотов -- тогда что?

Если клиенты не отваливаются - то это DDoS, или звонок, что пора ставить haproxy или nginx перед appserverом, если это проблема медленных клиентов на 3G соединении.

А так - в современной 64битной архитектуре линейная память практически не ограничена, если брать условные 10к клиентов, то можно просто зарезервировать через mmap() достаточно большой участок памяти под слоты, допустим в гигабайт размером, но не выделять его сразу (reserve vs commit).

И операционная система будет сама выделять виртуальную память по мера роста размера массива, начав с 4к (страница) и далее по потребности.

Это как раз те самые Memory pools, Contexts, Arenas в действии.

Вообще вопрос провокационный - так или иначе, все, что на Rust в части джедайских веб техник можно сделать или что там считается сейчас особо прогрессивным - уже или реализовано было на С ранее, просто не так популяризовано, или вполне реализуется относительно небольшими усилиями. libreactor и h2o не дадут соврать: https://www.techempower.com/benchmarks/#section=data-r23&test=json

Ее круто. Давай ещё. Туда эту семейку Сишную. Сегфолт теперь шиза дедов присмерти. Ееее

ну я могу так же сравнить 1 клон майнкрафта со своим - его на на зиг мой на раст, и у него и у меня бесконечные миры, вчера я потестил сборку мне планировщик нарисовал 4 гигабайта ), а моя сборка, 200-300 мегабайт, потом тоже интересно нагрузка будет расти, слоты покроют ну тысячу или 500, и вот уже будет близко даже к яве и это на зиг и раст. На самом деле и Раст и Зиг почти одинаково работают, то что вы описали слоты и 2 канала можно и на расте написать...

покажу свой пример из недавнего как я пинговал свою фс, ну без слотов

Скрытый текст
use std::{
    collections::HashMap,
    fs::File,
    io::{BufRead, BufReader},
    os::unix::fs,
    sync::{
        Arc,
        mpsc::{self, Receiver, Sender},
    },
    thread,
};

//
fn filer_test(file1: &str, st: &str) {
    let file = File::open(file1).expect("msg");
    let reader = BufReader::new(&file);
    // let y = file.metadata().expect("msg").is_file();
    let mut chanell: Chanell = Chanell::new();
    chanell.recieve();
    // if y {
    for (l, line) in reader.lines().enumerate() {
        if let Ok(line_content) = line {
            if line_content.contains(st) {
                let data = Res1::new(file1.to_string(), line_content);
                chanell.spawn(data);
                thread::sleep(std::time::Duration::from_millis(200));
            }
        }
    }
    // }
}

fn test_filler(p: &str, f: &str) {
    let files = std::fs::read_dir(p)
        .expect("msg")
        .filter_map(|e| e.ok())
        .filter(|e| e.path().is_file())
        .collect::<Vec<_>>();
    for i in files {
        let r = String::from(p) + i.file_name().into_string().expect("msg").as_str();
        filer_test(r.as_str(), f);
    }
}

struct Chanell {
    sender: Sender<Arc<Res1>>,
    receiver: Option<Receiver<Arc<Res1>>>,
}

impl Chanell {
    fn new() -> Self {
        let (tx, rx) = mpsc::channel();
        Self {
            sender: tx,
            receiver: Some(rx),
        }
    }

    fn spawn(&self, data: Res1) {
        let tx1 = self.sender.clone();
        let a = Arc::new(data);
        thread::spawn(move || {
            tx1.send(a).expect("Failed to send");
        });
    }
    fn recieve(&mut self) {
        if let Some(rx) = self.receiver.take() {
            thread::spawn(move || {
                // Теперь rx принадлежит этому потоку
                while let Ok(received) = rx.recv() {
                    for (key, value) in &received.n {
                        println!("Got Key: {}, Value: {}", key, value);
                    }
                }
                println!("Channel closed");
            });
        }
    }
}
struct Res1 {
    n: HashMap<String, String>,
}

impl Res1 {
    fn new(st: String, st1: String) -> Self {
        let mut n1 = HashMap::new();
        n1.insert(st, st1);
        Self { n: n1 }
    }
}
fn main() {
    let args: Vec<String> = std::env::args().collect();

    // ./app ~/ apt - директория с закрывашкой /
    if args.len() == 3 {
        let first_arg = &args[1];
        let second_arg = &args[2];
        println!("{} {}", first_arg, second_arg);
        test_filler(first_arg, second_arg);
    }
}

тут 1 канал, а по вашему примеру для флексити нужно 2 канала, там кстати на С как не раскручивайся даже с калбеками память отестся, и мой бинарник который я привел весит 600 килобайт).

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации