4 apr 2018

Konfigurera CMake för att lyckas

Configuring-CMake-for-success.jpg

Jag gillar att använda moderna verktyg och strukturer när jag utvecklar så jag kan fokusera på att ge nytt värde till användarna. Jag använder unittester och CI så att jag kan bärsärk-refaktorisera när det behövs utan att vara rädd för att introducera nya buggar. Jag följer dessa regler både professionellt och för mina privata sidoprojekt.

Nyligen har jag och en kollega (Stephen Lau) jobbat på en Augmented Reality app för att lösa en Rubiks Kub på en Android telefon. Vi har utvecklat detektering och lösningsalgoritmer i C++ för det ger oss tillgång till bibliotek som OpenCV tillsammans med prestandan som vi behöver. 

Ett av de första problemen som vi sprang på var hur lång tid det tog att skicka över och starta applikationen på en telefon efter att vi hade gjort justeringar i våra algoritmer. Att starta appen tog 20-30 sekunder och dessutom var debugging långsam och vi kunde inte reproducera gamla testresultat när vi jobbade med en riktig kamera. Låter som att unittester är den perfekta lösningen där vi också kan testa mot en lista av bilder som vi sparat ner från telefonen för att detektera regressioner och prestanda för varje version. Och vi insåg även att vi kan köra unittesterna direkt på desktop utan att blanda in Android vilket ger en stor prestanda vinst på byggtiderna.

En vanlig fråga som kommer upp på vår kurs i Avancerad C++ är: hur ska jag strukturera mitt projekt? Ska jag dela min källkod över många kataloger? Var ska jag lägga mina unittester? Ska jag använda statiska bibliotek?

Varje projekt är unikt i vad som behövs, så i den här blogposten går jag först igenom hur jag föredrar att strukturera mina projekt och sen vilka ändringar som behövs för C++ på Android där vi också har bindningar mot Kotlin/Java.

Generell setup för ett C++ projekt

Jag föredrar att använda CMake, jag gillar att det finns en fil som konfigurerar projektet, oavsett om jag använder CLion, XCode eller Microsoft Visual Studio som min IDE.

Jag delar in min källkod efter följande biblioteksstuktur:

Screen Shot 2018-04-03 at 15.44.07.png

Jag kompilerar alla källkodsfiler till ett statiskt bibliotek som används av mitt exekverbara program, förutom main funktionen som jag håller separat. Det statiska biblioteket länkas också in i unittesterna.

Där finns många variationer som man kan göra från den här strukturen. Om jag skapar ett bibliotek med ett externt API så lägger jag till include/libProject under src och lägger publika headerfiler där. Privata headerfiler behåller jag i källkods trädet bredvid respektive implementationsfil. Externa API användare får lägga till projectRoot/src/include till deras include path, och kan sen importera headerfilerna med #include <libProject/header.h>.

Ibland har jag multipla test-targets, som unittester, prestandatester och end-to-end tester. I sådana fall lägger jag till ett extra lager med bibliotek under tests för att separera de olika typerna av test.

Om projektet har externa beroenden så föredrar jag att använda https://github.com/Crascit/DownloadProject  för att ladda ner dem via CMake. Det här gör att CMakeLists.txt kommer att innehålla versionsnumret på mina beroenden och därmed sparas de även som en del av revisionshistoriken. Detta gör att jag kan återskapa gamla versioner (vilket är väldigt trevligt när buggrapporterna börjar rulla in), och gör det lättare för andra att bidra till mitt projekt. 

Jag försöker hålla mig till att bara ha en typ av källkod per bibliotek, exempelvis att externa headerfiler ska vara separata från implementationsfilerna. Min erfarenhet är att detta gör det enklare att skriva installationsskript och gör det lättare att ha olika linter-regler för publika header filer jämfört med resten av projektet.

C++ på Android med desktop tester

Now let’s turn to a C++ application on Android. The UI handling is written in Kotlin, and it has bindings into a C++ library for performance or other reasons via the Java Native Interface (jni). The jni layer we have is just a thin layer on top of native library. A diagram to clarify:

Låt oss nu titta på vårt Android projekt. UI är implementerad i Kotlin och har bindningar mot ett C++ bibliotek. Vårt jni lager är väldigt tunt och skickar bara vidare vår data ner i vårt bildbehandlingsbibliotek. Här är ett diagram som förklaring:

image3.png

Vi kompilerar libPalindrome flera gånger med olika toolchains (desktop och Android). När vi bygger för Android så kommer Gradle att exekvera CMake åt oss. Vi sätter upp vårt projekt så att vi även kan köra CMake direkt för att generera vårt C++ projekt. Exempelvis så kompilerar vi bara unittesten när vi bygger för desktop, och i CMakeLists.txt ser det ut så här:

if (NOT CMAKE_SYSTEM_NAME STREQUAL "Android")

   add_executable(unittests …)

endif()

Största delen av konfigurationen delas mellan desktop och Android byggen. Katalogstrukturen för projektet ser ut så här:

Screen Shot 2018-04-04 at 10.40.37.png

Vi gjorde några ändringar i katalogstrukturen jämfört med den generella uppsättningen för att passa bättre med hur gradle/Android projekt brukar vara strukturerade.

Jag använder två olika IDE:er när jag utvecklar. För att koda på Android delen så öppnar jag rotkatalogen 03-android-jni med Android Studio, och jag öppnar app katalogen med CLion när jag vill utveckla med unittesten på desktop.

Sammanfattning

Jag har beskrivit en generell struktur för ett C++ projekt och hur vi adapterat strukturen för ett Android projekt. Vi använder desktop versionen för att testa bilanalys algoritmen mot ett set av bilder på Rubiks Kuber från olika miljöer med olika ljusförhållanden, orientering etc. Att köra alla testerna tar cirka sju sekunder så jag får direkt återkoppling, och eftersom vi alltid kör på samma bilder så blir det lätt att detektera regressioner och att felsöka problem vid detekteringen. 

Att starta Android applikationen kan ta uppemot trettio sekunder och använder sen en riktig kamera vilket gör det svårt att upptäcka regressioner från olika ljusmiljöer. Vi har istället en knapp i UI:t för att spara nuvarande bild så vi kan testa bilden senare från vår desktop miljö. De här bilderna låter utökar också vår testsuite så vi hela tiden testar vår kod mot de svåraste bilderna vi har sett. 

https://github.com/edumentab/cpp-project-example finns några exempelprojekt där du kan du se projektstrukturen fungerar med modern CMake stil.

Relaterade kurser

  • Avancerad C++

    Vill du lära dig om de nya standarderna för C++, och ta din programmering till nästa nivå? Kursen Avancerad C++ fokuserar på hur du drar nytta av nyheterna i de moderna versionerna av C++. Kursen ger dig verktyg så du kan arbeta enligt de senaste riktlinjerna och designmönstren och skriva kod som evolverar långt in i framtiden. 

    Kursområde: C++
    Omfattning: 2 dagar
    Kostnad: 21 500 SEK