by hugeping on 2021-11-09 17:14:26
Интересное совпадение. На днях из двух независимых источников вышел на статьи, описывающие проблемы Си.
https://www.yodaiken.com/2018/12/31/undefined-behavior-and-the-purpose-of-c/ [1]
http://cmustdie.com/ [2]
Скажу сразу, читается интересно. С некоторыми вещами сталкивался на практике. Правда, в статьях я не увидел "выхлопа", какого-нибудь позитивного вывода. Проблемы озвучены, но что предлагается в качестве решения?
Да, неопределённое поведение -- это угроза. И угроза вдвойне, когда по мере развития компиляторов, это поведение воспринимается как территория, на которой можно "срезать углы". При этом мы получаем мину замедленного действия. Старый софт, собранный новым компилятором может "бахнуть" когда и где угодно... Так что с точки зрения индустрии, нужен простой (не надо много платить за кадры), дубовый и предсказуемый язык. Правда, при этом, он должен быть ещё и системным. Совместить эти противоречивые требования нелегко...
Как программист на Си, я понимаю, что по современным меркам этот язык нарушает все мыслимые правила приличия. Но при этом я действительно НЕ ВИЖУ адекватной замены в той сфере где он применяется. Это как с самолётами. Неустойчивый самолёт -- маневренный и опасный. Устойчивый -- безопасный и неманевренный. Что выбрать? Что-то среднее? Чем мы готовы пожертвовать?
Представим себе, что нам нужно написать ядро ОС. Какой язык выбрать? C, C++... Rust?
Мои знания о Rust ограничены чтением книги, поэтому я недостаточно подкован для того, чтобы адекватно оценить его кандидатуру как замену Си. Субъективно, мне кажется, что Rust не сможет стать таким же массовым языком. В нём нет простоты Си. Его синтаксис многословен, а в некоторых задачах он выкручивает программисту руки. Я надеюсь, что идеи заложенные в Rust найдут реализацию в каком-то другом ЯП. Но это моё личное, субъективное мнение. Конечно, если индустрия перейдёт на Rust, я буду его использовать в любом случае. А пока, можно посмотреть на пример реализации драйвера на Си и Rust: https://lwn.net/Articles/863459/ [3] который меня совсем не впечатляет.
Си был моим любимым языком с самого начала профессиональной деятельности. Но с годами я стал замечать, что программирование на Си всё чаще воспринимается как рутина. Я перестал чувствовать удовольствие от программирования и изучения чужого кода. Работал скорее как "ремесленник". К счастью, были и другие ЯП, которые мне было интересно изучить, так что выгорания я избежал.
Но не так давно я открыл для себя Си заново. Помогло мне в этом осознание:
Код на Си прекрасен, когда он прост!
Но что значит простой код? Программисты всегда пытаются писать просто. Или нет? И можно ли писать современное ПО просто?
Посмотрите. Это реализация cat в Plan9: https://github.com/0intro/plan9/blob/master/sys/src/cmd/cat.c [4]
А это, для сравнения, cat из coreutils: https://github.com/coreutils/coreutils/blob/master/src/cat.c [5]
Я отдаю себе отчёт в том, что современное ПО так не пишется. Поддержка локалей и gettext. Учёт особенностей системных вызовов на разных ОС. Поддержка множества параметров. И вот, объём и сложность кода растут как снежный ком. А если вспомнить про полную поддержку Unicode (со всеми этими проклятыми эмодзи и сменой направления вывода), то становится совсем уж плохо. Мы так привыкли к универсальности "правильного" подхода, что когда ты видишь "наивный" код Plan9, то испытываешь шок. Что, так можно было?
Но если по чесноку, разве cat не должен быть именно таким, каким он остался в Plan9? :) Думаю, каждый программист чувствует здесь какую-то правду.
Си -- это портативный ассемблер. В этом его основная миссия, сила и слабость. И сегодня, когда разрыв между низким и высоким уровнем многократно увеличился, я не могу не признать, что он мало пригоден для написания сложного прикладного ПО. Но что с системным?
Системное ПО тоже стало сложнее. Как системный программист, я начинал с ядра Linux 2.2. Читать и понимать код современного ядра стало намного сложнее.
В качестве примера, посмотрите реализацию системного вызова open в ядре 2.2.
https://elixir.bootlin.com/linux/2.2.26/source/fs/open.c#L757 [6]
asmlinkage int sys_open(const char * filename, int flags, int mode) { char * tmp; int fd, error; tmp = getname(filename); fd = PTR_ERR(tmp); if (!IS_ERR(tmp)) { lock_kernel(); fd = get_unused_fd(); if (fd >= 0) { struct file * f = filp_open(tmp, flags, mode); error = PTR_ERR(f); if (IS_ERR(f)) goto out_error; fd_install(fd, f); } out: unlock_kernel(); putname(tmp); } return fd; out_error: put_unused_fd(fd); fd = error; goto out; }
Довольно наглядно и просто, не правда ли?
А теперь начните своё путешествие по ядру 5.10.
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1200 [7]
Вам придётся пройтись по цепочке: do_sys_open -> do_sys_openat2
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1193 [8]
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1164 [9]
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode) { if (force_o_largefile()) flags |= O_LARGEFILE; return do_sys_open(AT_FDCWD, filename, flags, mode); } long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode) { struct open_how how = build_open_how(flags, mode); return do_sys_openat2(dfd, filename, &how); } static long do_sys_openat2(int dfd, const char __user *filename, struct open_how *how) { struct open_flags op; int fd = build_open_flags(how, &op); struct filename *tmp; if (fd) return fd; tmp = getname(filename); if (IS_ERR(tmp)) return PTR_ERR(tmp); fd = get_unused_fd_flags(how->flags); if (fd >= 0) { struct file *f = do_filp_open(dfd, tmp, &op); if (IS_ERR(f)) { put_unused_fd(fd); fd = PTR_ERR(f); } else { fsnotify_open(f); fd_install(fd, f); } } putname(tmp); return fd; }
Необходимость изменений очевидна. Появилась подсистема fsnotify. Добавили новые системные вызовы -- пришлось нарезать функции и сводить open к openat...
Код выглядит аккуратно, но разобраться в нём стало сложнее. А значит, проще ошибиться.
Да, работая на индустрию не удастся вернуться в прошлое. Однако, Plan9 научил меня тому, что во многих случаях у меня, как у программиста, всё-таки есть свобода избежать сложного кода, в том числе ценой выбора более подходящей архитектуры. Например, написав небольшое ядро приложения на Си и функциональную часть на Lua. Если я вижу, что код на Си становится сложным, это повод остановиться. Стоп! Что-то не так!
Что касается судьбы Си... Я тоже хотел бы увидеть современного приемника, но в Rust я не смог его узнать. Возможно, это моя ошибка. Поживём -- увидим.
Интересно, сколько лет ещё продержится старичок Си? А то ведь и 70-летие не за горами. :)
P.S. Ещё одна "эмоциональная" статья на тему C и C++: Почему я всё ещё люблю C, но при этом терпеть не могу C++?
https://habr.com/ru/company/ruvds/blog/562530/ [10]
https://www.yodaiken.com/2018/12/31/undefined-behavior-and-the-purpose-of-c/ [1]
https://lwn.net/Articles/863459/ [3]
https://github.com/0intro/plan9/blob/master/sys/src/cmd/cat.c [4]
https://github.com/coreutils/coreutils/blob/master/src/cat.c [5]
https://elixir.bootlin.com/linux/2.2.26/source/fs/open.c#L757 [6]
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1200 [7]
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1193 [8]
https://elixir.bootlin.com/linux/v5.10.78/source/fs/open.c#L1164 [9]