LINUX.ORG.RU

Дискуссия об использовании языка C++ для разработки ядра Linux

 ,


1

5

В списке рассылки разработчиков ядра Linux возобновилось начатое шесть лет назад обсуждение перспектив использования современного кода на C++ в ядре Linux, помимо нынешнего применения языка Си с ассемблерными вставками и продвижения языка Rust. Изначально тема разработки ядра на C++ была поднята в 2018 году инженером из Red Hat, который первого апреля в качестве шутки опубликовал набор из 45 патчей для использования шаблонов, наследуемых классов и перегрузки функций C++ в коде ядра.

С инициативой продолжения обсуждения выступил Ганс Питер Анвин (Hans Peter Anvin), один из ключевых разработчиков ядра в компании Intel и создатель таких проектов как syslinux, klibc и LANANA, разработавший для ядра Linux систему автомонтирования, реализацию RAID 6, драйвер CPUID и x32 ABI. По мнению Анвина, который является автором многочисленных макросов и ассемблерных вставок в ядре, с 1999 года языки C и C++ значительно продвинулись вперёд в своём развитии и язык C++ стал лучше, чем С, подходить для разработки ядра операционных систем.

Возможности, для которых ещё недавно приходилось привлекать специфичные GCC-расширения, теперь легко реализовать на стандартном C++, и во многих случаях использование C++ позволит улучшить инфраструктуру без глобального изменения кода. В качестве минимальной упоминается использование спецификации C++14, которая включает необходимые средства метапрограммирования, а в качестве желаемой - использование спецификации C++20, в которой появилась поддержка концепций, способных исключить появление многих ошибок.

Анвин считает, что C++ более предпочтителен, чем Rust, так как последний существенно отличается от языка С по синтаксису, непривычен для текущих разработчиков ядра и не позволяет постепенно переписывать код (в случае языка С++ можно по частям переводить код с языка C, так как С-код можно компилировать как C++). В поддержку использования С++ в ядре также выступили Иржи Слаби (Jiri Slaby) из компании SUSE и Дэвид Хауэллс (David Howells) из Red Hat.

>>> Подробности

★★★

Проверено: hobbit ()
Последнее исправление: hobbit (всего исправлений: 1)
Ответ на: комментарий от sadko4u

забыть где-нибудь поймать эти исключения и словить kernel panic?

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

Исключения в ядро пока тащить никто не предлагает вроде.

Но даже если рассматривать исключения в случае ядра, то если ты забыл обработать исключение, то точно так же (скорее более вероятно) забудешь обработать код возврата. И kernel panic куда лучше расстрела памяти, она хотя бы точно ничего не повредит.

учитывая что у С++ исключений даже stack trace точки, в которой они были сгенерированы, получить нельзя

Можно. std::stacktrace/boost::stacktrace в помощь.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Но даже если рассматривать исключения в случае ядра, то если ты забыл обработать исключение, то точно так же (скорее более вероятно) забудешь обработать код возврата.

Спецификацию исключений удалили из стандарта.

Аттрибуты требующие обработку кодов возврата ввели.

Предупреждения на пропуск в switch возможных вариантов работают.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 1)
Ответ на: комментарий от x86_64

Аттрибуты требующие обработку кодов возврата ввели.

[[nodiscard]] из C23 в ядро попадёт лет через 10 в лучшем случае, так что считай его там нет. А когда попадёт его ещё везде добавить надо будет, что тоже не мало времени займёт.
Но в целом движение хорошее.

Предупреждения на пропуск в switch возможных вариантов работают.

Это подразумевает, что для каждой функции будет создаваться свой enum, содержащий только то, что может вернуть эта функция. При этом она может вернуть что-то сразу из другой функции, т.е. встретить такое в реальном коде практически невозможно.
По сути, если ты обрабатываешь код возврата через switch, то у тебя обязан быть default, иначе что-то пропустишь.

Спецификацию исключений удалили из стандарта.

catch(some_base_exception& ex) перехватит всё, что вылетит (при условии, что все исключения наследуются от some_base_exception). Т.к. это что-то не ожидавшееся тобой, то трактовать его надо как невосстановимое исключение.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

[[nodiscard]]

До этого момента можно пользоваться. [[gnu::warn_unused_result]]

clang то же умеет.

По сути, если ты обрабатываешь код возврата через switch, то у тебя обязан быть default, иначе что-то пропустишь.

Это то же обрабатывается. Да. Для каждой функции с несколькими вариантами отказов придется свой enum писать. Но в них можно константы со сквозной нумерацией. И да. Большинство функций возвращают только один вариант отказа, который и надо обработать.

