До питання про стандартних бібліотеках

Це розповідь ми з загадки почнемо,
Навіть Аліса відповість чи,
Що залишається від казки потім,
Після того, як її розповіли?
Дане есе буде присвячений різним темам, серед яких знайдеться місце і відповіді на питання, винесене в підзаголовок, а розвиватися розповідь буде в основному навкруги проблем, пов'язаних з ініціалізацією периферії сучасного МК.
Отже, окреслимо основні проблеми, пов'язані з налаштуванням апаратної частини МК: необхідність, завдання значної кількості параметрів, з яких більша частина не визначається в кожному конкретному випадку, але, тим не менш, не може бути залишена довільною, а повинна приймати деякі пред-певні значення. Якщо Ви вірите, що така проста постановка задачі здатна викликати потік свідомості і привести до деяких не цілком очевидним рішенням, то

Розглянемо приклад, пов'язаний з налаштуванням набила оскому інтерфейсу, а саме ИРПС (Інтерфейс Радіальний Послідовний — саме під таким ім'ям у дівоцтві виступав, нині відомий, як UART персонаж). Відразу відповім на питання, чому російська абревіатура — справа в тому, що я пишу нотатки частково в дорозі, а перемикання розкладок я б особисто зручною частиною Андроїд клавіатури не назвав (до речі, якщо хто знає зручну клавіатуру зі стрілками для управління курсором і не гальмівну, киньте в приват, а то так незручно тикати в екран).
Звичайно, піднятий коло питань не обмежується тільки ІРПС, необхідно налаштовувати і іншу апаратуру МК, але його я взяв просто для прикладу. Так от, для налаштування роботи ІРПС ми повинні, як мінімум, задати формат посилки, який визначається наступними параметрами — швидкість передачі, кількість переданих біт даних, наявність і тип біта парності, кількість стопових бітів. І це тільки початок, насправді є ще й розширена конфігурація і вона набагато довший, але зараз мова не про це. Потрібно також врахувати, що в 70 відсотків випадків при використанні ІРПС буде задана стандартна конфігурація 9600-8-0-1, ще в 25 відсотках буде змінюватися тільки швидкість передачі, і всі інші конфігурації поділять решту 5 відсотків, але саме для них і повинна існувати можливість налаштування.

