Как да направите вашия C код междуплатформен
Може би някой, четейки заглавието, ще попита: „Защо да правите нещо с вашия код ?! В крайна сметка C++ е междуплатформен език! Като цяло това е така ... но само досега няма низове за конкретните възможности на компилатора и целевата платформа ...
В реалния живот разработчиците, решаващи конкретен проблем за конкретна платформа, рядко си задават въпроса „Това точно отговаря ли на стандарта C ++? Ами ако това е разширение на моя компилатор? Те пишат кода, изпълняват компилацията и поправят местата, които техният компилатор е проклел.
В резултат на това получаваме приложение, което до известна степен е „пригодено“ за конкретен компилатор (и дори за неговата конкретна версия!) И целевата ОС. Освен това, поради недостига на стандартната библиотека на C++, някои неща просто не могат да бъдат написани без използването на специфичния API на системата.
Така беше и при нас в Tensor. Писахме в MS Visual Studio 2010. Нашите продукти бяха 32-битови Windows приложения. И, разбира се, кодът беше пълен с всякакви връзки с технологията на Microsoft. След като решихме, че е време да изследваме нови хоризонти: беше време да научим VLSI да работи под Linux и други операционни системи, беше време да опитаме да преминем към друг хардуер (POWER).
В тази поредица от статии ще ви разкажа как направихме нашите продукти истински междуплатформени приложения; как са ги накарали да работят на Linux, MacOS и дори под iOS и Android; как са стартирали своите приложения на различни хардуерни архитектури (x86-64, POWER, ARM и други); как са учили да работят на машини с голям порядък.
В основата на всички наши продукти е нашата собствена рамка „VLSI платформа“ (наричана по-нататък „Платформата“), която е сравнима по мащаб с Qt. Платформата има почти всичко, от което се нуждае един разработчик: от прости функции за бързо преобразуване на число в низова форма домощен сървър за приложения, устойчив на грешки.На базата на Платформата нашите разработчици внедряват своите продукти (дори мобилни приложения), които решават всякакви бизнес проблеми. Искахме да освободим техния код (по-нататък ще наричаме техния код „приложен“) от всички възможни връзки с целевата софтуерна и хардуерна платформа, скривайки всички специфики в дълбините на нашата рамка.
Нашата компания активно развива своите продукти, така че беше необходимо да "ремонтираме влака с пълна скорост" :)
Беше необходимо да работим по такъв начин, че другите разработчици да не страдат от нашите дейности и да продължат удобно да развиват своята функционалност под Windows на MSVC. Това изискване повлия силно на много технически решения и значително усложни работата.
За да може читателят да формира представа за мащаба на работата, ще дам някои цифри:
-
Обхватът на нашия рамков код
2 милиона реда
Използване на API на операционната система
Както бе споменато по-горе, стандартната библиотека на C++ е много оскъдна, тя не включва много от функциите, които са необходими навсякъде. Например в C++11 няма функционалност за работа с мрежата ... Тоест, щом искаме да направим най-простата HTTP заявка, ние сме принудени да ... пишем некросплатформен код!
Ситуацията се влошава още повече, ако не използвате най-новата версия на компилатора, както направихме - в MSVS 2010, отвратителна поддръжка за C ++ 11, няма огроменчаст от иновациите в ядрото на езика и в стандартната библиотека.
Но, за щастие, такива проблеми се решават доста лесно. Има няколко начина:
- Ние пишем наш собствен клас с няколко специфични за платформата реализации, базирани на API извиквания на целевата система. По време на компилация ние избираме подходящото внедряване с помощта на директиви за препроцесор ifdef.
- Ние използваме крос-платформени библиотеки – има много готови крос-платформени библиотеки (отново използвайки специфични за платформата имплементации вътре в тях), които значително улесняват нашата задача. Например взехме cURL, за да внедрим HTTP клиент.
Характеристики на реализациите на компилатора
Всяка програма има грешки. И компилаторът не е изключение. Следователно дори код, който е 100% съвместим със стандарта, може да не бъде асемблиран на някой компилатор.
Също така, почти всички разработчици на компилатори считат за свое задължение да добавят функции към своите потомци, които не са предоставени от стандарта, и по този начин провокират програмистите да пишат непреносим код.
Какво получаваме в резултат? Кодът, който е написан ясно в съответствие със стандарта, може да не се компилира на някои компилатори; кодът, който се компилира и изпълнява на един компилатор, може да не се компилира или да работи правилно на друг...
Човек може да изброи много проблеми от този клас. Ето един от тях:
Този код ще се изгражда в MSVC++, тъй като те имат дефиниран допълнителен конструктор:
За съжаление няма общи техники за решаване на подобни проблеми. В тези случаи помага само опитът, натрупан при изучаването на инструментите, използвани в работата, и доброто познаване на стандарта C ++.
Недефинирано поведение
Недефинирано поведение (английско недефинирано поведение, в редица източници непредвидимоповедение [1][2]) е свойство на някои езици за програмиране (най-вече в C), софтуерни библиотеки и хардуер в определени маргинални ситуации да произвеждат резултат, който зависи от изпълнението на компилатора (библиотека, чип) и случайни фактори като състоянието на паметта или задействаното прекъсване. С други думи, спецификацията не дефинира поведението на езика (библиотека, чип) във всички възможни ситуации, но казва: "при условие A, резултатът от операция B е недефиниран." Допускането на такава ситуация в програмата се счита за грешка; дори ако програмата работи успешно на някакъв компилатор, тя няма да бъде междуплатформена и може да се провали на друга машина, в друга операционна система или с различни настройки на компилатора.
Ако позволите недефинирано поведение във вашата програма, това изобщо не означава, че тя ще се срине или ще даде някакви грешки на конзолата. Такава програма може да работи според очакванията. Но всяка промяна в настройките на компилатора, преминаване към друг компилатор или версия на компилатор или дори модификация на която и да е част от кода може да промени поведението на програмата и да счупи всичко!
Много ситуации с недефинирано поведение на един конкретен компилатор произвеждат постоянно едно и също поведение и вашето старателно тествано приложение ще работи като швейцарски часовник. Но веднага щом променим средата (например, опитаме се да стартираме програма, компилирана от друг компилатор), тези грешки започват да се проявяват и напълно прекъсват програмата.
Класически пример за недефинирано поведение е препълване на масив в стека. По-долу е даден опростен кодов фрагмент от едно от нашите приложения с този проблем. Този бъг не се прояви по никакъв начин под Windows в продължение на няколко години и "изстреля" едва след товапренасяне за Linux:
Очевидно MSVS подравнява буфера в стека, добавяйки няколко байта след него, и когато презаписваме паметта на някой друг, се оказваме на празно, неизползвано място. И в GCC проблемът започна да се проявява по интересен начин - програмата се срина далеч от този код, в друга функция (очевидно GCC вгради тази функция и започна да пренаписва локалните променливи на друга функция).
Има и по-елегантни, фини ситуации с UB. Например, много интересен рейк може да бъде настъпен при използване на std::sort:
Изглежда къде може да е UB тук? И всичко е свързано с "лошия" компаратор. Компараторът трябва да върне true, ако s1 трябва да бъде поставен преди s2. Помислете какво ще произведе нашият компаратор, ако като вход са дадени два празни низа:
s1 = ""; s2 = ""; cmp( s1, s2 ) == вярно => s1 трябва да идва преди s2 cmp( s2, s1 ) == true => s2 трябва да е преди s1
И това не е измислен пример. Уловихме такъв проблем при преминаването към Linux. Компаратор с подобна грешка работи много години под Windows и ... започна да прекъсва приложението със SIGSEGV под Linux (i686). Интересното е, че бъгът се държи различно дори на различни дистрибуции на Linux (с различни GCC на борда): някъде приложението се срива, някъде замръзва, някъде просто сортира не според очакванията.
Често ситуации с недефинирано поведение могат да бъдат уловени от статични анализатори (включително вградени в компилатора). Следователно в настройките за изграждане винаги трябва да задавате максималното ниво на предупреждения. И за да не загубите полезно предупреждение в тълпата от предупреждения като „неизползвана променлива“, е полезно един ден да почистите кода и след това да включите опцията за изграждане „третирайте предупрежденията като грешки“, за да предотвратите появата на новинезабелязани предупреждения.
Модели на данни
Стандартът C++ не дава твърди и бързи гаранции за представянето на типове данни в компютърната памет; той определя само някои съотношения (например sizeof(char) ". В Windows този код ще се компилира, но в Linux ще получите грешка, че файл с име "myfolder\file.h" не е намерен.
Но, за щастие, избягването на подобни проблеми е много просто - просто приемете правилата за именуване на файлове (например, именувайте всички файлове с малки букви) и се придържайте към тях и винаги използвайте "/" като разделители на пътя (Windows също го поддържа).
За да елиминираме напълно досадните грешки, ние добавихме проста кука към нашите git хранилища, която проверява дали директивите за включване отговарят на тези правила.
Освен това характеристиките на FS засягат самото приложение. Например,
Ако имате код, който "залепва" пътеки през нормални операции за конкатенация на низове и използва "\" като разделители, той ще се повреди, защото при някои операционни системи разделителят ще бъде приет като част от името на файла.
Разбира се, можете да използвате '/', но на Windows изглежда грозно и като цяло няма гаранция, че няма да има ОС, която да използва друг разделител.
За да разрешим този проблем, използваме библиотеката boost::filesystem. Позволява ви да формирате правилно пътя за текущата система:
Заключение
Разработването на междуплатформен софтуер в C++ не е тривиална задача. Вероятно е невъзможно да се напише програма, която да работи на различни софтуерни и хардуерни платформи без допълнителни усилия. Да, и разработете голяма програма в C ++, която без промени ще се сглоби правилно на всеки компилатор за всяка операционна система и за всеки хардуер,Не можете, въпреки факта, че C++ е междуплатформен език. Но ако се придържате към някои от правилата, които обобщих в тази статия, ще можете да пишете код, който ще работи на всички платформи, от които се нуждаете. И няма да е толкова трудно да прехвърлите тази програма на нова операционна система или хардуер.
Като цяло, за да напишете междуплатформен код, ви трябва:
-
Добре е да познавате стандарта C++, за да разберете какво е позволено в него и какво е разширение на даден компилатор или дори води до недефинирано поведение.
Откажете да използвате системния API в кода, като капсулирате специфичен за платформата код в някои класове или използвайте готови междуплатформени библиотеки.
Вземете под внимание възможните разлики в типизирането, не зависете от свойства на базови типове, които не са гарантирани от стандарта C++. За да направите това, можете да използвате типове с фиксирана дължина от C++ Standard Library.
Решете формата за представяне на низове в програмната памет. Тук може да има много опции. Например, използвайте UTF-8, както се прави в много програми, или дори преминете към „широки“ низове, абстрахирайки се изцяло от формата за представяне на низове.
Можете да помогнете и да прехвърлите средства за развитието на сайта