catch(some_base_exception& ex) перехватит всё, что вылетит (при условии, что все исключения наследуются от some_base_exception). Т.к. это что-то не ожидавшееся тобой, то трактовать его надо как невосстановимое исключение.

Проблема как всегда не в написании кода с исключениями. А в рефакторинге кода, у которого глубоко внутри исключения. Разработчики gcc не зря от использования исключений при разработке gcc отказались. Для больших проектов их применять нецелесообразно, а ядро - большой проект.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 6)
Ответ на: комментарий от x86_64

До этого момента можно пользоваться. [[gnu::warn_unused_result]]

Согласен. В ядре для этого __must_check предусмотрен и даже используется. Ок, часть про «скорее более вероятно» снимается.

придется свой enum писать.

Ну вот в ядре все функции, помеченные __must_check возвращают int’ы, да разного рода long’и. Enum’ов я там не видел. Да и вообще в сишном коде возврата ошибок в виде enum’ов я не припомню, т.е. не сильно распространённая практика, судя по-всему.

А рефакторинге кода, у кторого глубоко внутри исключения.

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

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

Ivan_qrt ★★★★★
()
Ответ на: комментарий от x86_64

Это как раз особенность Си.

Да в плюсах так же.

Так а ты имел ввиду, что при обработке кода полученный int к какому-то enum’у кастуется? Или как проверку в switch получить?

Ivan_qrt ★★★★★
()
Ответ на: комментарий от x86_64

Ок, понял.

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

Ну и в случае проброса ошибки из другой функции с другим возвращаемым enum class будет что-то типа:

OtherFuncErrorEnum rc = other_func();
if (rc != OtherFuncErrorEnum::Success) {
    return static_cast<ThisFuncErrorEnum>(rc);
}

Что при рефакторинге и добавлении нового варианта в OtherFuncErrorEnum приведёт к утечке варианта без ошибок/предупреждений.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Что при рефакторинге и добавлении нового варианта в OtherFuncErrorEnum приведёт к утечке варианта без ошибок/предупреждений.

Тут как раз switch надо. С предупреждением при компиляции. А не if.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 1)
Ответ на: комментарий от x86_64
OtherFuncErrorEnum rc = other_func();
switch (rc) {
    case OtherFuncErrorEnum::Success: break;
    case OtherFuncErrorEnum::Err1:    return ThisFuncErrorEnum::Err1;
    case OtherFuncErrorEnum::Err2:    return ThisFuncErrorEnum::Err2;
    case OtherFuncErrorEnum::Err3:    return ThisFuncErrorEnum::Err3;      
}

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

Исключения всё-таки выглядят по надёжнее.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Можно такой вариант.

OtherFuncErrorEnum rc = other_func();

switch (rc) {

case Success: break;

case Err1:
case Err2:
case Err3:

    return static_cast<ThisFuncErrorEnum>(rc);

}

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

Эта дисциплина компилятором внедряется.

x86_64 ★★★
()
Последнее исправление: x86_64 (всего исправлений: 2)
Ответ на: комментарий от x86_64

Можно такой вариант.

Можно, но тогда значения ошибок между enum’ами должны совпадать, т.е. нужен глобальный реестр кодов возврата (предполагаю, что как правило он есть). Ну и он особо ничего не упрощает.

Эта дисциплина компилятором внедряется.

Я имел ввиду, что разработчик должен всё это писать вместо простого if’а и static_cast’а. Должен своевременно обновлять все значения и т.п. Всё это можно обойти менее многословными методами, т.е. требуется дисциплина именно от разработчика/ревьювера.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Можно. std::stacktrace/boost::stacktrace в помощь.

А как-нибудь без костылей, которыми надо подпирать исключения, обойтись нельзя?

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

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

sadko4u ★★
()
Ответ на: комментарий от sadko4u

А как-нибудь без костылей, которыми надо подпирать исключения, обойтись нельзя?

А в чём собственно разница? Если бы в плюсах это же решение (std::stacktrace) выводило бы stacktrace для необработанных исключений ты бы уже не считал это костылями? Или в каком-нибудь расте оно по-твоему технически как-то по другому сделано?

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

т.е. нужен глобальный реестр кодов возврата (предполагаю, что как правило он есть) Ну и он особо ничего не упрощает.

А мне нравится:

enum class AllResultCodes {
	AAA,
	BBB,
	CCC,
};
enum class FResult {
	AAA = (int)AllResultCodes::AAA,
	BBB = (int)AllResultCodes::BBB,
};
enum class GResult {
	CCC = (int)AllResultCodes::CCC,
};

FResult f() { return FResult::AAA; }
GResult g() { return GResult::CCC; }