Перш, ніж ми почнемо розглядати різні варіанти вирішення даної задачі, нам слід визначитися з критеріями оцінки вдалості того або іншого варіанту, інакше вибір кращого з них перетвориться в обговорення смакових уподобань. Я в своєму пості виходжу в першу чергу з критерію надійності, оскільки вважаю визначальними наступні обставини: 1.людині властиво помилятися, 2. завдання компілятора складається (у тому числі) у тому, щоб вказати на ці помилки як можна раніше, а в ідеалі запобігти їх появі. Саме з таких позицій я буду розглядати прийнятність того чи іншого рішення, якщо ж Ви не згодні з хоча б одним з вищенаведених двох постулатів, то, швидше за все, цей пост Вам не дуже сподобається і Вам слід припинити його читання, хоча не забороняється дане заняття продовжити і (обов'язково аргументовано) викласти свою позицію в коментарях.

Для початку зрозуміємо, чому така задача виникла, адже в МК попередніх поколінь (PIC, AVR, 51) ми чудово справлялися з налаштуванням апаратури шляхом прямих записів у відповідні регістри? Ну, перш за все, для цього треба знати регістри, або, як нині прийнято виражатися, курити мануали, а там багато букв і, хоча особисто мені це заняття не представляється особливо напружує, тим не менш, особливо враховуючи якість нинішніх мінлива, для значної частини спільноти такий підхід може дійсно представляти проблему.

Невелика (хахаха я справді так думав) замітка з приводу якості сучасної документації — може бути, раніше трава була зеленішою, але я цілком солідарний з фрази Джека Ганслі в одному з недавніх блогів: «останнім часом мені все частіше зустрічаються прилади, до документації на які самим м'яким визначенням буде слово недостатня, хоча раніше її визначили б як відсутню». Звичайно, Інет велика річ (а тут сарказму ні, це дійсно нове чудо світу) і Ви можете задати питання за незрозумілих місць як безпосередньо розробникам, так і на форумах і отримати безліч відповідей, серед яких навіть при деякому везінні, будуть і вірні, але чому б не написати документацію таким чином, щоб вона не залишала місця різночитання? Мені зазвичай відповідають, що для написання хорошої документації слід залучати розробників, а їх час занадто цінне, а технічні письменники з завданням не справляються, але, по-моєму, це не мої проблеми, воут?

Можу тільки рекомендувати спробувати віддати написання документації на аутсорс, причому бажано розробникам пристроїв, які Ваші прилади використовують, оскільки розробники власне приладу занадто багато вважають очевидним, а те, що очевидно розробнику, не завжди (точніше, не завжди) очевидно користувачеві. Може бути, створити контору з надання даного виду послуг, хоча, враховуючи ставлення наших виробників до документації, навряд чи вони будуть користуватися настільки дорогий і непотрібною послугою.

Невелика ремінісценція на тему документації. Вивчав нещодавно опис на один кристал (фірми Intel, між іншим) і там є вхід з промовистою назвою PE_RST_N, який описаний як +3.3 Vdc вхід, який, будучи затверджений (asserted), сигналізує про наявність живлення і тактової частоти. Як саме вхід затверджується високим або низьким рівнем, не зазначено, тимчасових діаграм для цього сигналу не наведено (хоча на тимчасовій діаграмі є сигнал PERST# і в діаграмі станів приладу він згадується саме з такою назвою), далі за текстом є фраза про те, що значення 0b відображає активність скиду в таблиці режимів роботи при наявності скидання стоїть галочка в графі значення, що як би натякає. Загалом, за сукупністю непрямих ознак можна зробити висновок, що активний рівень на цій нозі, що приводить до скидання приладу, таки так, низький, але чому я повинен здогадуватися, замість того, щоб просто прочитати відповідне ясне, чітке і зрозуміле опис в документації?
Чому мене змушують вступати на хиткий грунт здогадок і припущень? Напевно, особисто для мене це покарання за погану карму в минулих життях (ну і в цій я янголом не був), але інші розробники чим завинили? Один мій колега висловив цікаву гіпотезу, що таку документацію роблять спеціально, щоб ускладнити нам підняття з колін, але навіщо тоді вона написана на англійській мові? Або ж вона зроблена спеціально для нас, а на Заході користуються справжньою, правильної документацією? Хоча в даному випадку як не можна краще підходить фраза «Не слід пояснювати злим умислом те, що можна пояснити простою лінню»(не буду ображати творців такої документації дослівним цитуванням).

Але справжній шедевр в моєї особистої колекції — це опис одного, загалом непоганого МК виробництва вітчизняної фірми, документації на який був описаний біт керування підключенням підтягуючих резисторів наступним чином: «0 — підтягуючі резистори випадають», 1-… вгадайте, як там написано? — «підтягуючі резистори не випадають» — непогана спроба, але не вгадали… барабанний дріб в студію… «значення, протилежне 0». Всі аплодують, завіса.

Але все перераховане вище (це я про незадовільну якість документації, якщо хто вже забув) тільки частина проблеми, друга складова
полягає в тому, що сучасні МК дійсно складніше своїх попередників. У мене є своя точка на те, чому відбувається ускладнення апаратури і «все більш повне задоволення безперервно зростаючих потреб розробників» не стоїть на першому місці в списку причин даного явища, але ситуацію в цілому мої роздуми на цей рахунок не міняють — сучасні МК дійсно складніше своїх попередників, у них набагато більше апаратних блоків і самі блоки стали набагато складніше, реалізують додаткові функції, які розробники можуть (і повинні) використовувати з користю для справи. Я тут замислив пост на тему деяких фіч (дійсно корисних) в UART сімейства STM, коли закінчу цей, обов'язково напишу, як раз при реалізації використання цих особливостей я замислився над проблемами, що викликали до життя цю посаду.

Наступна частина проблеми пов'язана з наростанням номенклатури як сімейств МК від різних виробників, так і підвидів МК в рамках однієї серії від одного виробника, завершуючи значною різноманітністю приладів в рамках одного підвиду. Одночасно з розширенням номенклатури коротшає життєвий цикл конкретного представника сімейства, що ставить питання перманентного переходу на нові прилади. Знову-таки, далеко не завжди це дійсно необхідно, але тенденція очевидна і боротися з нею дуже важко, тому бажано вести розробку програмного забезпечення таким чином, щоб перехід на інший МК був як можна більш безболісним і в ідеалі зводився до заміни одного рядка
#define device xxxxx
чому, безсумнівно, сприяє використання стандартних бібліотек, тому я і пишу цей пост про правильне (з моєї точки зору, а про іншу, Ви вже знаєте) їх написанні.

Почнемо розглядати можливі варіанти реалізації налаштування ІРПС і першим з них буде просто функція ініціалізації зі всіма можливими параметрами (тут я деякий час боровся зі спокусою писати тексти прикладів на АЯП — алгоритмічній мові програмування, але потім вирішив, що це уже буде по той бік межі, яка відділяє добро від зла і легкий тролінг від знущання). І ми отримуємо щось на зразок
UARTInit(9600,8,0,1);
для вищенаведеного стандартного випадку (тут і далі залишимо за дужками питання про вибір одного настроюваного каналу апаратури з існуючих у МК). Начебто тут все нормально і нічого надприродного нам писати не доводиться, але спробуємо знайти недоліки у даному варіанті і (зрозуміло, інакше навіщо шукати) усунути їх.

Перш за все, треба визначитися, що ми взагалі хочемо від процедури налаштування апаратної частини і які вимоги до неї пред'являємо. На мій погляд, програма має бути насамперед надійною, безпечною, і зрозумілою. Вимоги ефективності не настільки значущі, оскільки ініціалізації, як правило, проводиться один раз і може бути не надто швидка, але якщо вона буде компактна по пам'яті і невимоглива по швидкості, то це додатковий плюс при оцінці. До речі, щонайменше одна технологія, а саме Чарлиплекинг, вимагає оперативної зміни режиму роботи ніжок МК, тому зовсім не варто нехтувати швидкодією.

Які ж недоліки ми бачимо в даній технології (а ми їх бачимо, інакше про що писати далі) — насамперед, це необхідність перераховувати велика кількість параметрів, причому в строго визначеному порядку, і якщо ми помилимося, то результат буде зовсім не той, на який ми розраховували. Питання з порядком параметрів можна дещо послабити, якщо визначити користувальницькі типи даних для кожного параметра, тоді отримаємо:
typedef enum {...UARTSpeed9600...} UARTSpeedT;
void UARTInit(UARTSpeesT UartSpeed,...};
, і при спробі помилитися ми отримаємо попередження від компілятора. Дані підхід не стоїть нічого на етапі виконання і зовсім трохи на етапі компіляції, тому я можу його настійно рекомендувати і одночасно висловити тяжке здивування з приводу того, що автори (у тому числі фірмових) бібліотек таким простим і в той же час ефективним способом нехтують.

Єдиним їх виправданням може служити необхідність використання виразів, які треба запам'ятовувати, на відміну від магічної цифри 9600, яка зрозуміла інтуїтивно (це був сарказм). Альтернативою служить велика кількість assertов при вході у функцію, які перевірять правильність параметрів за компілятор. В принципі підхід не такий вже поганий (навіть маленька рибка краще великого таргана), але вимагає від нас налагоджувального релізу і переводить повідомлення про помилку на етап виконання, що гірше, ніж отримати їх при компіляції.

Трохи згладивши вимоги до порядку параметрів (тепер нас вилають при спробі його порушити) ми, тим не менш, як і раніше, повинні перераховувати їх всі, що напружує. Якщо ми працюємо з просунутим мовою, то у нас є значення параметрів за замовчуванням, але якщо ми залишаємося в рамках класичного С (а ми в них залишаємося, хіба я раніше не попереджав), то цей шлях не для нас. Крім того, даний метод має досить істотне обмеження, а саме, все, що приймаються за умовчанням параметри повинні слідувати за обумовленими, тому для завдання явним чином останнього параметра в списку ми зобов'язані вказати і всі попередні.

Якщо ми знову таки працюємо з просунутим типу мовою С++, то у нас є ще один метод — перевантаження оператора присвоєння, хоча особисто в мене викликає зневіру перспектива написання (4+4*3+4+1=21) функцій присвоєння з жорстким порядком аргументів і ще більш неймовірного числа функцій присвоєння з довільним порядком. Тим не менш, така можливість існує, і правила пристойності вимагають її згадати, хоча не зобов'язують використовувати.

Якщо б у нас був хороший препроцесор, який дійсно надавав би нам можливості макромови, то ми могли б написати виклик макрофункцій зі змінним числом типізованих аргументів і отримати згенерований препроцесором виклик власне функції, але у нас його немає (ну немає у нас З хорошого препроцесора, у нас то і середнього немає). Якщо хтось з читачів цього поста вважає стандартний препроцесор до мови З дійсно макромовою, то я настійно рекомендую ознайомитися з описом макропроцессора мови асемблер для машин фірми DEC, і потім можна буде подискутувати на цю тему більш предметно. Але тільки у такому порядку, причому я не виключаю і знайомства з іншими розвиненими засобами генерації коду. Тим не менш, нам такий підхід недоступний в силу обмеженості виразних засобів, а писати свій власний препроцесор ми не будемо, хоча іноді й хочеться. На цій песимістичній ноті ми завершимо розгляд варіантів з використанням функції ініціалізації з прямою передачею параметрів.

Інший варіант полягає в застосуванні якоїсь керуючої структури та відділення процесу завдання значення полів цієї структури (в тому числі і задаються за умовчанням) від власне процесу ініціалізації. Що нам дає такий поділ? Насамперед, довгоочікувану можливість модифікувати тільки ті параметри, які повинні відрізнятися від значень за замовчуванням.
Звичайно, при цьому ми ці значення повинні собі дуже добре уявляти і пам'ятати, але двох-командний інтерпретатор поки залишається недосяжним ідеалом. Але за все на цьому світі треба платити, і за таку можливість нам доведеться заплатити необхідністю ці самі значення за замовчуванням гарантувати, що при використанні прямого звернення до функції так чи інакше гарантувалася компілятором.

Зрозуміло, якщо ми напишемо функцію встановлення значень за замовчуванням і викличемо її для керуючої структури, планованої до використання, то ніяких проблем не буде. А якщо ми забудемо це зробити? У кращому випадку ми отримаємо трохи не те, що ми очікували, у разі середньої тяжкості отримаємо ну зовсім не те, що ми очікували, в дуже поганому випадку ми отримаємо настільки не те, що пошкодимо апаратуру, а ось в найгіршому випадку ми отримаємо все вищеперелічене раптово, тобто в деяких рідкісних і погано повторюваних ситуаціях. Це я все до того, що завдання аж ніяк не надумана.

Знову таки, якщо ми використовуємо З++, то конструктор є природною відповіддю на наші сподівання, а якщо ще і прикрутити розумні покажчики, то рішення близьке до ідеального, але ми як і раніше залишаємося в рамках чистого С, так як це шлях самурая (шлях постійної готовності до Segmentation Fault).

Визначу своє ставлення до одного питання, пов'язаного з використанням керуючої структури, а саме формату власне структури і способом зміни її складових.
Перш за все, відразу заявлю, що особисто я проти надання користувачеві інформації про внутрішній устрій бібліотеки взагалі і керуючих структур цієї бібліотеки зокрема. Причини такого відхилення у поведінці лежать далеко від принципів ООП, обумовлені проклятим минулим і були закладені в далекі часи, коли комп'ютери були великими, а оперативна пам'ять маленької і великої розмір таблиці імен дійсно міг уповільнити компіляцію. Тому я абсолютно спокійно, без внутрішнього опору, прийняв у свій час концепцію інкапсуляції і інтерфейсу, хоча вона створена зовсім з інших міркувань, ніж економія пам'яті на етапі компіляції.
До речі, рекомендую книгу «Програмування для математиків», засновану на курсі, що читається (колись, не знаю як зараз) на ВМК, яка чудово викладає принципи ООП без використання цього терміна з позицій операцій.

А що моя поведінка є девіація і саме відхилення від мейнстріму, підтверджується вивченням вихідних текстів відомих програмних пакетів, в тому числі від STM і TI. В силу незрозумілих міркувань автори даних пакетів вважають, що всі повинні знати все про всіх, що досягається використанням вкладених включень заголовних файлів і захист від повторного включення. Тобто, якщо раптом модуль роботи з ІРПС не зможе дізнатися розподіл бітів у регістрі управління USB-хостом, то він буде страждати, марніти і навіть… еее… умретъ».

Невеликий відступ на порушену тему — я дійсно вважаю використання директиви include в h файлах злом, а рекомендації по розміщенню на початку заголовкого файлу умовного макросу для захисту від повторного включення розглядаю як поради щодо того, як зменшити шкоду від куріння. Правильне і просте рішення — не палити — авторами рекомендацій як би не розглядається, то є апріорі вважається, що продумати архітектуру програмного пакету, визначити взаємозв'язку модулів і вибудувати їх ієрархію середній (ну або вище середнього, все таки мова йде про продукцію відомих фірм) програміст вбудованих систем не здатний за визначенням, тому мова може йти тільки про мінімізацію шкоди від його криво-рукості.
Ну не знаю, наскільки це відповідає дійсності, але особисто у мене такі вкладені відмінності файли викликають сумнів у здатності їх творця написати хороший надійний і зручний) код. Але це особиста думка, виражене у приватній бесіді, і, як кажуть, «а я і не знав, що так можна було».

Так що вони (STM і TI) як хочуть, а я буду продовжувати дотримуватися принципу «Чим менше знаєш, тим краще спиш», або в іншому формулюванні: «Те, про що Ви не знаєте, не може викликати у Вас занепокоєння». Тому я вважаю, що користувачу бібліотеки ні на фіг не здалася інформація про її внутрішній устрій, хоча в рамках З ми зобов'язані її надати (а як все класно було в Turbo Pascal з його концепцією Unit, але мріяти про нездійсненне, нам вже сказали, що їх не буде і в С++17). Так що у нас десь буде вираз типу
Typedef struct {
UARTSpeedT UARTSpeed;
...
} UARTConfigT;
та її поява так само неминуче, як перемога комуністичної праці. Але ніхто нас не змушує зробити ще один крок по дорозі в трясовину і задавати значення керуючої інформації в стилі
UARTConfigT UARTConfig;
UARTConfig.UARTSpeed=UARTSpeed9600;
бо користувача абсолютно не цікавлять наші конкретні поля, а йому всього лише потрібна впевненість, що станеться те, що треба. Тому застосування того або іншого, SET-тера видається кращим, а реалізація його у вигляді окремої функції або у вигляді функції-члена залишається справою смаку, що показано в наступному фрагменті коду
void UARTSetSpeed(UARTConfigT *UARTConfig, UARTSpeedT UARTSpeed);
UARTSetSpeed(&UartConfig, UARTSpeed9600);
Прошу вибачення за наскільки великоваговий стилі іменування функцій, але якщо зайві 20 символів в назві дозволили Вам заощадити півгодини налагодження, то Ви виграли.
Цей підхід, крім того, що слід принципами ООП, має і утилітарне значення — якщо ми використовуємо З++ (але ми його не використовуємо, не забули?), то ми можемо написати одну перевантажену функцію і використовувати її для задання різних параметрів в стилі
void UARTSetParam(UARTConfigT *UARTConfig, UARTSpeedT UartSpeed);
void UARTSetParam(UARTConfigT *UARTConfig, UARTParityT UartParity);
і так далі, що дозволяє нам знизити навантаження на мозок користувача за рахунок зменшення кількості необхідних для запам'ятовування імен функцій.
Звичайно, ми повинні розуміти, що «ДарЗаНеБы» (окремий привіт тим, хто цінує Хайнлайна) і звернення до сетерові потребуватиме більшого часу при виконанні більшого обсягу пам'яті для зберігання коду в порівнянні з прямим присвоєнням значення поля (хоча для inline функцій дане твердження і не без суперечливості), але, з моєї точки зору, плюси переважують.

Ще один цікавий аспект — користувачеві не потрібна наша керуюча структура і він про неї знати не повинен (що б він сам не думав з цього приводу), тому добре б зробити її невидимою для користувача і тут підійшла б глобальна статична змінна, якщо ми врахуємо, що це накладає певні обмеження на методику її використання, або анонімний примірник, але про це трохи пізніше.

Так ось, при будь-якому способі задання значень окремих полів виникає питання про значення полів, не заданих явно, і, відповідно, встановлюються за замовчуванням. Тут треба розрізняти два аспекти: гарантія відсутності сміття в полях і значення, що залишилися від попереднього використання структури. І якщо з першим ще можна боротися шляхом використання ініціалізований змінних, то друга без явного і прямого виклику функції ініціалізації не вирішувана ніяк, навіть конструкторами (яких у нас і немає).

Що ж стосується початкової ініціалізації, то це або пряма ініціалізація в точці оголошення структури, що в принципі є правильно і нормально, оскільки контрольовано, або розміщення структури в області глобальних змінних, що забезпечує її обнулення, і ось цей метод я вважаю неприйнятним в принципі, оскільки він неконтрольований і накладає істотні обмеження на значення за замовчуванням вони повинні бути рівні 0. Ну і в будь-якому випадку питання з повторним використанням така методика ніяк не вирішує, так що навіть і початкова ініціалізація може розглядатися лише як демонстрація гарного стилю, але не як рішення.

Яке ж рішення я вважаю прийнятним, після того, як змішав з їжею для воробйов всі інші? Це комплексне рішення, тобто воно дозволяє і забезпечити значення за замовчуванням, і дає можливості вибіркового зміни параметрів, і виключає помилки користувача і знижує рівень холестерину в крові і робить ще купу корисних і потрібних речей, як і властиво по справжньому комплексного вирішення. І ще одне його важлива властивість воно написано на чистому, тобто шлях бусідо не зазнав шкоди.

Адже можна скільки завгодно розповідати про погані властивості японської сталі і пов'язаної з цим, неможливість класичного фехтування клинок у клинок, але це дуже красиво вирішити сутичку одним ударом, починаючи з вихоплювання меча з піхов і завершуються поверненням його назад одним злитим рухом зі струшуванням крапель крові невдачливого опонента по дорозі. А що стосується критиків, нещодавно прочитав чудову фразу «критика імпотентами Дон Жуана може бути об'єктивно справедливою, але все одно має неприємний відтінок».
Чесно кажучи, відчуваю сильний спокуса на цьому моменті пост перервати і залишити читача в найсильнішому подиві і розчарування, але все-таки піддамо його подальших випробувань і дозволимо отримати розчарування іншого виду від того, що так гарно описане рішення виявилося незграбним і незручним.

Отже, ось воно.
UARTConfigT UARTConfigInit(void) {
UARTConfigT UARTConfig;
UARTConfig.UARTSpeed=UARTSpeed9600;
return UARTConfig; 
};

Так дійсно можна, ми можемо повертати будь-який тип, за винятком масиву, причому можемо повертати структуру, масив містить, що мене трохи дивує, але, мабуть, у Кернигана і Річі були підстави для такого рішення, шкода, що мені вони незрозумілі. При цьому ніяких поганих речей статися не може, таке рішення абсолютно надійно і відповідає стандарту мови. Але це тільки процедура ініціалізації, а як ми будемо проводити завдання значущих параметрів? Варіант з використанням проміжної змінної відмітаємо з обуренням, оскільки він не гарантує нам виключення помилок користувача і створюємо каскадне використання в наступному стилі:
UARTConfigT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigT UARTConfig) {
UARTConfig.UARTSpeed=UARTSpeed;
return UARTConfig; 
};
Звернемо увагу на порядок визначення параметрів, переваги такого рішення ми бачимо в рядку
UARTConfigSpeed(UARTSpeed4800,UARTConfigParity(UARTParityEven,UARTConfigInit()));

