1.2: Реалізація абстракції
- Page ID
- 34572
Загалом, абстракція реалізується тим, що загалом називається Інтерфейс прикладного програмування (API). API - це дещо туманний термін, який означає різні речі в контексті різних зусиль програмування. По суті, програміст розробляє набір функцій і документує їх інтерфейс і функціональність з принципом, що фактична реалізація, що забезпечує API, є непрозорою.
Наприклад, багато великих веб-додатків надають API, доступний через HTTP. Доступ до даних за допомогою цього методу, безумовно, викликає багато складних серій віддалених викликів процедур, запитів до бази даних та передачі даних, всі з яких непрозорі для кінцевого користувача, який просто отримує контрактні дані.
Ті, хто знайомий з об'єктно-орієнтованими мовами, такими як Java, Python або C ++, були б знайомі з абстракцією, що надається класами. Методи надають інтерфейс класу, але абстрактні реалізацію.
Поширеним методом, який використовується в ядрі Linux та інших великих кодових базах C, у яких відсутня вбудована концепція об'єктно-орієнтації, є покажчики функцій. Навчання читати цю ідіому є ключовим для навігації більшості великих баз коду C. Розуміючи, як читати абстракції, надані в коді, можна побудувати розуміння внутрішніх конструкцій API.
#include <stdio.h>
/* The API to implement */
struct greet_api
{
int (*say_hello)(char *name);
int (*say_goodbye)(void);
};
/* Our implementation of the hello function */
int say_hello_fn(char *name)
{
printf("Hello %s\n", name);
return 0;
}
/* Our implementation of the goodbye function */
int say_goodbye_fn(void)
{
printf("Goodbye\n");
return 0;
}
/* A struct implementing the API */
struct greet_api greet_api =
{
.say_hello = say_hello_fn,
.say_goodbye = say_goodbye_fn
};
/* main() doesn't need to know anything about how the
* say_hello/goodbye works, it just knows that it does */
int main(int argc, char *argv[])
{
greet_api.say_hello(argv[1]);
greet_api.say_goodbye();
printf("%p, %p, %p\n", greet_api.say_hello, say_hello_fn, &say_hello_fn);
exit(0);
}
Код, такий як вище, є найпростішим прикладом конструкцій, які неодноразово використовуються в ядрі Linux та інших програмах C. Давайте розглянемо деякі конкретні елементи.
Ми починаємо зі структури, яка визначає API (структура greet_api). Функції, імена яких укладені в дужки з маркером покажчика, описують покажчик функції [1]. Покажчик функції описує прототип функції, на яку він повинен вказувати; вказуючи його на функцію без правильного типу повернення або параметрів, принаймні, буде генерувати попередження компілятора; якщо його залишити в коді, швидше за все, призведе до некоректної роботи або аварійного завершення роботи.
Потім ми маємо нашу реалізацію API. Часто для більш складної функціональності ви побачите ідіому, де функції реалізації API будуть лише обгорткою навколо інших функцій, які умовно додаються одним або двома підкресленнями [2] (тобто say_hello_fn () викликала б іншу функцію _say_ функція hello_()). Це має кілька застосувань; як правило, це стосується того, що простіші та менші частини API (наприклад, марширування або перевірка аргументів) окремо від більш складної реалізації, що часто полегшує шлях до значних змін у внутрішній роботі, гарантуючи, що API залишається постійним. Наша реалізація дуже проста, однак, і навіть не потребує власних функцій підтримки. У різних проектах префікси функцій одно-, подвійного або навіть потрійного підкреслення означатимуть різні речі, але повсюдно це візуальне попередження про те, що функція не повинна бути викликана безпосередньо «за межами» API.
По-друге, ми заповнюємо покажчики функцій у структурі greet_api greet_api. Назва функції є покажчиком, тому немає необхідності брати адресу функції (тобто &say_hello_fn).
Нарешті, ми можемо викликати функції API через структуру в main.
Ви будете бачити цю ідіому постійно під час навігації по вихідному коду. Крихітний приклад нижче взято з include/linux/virtio.h у вихідному коді ядра Linux, щоб проілюструвати:
/**
* virtio_driver - operations for a virtio I/O driver
* @driver: underlying device driver (populate name and owner).
* @id_table: the ids serviced by this driver.
* @feature_table: an array of feature numbers supported by this driver.
* @feature_table_size: number of entries in the feature table array.
* @probe: the function to call when a device is found. Returns 0 or -errno.
* @remove: the function to call when a device is removed.
* @config_changed: optional function to call when the device configuration
* changes; may be called in interrupt context.
*/
struct virtio_driver {
struct device_driver driver;
const struct virtio_device_id *id_table;
const unsigned int *feature_table;
unsigned int feature_table_size;
int (*probe)(struct virtio_device *dev);
void (*scan)(struct virtio_device *dev);
void (*remove)(struct virtio_device *dev);
void (*config_changed)(struct virtio_device *dev);
#ifdef CONFIG_PM
int (*freeze)(struct virtio_device *dev);
int (*restore)(struct virtio_device *dev);
#endif
};
Потрібно лише смутно розуміти, що дана структура є описом віртуального пристрою введення/виводу. Ми бачимо, що користувач цього API (автор драйвера пристрою), як очікується, надасть ряд функцій, які будуть викликатися в різних умовах під час роботи системи (при зондуванні нового обладнання, при видаленні апаратного забезпечення і т.д.). Він також містить ряд даних; структури, які повинні бути заповнені відповідними даними.
Починаючи з таких дескрипторів, як правило, найпростіший спосіб розпочати розуміння різних шарів коду ядра.
Бібліотеки мають дві ролі, які ілюструють абстракцію.
- Дозволити програмістам повторно використовувати часто доступний код.
- Виступайте як чорний ящик, реалізуючи функціонал для програміста.
Наприклад, бібліотека, що реалізує доступ до необроблених даних у файлах JPEG, має ту перевагу, що багато програм, які бажають отримати доступ до файлів зображень, можуть використовувати одну і ту ж бібліотеку, і програмістам, які будують ці програми, не потрібно турбуватися про точні деталі формату файлу JPEG, але можуть зосередитися. їх зусилля на те, що їх програма хоче зробити з зображенням.
Стандартна бібліотека платформи UNIX загалом називається libc. Він забезпечує базовий інтерфейс системи: фундаментальні виклики, такі як read (), write () і printf (). Цей API повністю описується специфікацією під назвою POSIX. Він знаходиться у вільному доступі в Інтернеті і описує безліч викликів, які складають стандартний UNIX API.
Більшість платформ UNIX в цілому дотримуються стандарту POSIX, хоча часто відрізняються невеликими, але іноді важливими способами (звідси складність різних автоінструментів GNU, які часто намагаються абстрагуватися від цих відмінностей для вас). Linux має багато інтерфейсів, які не вказані POSIX; написання програм, які використовують їх виключно, зробить вашу програму менш портативною.
Бібліотеки - це фундаментальна абстракція з багатьма деталями. У наступних розділах буде описано, як працюють бібліотеки набагато докладніше.