Концепция Linux «всё есть файл» или пример качественной архитектуры ПО
В этой статье мы рассмотрим, каким образом в Linux/FFmpeg можно организовать кодовую базу на языке C с учётом расширяемости, работающей, как будто в C есть полиморфизм. Также обсудим, каким образом концепция Linux «всё — файл» функционирует на уровне исходного кода. И почему FFmpeg даёт возможность легко и быстро осуществлять добавление поддержки новых кодеков и форматов.
Введение
Не секрет, что хороший код, написанный качественно, окупится впоследствии, особенно если продукт усложнится. Чтобы такой код создать, программисты подбирают специальные шаблоны, объединяя их в абстракции. В нашем случае именно так и поступили разработчики Linux и FFmpeg. Речь идёт о том, что в процессе разработки ПО создаются структуры данных, причём определяется их поведение и зависимости. А то, каким образом они построены и взаимосвязаны, мы можем рассматривать как архитектуру ПО или, если хотите, дизайн.
Допустим, мы разрабатываем фреймворк, необходимый для обработки аудио- и видеофайлов. Следует учесть, что кодеки H264, AV1, HEVC и AAC некоторые операции с данными выполняют идентично, поэтому, разработав обобщённую абстракцию, можно будет задействовать её вместо того, чтобы плотно работать с каждым отдельным кодеком.
Другой неплохой приём — использование слабосвязанных компонентов, но для этого потребуется чётко определить их функции.
Ruby
Концепцию гораздо проще понять на практическом примере. Давайте создадим (примерно) набросок фреймворка, необходимый для обработки потоковых данных с использованием нескольких различных кодеков:
class AV1 def encode(bytes) end def decode(bytes) end end class H264 def encode(bytes) end def decode(bytes) end end # ... supported_codecs = [AV1.new, H264.new, HEVC.new] class MediaFramework def encode(type, bytes) codec = supported_codecs.find {|c| c.class.name.downcase == type} codec.encode(bytes) end end
Без конкретизации в нашем коде мы предполагаем, что каждый кодек реализует такие функции, как encode и decode. А так как Ruby является языком с динамической типизацией, то любой из классов способен иметь реализацию данных 2-х операций, работая как кодек.
Этот дизайн можно назвать довольно неплохим, ведь, если надо будет добавить новый кодек, потребуется лишь включить в список его реализацию. Да, список мы можем сделать и динамическим. Но вообще, смысл примера заключается в том, что данный код легкорасширяем и хорошо поддерживается, поскольку компоненты между собой связаны слабо, а каждый из этих компонентов делает лишь то, что должен.
Кстати, к определённым способам организации кода подталкивает и фреймворк Ruby on Rails, но уже через архитектуру MVC.
Go
Когда мы обращаемся к языкам программирования со статической типизацией, мы должны быть более формальными, когда описываем требуемые типы. Тем не менее мы всё же можем создать код, который аналогичен вышеописанному примеру:
type Codec interface { Encode(data []int) ([]int, error) Decode(data []int) ([]int, error) } type H264 struct { } func (H264) Encode(data []int) ([]int, error) { // Здесь много кода return data, nil } var supportedCodecs := []Codec{H264{}, AV1{}} func Encode(codec string, data int[]) { // Здесь возможно выбрать e, применяя // supportedCodecs[0].Encode(data) }
Итак, тип interface в Go существенно мощнее той же конструкции в Java, ведь его определение не связано с реализацией, впрочем, как и наоборот. Мы даже можем присвоить тип ReadWriter каждому кодеку и использовать его в таком виде.
С
На «Си» мы также можем создать код со схожим поведением при некоторых отличиях:
struct Codec { *int (*encode)(*int); *int (*decode)(*int); }; *int h264_encode(int *bytes) { // ... } *int h264_decode(int *bytes) { // ... } struct Codec av1 = { .encode = av1_encode, .decode = av1_decode }; struct Codec h264 = { .encode = h264_encode, .decode = h264_decode }; int main(int argc, char *argv[]) { h264.encode(argv[1]); }
Смотрите, изначально в обобщённой структуре определяются абстрактные операции (в нашем случае функции). Потом они наполняются конкретным кодом, к примеру, декодером и кодером кодека AV1.
Многие другие языки поддерживают схожую логику распределения методов либо функций, как будто они придерживаются какой-либо конвенции.
В итоге софту на уровне операционной системы достаточно лишь уметь работать с показанными абстракциями высокого уровня.
Linux kernel и непосредственно концепция «всё — файл»
Эта концепция операционной системы «Линукс» даёт нам возможность применять один интерфейс при работе с любыми системными ресурсами. Допустим, при обработке сетевых сокетов, особых файлов и даже USB-устройств как файлов.
Такой подход упрощает программную разработку для ОС, ведь мы получаем возможность применять прекрасно исследованный набор операций для абстракции, которая названа «файлом».
Посмотрим, как это функционирует:
# В 1-м, самом простом случае, мы просто читаем несложный текстовый файл $ cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ... # Тут мы действуем, как будто считываем простой файл, # но на самом деле всё не так (хотя технически, разумеется, именно так) $ cat /proc/meminfo MemTotal: 2046844 kB MemFree: 546984 kB MemAvailable: 1535688 kB Buffers: 162676 kB Cached: 892000 kB # В конце концов, мы открываем файл с помощью fd=3 для чтения/записи # Данный «файл» на самом деле представляет собой сокет # потом мы отправляем запрос этому файлу >&3 # и из него же считываем $ exec 3<> /dev/tcp/www.google.com/80 $ printf 'HEAD / HTTP/1.1\nHost: www.google.com\nConnection: close\n\n' >&3 $ cat <&3 HTTP/1.1 200 OK Date: Wed, 21 Aug 2019 12:48:40 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1 P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info." Server: gws X-XSS-Protection: 0 X-Frame-Options: SAMEORIGIN Set-Cookie: 1P_JAR=2019-08-21-12; expires=Fri, 20-Sep-2019 12:48:40 GMT; path=/; domain=.google.com Set-Cookie: NID=188=K69nLKjqge87Ymv4h-gAW_lRfLCo7-KrTf01ULtY278lUUcaNxlEqXExDtVB104pdA8CLUZI8LMvJv26P_D8RMF3qCDzLTpjji96B9v_miGlZOIBro6pDreHP0yW7dz-9myBfOgdQjroAc0wWvOAkBu-zgFW_Of9VpK3IfIaBok; expires=Thu, 20-Feb-2020 12:48:40 GMT; path=/; domain=.google.com; HttpOnly Accept-Ranges: none Vary: Accept-Encoding Connection: close
Всё это становится возможным лишь из-за того, что концепция разрабатывалась как один из основных способов подсистемного взаимодействия. Давайте посмотрим на участок API-структуры file_operations:
struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); // ... }
Такая структура весьма чётко определяет то, что принято подразумевать под концепцией файла, а также то, какое именно поведение мы ожидаем:
const struct file_operations ext4_dir_operations = { .llseek = ext4_dir_llseek, .read = generic_read_dir, // ... };
Ниже можем посмотреть на набор функций, которые реализуют это поведение в файловой системе ext4:
static const struct file_operations proc_cpuinfo_operations = { .open = cpuinfo_open, .read = seq_read, .llseek = seq_lseek, .release = seq_release, };
Обратите внимание что даже cpuinfo proc-файлы реализованы посредством этой абстракции. То есть по факту при работе с файлами под «Линукс», вы применяете VFS, а она, в свою очередь, выполняет обращение к абстрактным функциям.
FFmpeg — форматы
Теперь посмотрим на общую схему архитектуры FFmpeg-процессов, демонстрирующих, что внутренние компоненты, в основном, связаны посредством таких абстрактных концепций, как AVCodec. То есть они не связаны напрямую через конкретные кодеки.
Идём дальше. Для входящих файлов в FFmpeg осуществляется создание структуры AVInputFormat, реализуемой посредством любого формата (видеоконтейнера), который нужно использовать. Что касается файлов MKV, то они тоже заполняют эту структуру собственной реализацией, своей реализацией заполняет структуру и MP4-формат.
typedef struct AVInputFormat { const char *name; const char *long_name; const char *extensions; const char *mime_type; ff_const59 struct AVInputFormat *next; int raw_codec_id; int priv_data_size; int (*read_probe)(const AVProbeData *); int (*read_header)(struct AVFormatContext *); } // matroska AVInputFormat ff_matroska_demuxer = { .name = "matroska,webm", .long_name = NULL_IF_CONFIG_SMALL("Matroska / WebM"), .extensions = "mkv,mk3d,mka,mks", .priv_data_size = sizeof(MatroskaDemuxContext), .read_probe = matroska_probe, .read_header = matroska_read_header, .read_packet = matroska_read_packet, .read_close = matroska_read_close, .read_seek = matroska_read_seek, .mime_type = "audio/webm,audio/x-matroska,video/webm,video/x-matroska" }; // mov (mp4) AVInputFormat ff_mov_demuxer = { .name = "mov,mp4,m4a,3gp,3g2,mj2", .long_name = NULL_IF_CONFIG_SMALL("QuickTime / MOV"), .priv_class = &mov_class, .priv_data_size = sizeof(MOVContext), .extensions = "mov,mp4,m4a,3gp,3g2,mj2", .read_probe = mov_probe, .read_header = mov_read_header, .read_packet = mov_read_packet, .read_close = mov_read_close, .read_seek = mov_read_seek, .flags = AVFMT_NO_BYTE_SEEK | AVFMT_SEEK_TO_PTS, };
Данный дизайн даёт возможность осуществлять простую интеграцию новых кодеков, форматов и протоколов. А весной 2019 г. в FFmpeg включили DAV1d-кодек и, после изучения изменений в коде, вы увидите, насколько успешно прошло это внедрение. В итоге ему нужно лишь зарегаться в качестве доступного кодека, а потом придерживаться списка общих операций.
+AVCodec ff_libdav1d_decoder = { + .name = "libdav1d", + .long_name = NULL_IF_CONFIG_SMALL("dav1d AV1 decoder by VideoLAN"), + .type = AVMEDIA_TYPE_VIDEO, + .id = AV_CODEC_ID_AV1, + .priv_data_size = sizeof(Libdav1dContext), + .init = libdav1d_init, + .close = libdav1d_close, + .flush = libdav1d_flush, + .receive_frame = libdav1d_receive_frame, + .capabilities = AV_CODEC_CAP_DELAY | AV_CODEC_CAP_AUTO_THREADS, + .caps_internal = FF_CODEC_CAP_INIT_THREADSAFE | FF_CODEC_CAP_INIT_CLEANUP | + FF_CODEC_CAP_SETS_PKT_DTS, + .priv_class = &libdav1d_class, + .wrapper_name = "libdav1d", +};`
Итак, вне зависимости от применяемого нами языка программирования мы всегда можем пробовать создавать слабозависимый код с повышенной согласованностью. Как раз эти 2 свойства и дают нам возможность писать софт, который потом будет легко расширять и так же легко поддерживать.
Источник — «Good Code Design From Linux/Kernel».