де значення параметра слід відразу після імені функції, що більше проглядається в порівнянні з наступним выражением
UARTConfigSpeed(UARTConfigParity(UARTConfigInit(),UARTParityEven),UARTSpeed4800));

Взагалі то ті, хто програмував на TurboVision, дізналися цей незабутній стиль з безліччю закриваючих дужок в кінці, але зовсім необов'язково намагатися побудувати однорядкове вираз і альтернатива вже виглядає менш жахливо
UARTConfigSpeed(UARTSpeed4800,
UARTConfigParity(UARTParityEven,
UARTConfigInit()
)
);
але це вже питання смаку і обговоренню не підлягає за визначенням — про смаки не сперечаються.

У чому переваги даного варіанту — він виключає саму можливість пропустити ініціалізацію керуючої структури, він виключає знайомство з цією структурою, оскільки вона анонімна, він перевіряє відповідність параметрів функції (за рахунок перелічуваних типів) і він може перевірити ще одну нашу можливу помилка, якщо ми все зробили правильно, але використовувати структуру забули (адже ми пам'ятаємо, що людині властиво помилятися). Адже ми випустили з виду, що всі наші маніпуляції поки не привели до налаштування власне ІРПС і нам необхідна ще функція
int UARTConfigUse(UARTConfigT UARTConfig) {
return DO_something(); // власне настройка з можливою помилкою 
};

і наш приклад у підсумковому вигляді буде виглядати, як
UARTConfigUse(
UARTConfigSpeed(UARTSpeed4800,
UARTConfigParity(UARTParityEven,
UARTConfigInit()
)
)
);

Як вишеньки на торті покажемо, що можна контролювати і можливість останньої помилки — пропуску використання сформованої структури, на жаль, ця можливість не відноситься до стандартних мовних засобів і наявний тільки в GCC сімействі — попередження про ігнорування значення, що повертається, для чого функції ініціалізації та установки повинні бути описані, як __attribute__((warn_unused_result)). Чи спрацює цей метод конкретно у Вас, залежить від розробників компіляторів, наприклад, в KEIL спрацювало, а в IAR немає (ні в якому разі не применшення IAR, я до нього добре ставлюся і використовую, але нікуди не дінешся від факту). Існують і інші рішення даної проблеми, засновані на макросах і деякої проміжної змінної часу компіляції, але вони, на жаль, не гарантують результат.

Чому наведений спосіб налаштування особисто я не можу вважати ідеальним, хоча він вирішує всі завдання по виразності і надійності? Виключно в силу його невисокої ефективності з точки зору витрат на виконання, кажучи простіше, в силу його виключної ненажерливості по часу.
Настав час відповісти на питання, поставлене в епіграфі до цього посту, стосовно повернення функцією результату не-примітивного типу. Адже ми не можемо повернути покажчик на нашу внутрішню змінну, оскільки отримаємо попередження компілятора (а якщо попередження відключити, то тоді могли б, ага).
Як же компілятор вирішує це завдання? Не знаю, як це повинно бути, я не читав стандарт мови С (так, це так, і мені не соромно в цьому зізнатися), не впевнений, що там описані тонкощі реалізації, але щодо IAR я просто дивився згенерований код на асемблері і побачив там наступний механізм. При вході у функцію (у тому числі main), яка викликає функцію, яка повертає не-примітивне тип, виділяється місце на стеку, достатню для зберігання змінної даного типу. Далі викликана функція ініціалізації здійснює побитное копіювання своєї внутрішньої змінної на стек під свою адресу повернення і після завершення її роботи на верхівці стека лежить значення повернутого результату. Перед викликом функції зміни значення параметра значення копіюється на стек і передається їй у якості аргументу, а результат знову побітно копіюється зі стека у виділену область пам'яті і доступний звідти для наступної функції зміни значення параметра.
Тобто деякий фрагмент даних (а він може бути досить значного розміру) постійно тягають між виділенням пам'яті (на стеку, між іншим) і верхівкою стека, і, зрозуміло, це не сприяє швидкій роботі.

Як ми можемо збільшити швидкість роботи функцій — змінити тип значення, що повертається, і повертати вказівник, який копіюється ефективно. Але тоді в результаті виконання функції ініціалізації покажчик повинен на щось вказувати, і ми ризикуємо впасти в погану рекурсію. Створення тимчасового об'єкта з наступним його видаленням у функції використання було б непогано, але ми не можемо застосовувати динамічні об'єкти з релігійних міркувань (тільки статика, тільки хардкор).

Повернення вказівника на внутрішню змінну, як не дивно, працює, якщо всі функції налаштування працюють з параметрами однакового розміру (при цьому інформація над верхівкою стека не псується), але все-таки залишає враження танцю на шаблях (саме на шаблях, а не з шаблями, аж надто ризикований трюк). Особливо гострі відчуття Вас чекають при такому рішенні, якщо можна переривання.

Можливе застосування локальної структури, але тоді з'являється її ім'я і турбота про неї передається на тендітні плечі користувача. Тому, як би ми не пручалися долі, глобальна керуюча структура і покажчик на неї видається єдино прийнятним і надійним варіантом. Ви запитаєте, а навіщо тоді її передавати від функції до функції, якщо вказівник не змінюється. Але це єдиний спосіб гарантувати виклик функції ініціалізації, ми могли б передавати не дороговказ, а що-небудь інше, взагалі не має відношення до нашої структури, наприклад, довільне ціле число, але так ми змушуємо користувача викликати саме функцію ініціалізації, причому для типу, з яким ми плануємо працювати, так що тип результату залишаємо незмінним.

З урахуванням перерахованих вище міркувань, отримуємо наступну модифікацію рішення:
typedef UARTConfigT *UARTConfigPT; 
UARTConfigPT UARTConfigInit(void) {
static UARTConfigT UARTConfig;
UARTConfig.UARTSpeed=UARTSpeed9600;
return &UARTConfig; 
};
UARTConfigPT UARTConfigSpeed(UARTSpeedT UARTSpeed,UARTConfigPT UARTConfigP) {
UARTConfigP->UARTSpeed=UARTSpeed;
return UARTConfigP; 
};
int UARTConfigUse(UARTConfigPT UARTConfigP) {
return DO_something(); // власне настройка з можливою помилкою 
};
а застосування залишається без змін.
Швидкодія підвищується в рази, платою за це є невелика кількість пам'яті, яка назавжди резервується під керуючу структуру, яка після ініціалізації апаратури і не потрібна. У той же час, якщо нам потрібно дійсно швидко змінювати режими роботи апаратури, можливість застосування такого рішення є предметом дискусії, але можливість застосування в такому випадку стандартних (універсальних і, тому, неефективних) бібліотек взагалі під великим питанням, швидше за все, Вам доведеться працювати руками.

До речі, ідеальним рішенням, швидкодіючим і не займає пам'яті було б отримання вказівника на що повертається функцією ініціалізації структуру з наступною передачею її по ланцюжку вызовово, але компілятор жорстоко обламав крила моєї фарнтазии, заявиви, що опереатор & застосовується тільки до L-висловом. Я, загалом, розумію, що це означає, то так і не зрозумів, за що вони зі мною так. жорстокі.

Щось забагато вийшло для поста на таку просту тему, але багато місця зайняли відхилення, які, сподіваюся, принесуть задоволення вдумливому читачеві і послужать поштовхом до коментарів та обговорень, що послужить розваги шановної публіки.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.