Правда задачу безопасного приведения FResult к GResult (если g() вызывает f()) эта фигня не решает. Чую, что решение есть, но думать лень.

UPD2: Например, можно нарисовать анализатор на clang libtooling, чтобы проверял, что если f() делает return g(), то элементы GResult были бы подмножеством элементов FResult. Гибко и не требует усложнения сорцов. И к слову, для работы такой проверки даже общий реестр кодов не нужен (хотя он нужен в самих сорцах, чтобы не получилось пересортицы).

UPD3: «если f() делает return g()» Точнее, натравливать libtooling на любые касты одного enum к другому.

dimgel ★★★★★
()
Последнее исправление: dimgel (всего исправлений: 6)
Ответ на: комментарий от dimgel

В принципе можно свой каст сделать, который будет проверять, что все варианты из кастуемого enum’а есть в целевом enum’е:

#include <iostream>
#include <magic_enum_utility.hpp>


enum class Err1: int {
    E1 = 1,
    E2 = 2,
#ifdef WITH_ERROR
    E7 = 7,
#endif
};

enum class Err2: int {
    E1 = 1,
    E2 = 2,
    E3 = 3,
    E4 = 4,
};

template <class T> concept Enum = std::is_enum_v<T>;

template <Enum DST, Enum SRC>
constexpr DST err_cast(SRC src) noexcept {
    magic_enum::enum_for_each<SRC>([](auto val) {
        constexpr SRC choice = val;
        static_assert(magic_enum::enum_contains<DST>(static_cast<std::underlying_type_t<SRC>>(choice)));
    });
    return static_cast<DST>(src);
}



int main() {
    Err1 err1 = Err1::E2;
    std::cout << magic_enum::enum_name(err1) << std::endl;
    Err2 err2 = err_cast<Err2>(err1);
    std::cout << magic_enum::enum_name(err2) << std::endl;
}

Сюда же спокойно и проверка наличия в глобальном реестре прикручивается.

Но это всё по-прежнему требует того, чтобы все разработчики делали именно так и не лепили где-то static_cast или ещё какие-то свои хотелки.

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

magic_enum::enum_contains

Гы, а я ведь когда-то это нагугливал. Но видимо не фштырило, раз забил и забыл.

// Меня вообще от шаблонной магии корёжит. В т.ч. как по мне, для задачи «проконтролировать то-то» решение «пишем тулзу, контролирующую то-то» – куда более прямолинейное и естественное, чем «нах@#вертим в сорцах гору нечитабельной дичи, чтобы компилятор, прожевав её, проконтролировал то-то ‘стандартными средствами’». Код пишется один раз, а читается тыщу.

Но это всё по-прежнему требует того, чтобы все разработчики делали именно так и не лепили где-то static_cast или ещё какие-то свои хотелки.

А внешний tidy-like анализатор не требует. :) Т.е. наоборот как раз требует, причём так, что его не обойдёшь – достаточно встроить его обязательным шагом в билд-скрипт.

dimgel ★★★★★
()
Последнее исправление: dimgel (всего исправлений: 2)
Ответ на: комментарий от dimgel

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

#include <iostream>
#include <magic_enum_utility.hpp>



template <class T> concept Enum = std::is_enum_v<T>;

template <Enum DST, Enum SRC>
consteval bool check_contains_all() noexcept {
    bool contains_all = true;
    magic_enum::enum_for_each<SRC>([&contains_all](auto val) {
        constexpr SRC choice = val;
        contains_all &= magic_enum::enum_contains<DST>(static_cast<std::underlying_type_t<SRC>>(choice));
    });
    return contains_all;
}

template <Enum DST, Enum SRC>
constexpr DST err_cast(SRC src) noexcept {
    static_assert(check_contains_all<DST, SRC>());
    return static_cast<DST>(src);
}



struct Err1 {
    enum class Error: int {
        Success = 0,
        E1 = 1,
        E2 = 2,
#ifdef WITH_ERROR
        E7 = 7,
#endif
    };

    const Error error;

    constexpr Err1(Error err) noexcept: error{err} {}
    constexpr Err1(std::underlying_type_t<Error> ec) noexcept: error{ec} {}

    constexpr operator bool() const noexcept { return error != Error::Success; } 
};



Err1 foo() { return Err1::Error::E2; }



struct Err2 {
    enum class Error: int {
        Success = 0,
        E1 = 1,
        E2 = 2,
        E3 = 3,
        E4 = 4,
    };

    const Error error;

    constexpr Err2(Error err) noexcept: error{err} {}
    constexpr Err2(std::underlying_type_t<Error> ec) noexcept: error{ec} {}

    constexpr Err2(Err1 err) noexcept: error{err_cast<Error>(err.error)} {}

    constexpr operator bool() const noexcept { return error != Error::Success; } 
};


Err2 bar() {
    Err2 err = foo();
    if (err) {
        return err;
    }
    // ...
    return Err2::Error::Success;
}



int main() {
    Err1 err1 = foo();
    std::cout << magic_enum::enum_name(err1.error) << std::endl;
    Err2 err2 = bar();
    std::cout << magic_enum::enum_name(err2.error) << std::endl;
}

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

Ivan_qrt ★★★★★
()
Ответ на: комментарий от dimgel

А внешний tidy-like анализатор не требует.

Ну я никогда так делать не пробовал и даже не представляю себе как к этому подступаться и какая там сложность. Но если оно прям надёжно может это контролировать и не мешать работать с другими enum’ами, не являющимися кодами возврата, то отлично.

чем «нах@#вертим в сорцах гору нечитабельной дичи, чтобы компилятор, прожевав её, проконтролировал то-то ‘стандартными средствами’». Код пишется один раз, а читается тыщу.

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

Ivan_qrt ★★★★★
()
Ответ на: комментарий от Ivan_qrt

Ну я никогда так делать не пробовал и даже не представляю себе как к этому подступаться и какая там сложность.

https://www.youtube.com/watch?v=yuIOGfcOH0k

У меня помечено, что AST Matchers @15:18.

Синтаксис с тех пор чуток поменялся (матчеры начинаются с lowerCase), но идея и даже ЕМНИП bootstrap-код – в точности те же.

Вообще парни шикарную хрень запилили: полноценный удобный pattern matching над AST – на языке, в котором этот самый pattern matching в принципе отсутствует.

Но если оно прям надёжно может это контролировать и не мешать работать с другими enum’ами, не являющимися кодами возврата, то отлично.

Ну дык сам tidy на libtooling и написан, об этом ЕМНИП есть в видео. Т.е. если ты можешь формализовать свои хотелки, то значит и на код их можно положить.

dimgel ★★★★★
()
Последнее исправление: dimgel (всего исправлений: 2)
Ответ на: комментарий от Ivan_qrt

Ещё немного поупарывался:

#include <iostream>
#include <magic_enum_utility.hpp>



enum class AllError: int {
    Success = 0,
    E1 = 1,
    E2 = 2,
    E3 = 3,
    E4 = 4,
    E7 = 7,
};


template <class T> concept Enum = std::is_enum_v<T>;

template <Enum DST, Enum SRC>
consteval bool check_contains_all() noexcept {
    bool contains_all = true;
    magic_enum::enum_for_each<SRC>([&contains_all](auto val) {
        constexpr SRC choice = val;
        contains_all &= magic_enum::enum_contains<DST>(static_cast<std::underlying_type_t<SRC>>(choice));
    });
    return contains_all;
}

template <Enum DST, Enum SRC>
constexpr DST err_cast(SRC src) noexcept {
    static_assert(check_contains_all<DST, SRC>());
    return static_cast<DST>(src);
}



template <Enum T>
struct [[nodiscard]] Error {
    using ErrorEnum = T;
    static_assert(check_contains_all<AllError, ErrorEnum>());

    const ErrorEnum error;

    constexpr Error(ErrorEnum err) noexcept: error{err} {}
    constexpr Error(std::underlying_type_t<ErrorEnum> ec) noexcept: error{ec} {}

    template <Enum E>
    constexpr Error(E err) noexcept: error{err_cast<ErrorEnum>(err)} {}
    template <Enum E>
    constexpr Error(Error<E> err) noexcept: error{err_cast<ErrorEnum>(err.error)} {}

    constexpr operator bool() const noexcept { return error != ErrorEnum::Success; } 
};



enum class FooError: int {
    Success = 0,
    E1 = 1,
    E2 = 2,
#ifdef WITH_ERROR
    E7 = 7,
#endif
};

Error<FooError> foo() { return FooError::E2; }



enum class BarError: int {
    Success = 0,
    E1 = 1,
    E2 = 2,
    E3 = 3,
    E4 = 4,
#ifdef WITH_ERROR2
    E9 = 9,
#endif
};


Error<BarError> bar() {
    Error<BarError> err = foo();
    if (err) {
        return err;
    }
    // ...
    return BarError::Success;
}



int main() {
    auto err1 = foo();
    std::cout << magic_enum::enum_name(err1.error) << std::endl;
    auto err2 = bar();
    std::cout << magic_enum::enum_name(err2.error) << std::endl;
}
Ivan_qrt ★★★★★
()
Последнее исправление: Ivan_qrt (всего исправлений: 2)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.