Sidan 1 av 2 12 SistaSista
Resultat 1 till 10 av 18

Ämne: Mina C/C++ libs

  1. #1

    C++ Mina C/C++ libs

    Under de senaste månaderna har jag börjat se över lite av all den kod som jag knåpat ihop under åren. Det finns en del saker som jag använt mycket under lång tid, och jag har fixat buggar, refaktoriserat och lagt till ett antal gånger. Och nu har jag påbörjat ett mer enhetligt refaktoriseringspass, och tänkte att jag skulle ta och dela med mig av dessa bibliotek framöver. Vissa är små och triviala, men kanske ändå kan spara lite tid, medans andra är betydligt mer omfattande. En genomgående tanke är dock att de ska vara så oberoende av varandra som möjligt. De flesta är 100% stand-alone (förutom ett fåtal mer hög-nivå libs, vilka kan använda sig av vissa av lågnivå biblioteken) och flera är skrivna i C (har bara tagit till C++ när jag kännt att det gett en gedigen fördel). Jag kommer att släppa all kod under två licenser (man får alltså välja vilken man vill använda koden under): public domain och MIT (public domain är ju bäst, men i vissa länder så är den ingen lagligt giltig licens, därav alternativet). Undantaget är ett fåtal bibliotek som använder/bygger på ett annat open source bibliotek - i dessa fall är hela koden släppt under det andra bibliotekets licens (men jag använder bara libs som har MIT-liknande licens, ingen GPL eller så).

    Då jag länge använt (och verkligen gillar) Sean Barret's en-fils libs (https://github.com/nothings/stb), så har jag börjat strukturera mina egna libs på samma sätt. Sean har en beskrivning på vad det innebär, och jag följer det i stort, om än inte i varje detalj: https://github.com/nothings/stb/blob.../stb_howto.txt. Intressant i sammanhanget är även Sean's anteckningar om minnesallokering i libs: http://nothings.org/gamedev/font_rendering_malloc.txt. Och även här håller jag i stort med - mina libs allokerar minne när de behöver, men jag ger den anropande koden möjlighet att overridea allokeringsfunktionerna vid behov. Och jag är i regel väldigt mån om *hur* jag allokerar minne - jag gör inte en massa småallokeringar, utan allokerar i stora block som får växa dynamiskt vid behov.

    Varje lib släpps som en enda källkodsfil, för att göra det så enkelt som möjligt att integrera i projekt (inga konstiga bygg-konfigurationer/projekt filer). Det är värt att notera att det här inte rör sig om en "motor" - det är snarare hjälpsamma byggnadsblock som man kan använda när man bygger en motor. Eller när man bara skriver ett spel direkt i C/C++ utan motor :-)

    Tänkte jag skulle posta dem ett i taget allt eftersom de blir färdiga, med en liten beskrivning om hur de är implementerade, och varför - förhoppningsvis ger det upphov till lite intressant diskussion (ifrågasätt gärna koden!) där både ni och jag kan lära oss nåt :-)

  2. #2
    Det låter perfekt. Gör ju själv mest mikroskopiska spel, så det tar emot att använda sig av en hel motor p.g.a paranoia, kontrollbehov, "kan själv" o.s.v. Små delar är däremot mycket användbara. Gillar tänket att överlåta allokering till lib-användaren. Lycka till!

  3. #3
    Ok, vi börjar med en klassiker (som med många andra av biblioteken har jag postat det här tidigare, men det här är en uppsnyggad version): debug text utritning:

    sysfont.hpp

    Det är väldigt praktiskt att kunna skriva ut debug-text på skärmen utan att behöva ladda någon font/bitmap eller använda några plattforms-specifika systemanrop. Detta lib inkluderar sin font-data som en statisk array, med 1-bit per pixel för effektiv lagring. Den innehåller två olika fonter - båda systemfonterna från DOS, en större i 9x16 format (som jag personligen tycker är en fantastiskt tydlig och läsbar font - använder den som min primära kod font när jag programmerar) och den mindre 8x8 fonten.

    Det finns två huvudsakliga sätt att använda sysfont - antingen kan man anropa sysfont::draw9x16/8x8 för att skriva ut en specifik sträng på angiven position, eller så kan man plocka ut font-datan till en egen textur, via systemfont::texture, för att använda i sin egen debug text renderare.

    Fonterna ser ut så här (och så ser även den textur ut som man får från systemfont::texture):



    Här är ett enkelt textprogram som visar båda funktionerna. Det använder "stb_image_write.h" som finns att ladda ner från länken i första inlägget i den här tråden.

    Kod:
        #include "sysfont.hpp"
    
    
        #define STB_IMAGE_WRITE_IMPLEMENTATION
        #include "stb_image_write.h"
    
    
        int main()
            {
            static unsigned tex[ 256 * 256 ];
            sysfont::texture( tex, 256 * sizeof( unsigned ),  0xffffffff, 0xff000000 );
            stbi_write_png( "texture.png", 256, 256, 4, tex, 256 * 4 );
    
    
            static unsigned test[ 320 * 200 ];
            for( int i = 0; i < 320 * 200; ++i ) test[ i ] = 0xff800000;
            sysfont::draw9x16( test, 320, 200, 20, 40, "A world can only take so much.", 0xffff00ff );
            sysfont::draw8x8( test, 320, 200, 30, 80, "Testing the sysfont lib.", 0xff00ffff );
            stbi_write_png( "test.png", 320, 200, 4, test, 320 * 4 );
        
            return 0;
            }
    Lite om implementationen: jag ville gärna ha så man kunde använda sysfont både för 8-, 16- och 32-bitars output, så för att undvika onödig kod-duplicering är själva utritningen imlementerad i två template-funktioner (för texture och draw), och de utåt sett exponerade funktionerna är bara wrappers som anropar dessa med rätt parametrar. Det gjorde också att jag kunde använda samma funktion för både 8x8 och 9x16 utritning, men ändå få den hyfsat effektiv (även om det här inte är skrivet med high performance i åtanke).

    Värt att notera, då det återkommer i framtida libs, är att jag delar upp den del av header filen som deklarerar det publika API:t, och den del av header filen som implementerar (då vissa saker, som inlineade funktioner och templates, måste definieras i headern). Jag använder två olika include-guards, sysfont_hpp för det publika API:t, och sysfont_impl för det implementations-specifika. Nu behövde jag ju ingen övrig implementation för sysfont (sådan som normalt skulle ha varit i .cpp filen), men om jag hade haft det så skulle man, precis som i Sean's STB libbar, behövt definiera SYSFONT_IMPLEMENTATION i en fil innan inkludering.

    Tanken är att om man bara vill veta hur man anropar ett lib, så ska man kunna sluta läsa efter "IMPLEMENTATION" kommentars-blocket, och det ska i regel inte finnas så mycket (om ens något) implementations-specifikt före det.

    För övrigt så kommer jag att ge C libs extension .h och C++ libs extension .hpp. I regel använder jag namespaces för C++ libs (med ett par undantag) och namn-prefix för C libs. För C++ libs så försöker jag stoppa alla interna, implementationsspecifika, typer och funktioner i ett "internal" namespace (för det här fallet i sysfont::internal). Har märkt att det är ganska hjälpsamt när man har intellisense som försöker föreslå saker (man får inte upp ovidkommande hjälpfunktioner om man inte specifikt går in i "internal").

    Edit: Tänkte att det kanske kunde vara intressant att få en uppfattning om hur stort varje lib är, så har bestämt mig för att köra "cloc" på alla filer allt eftersom jag lägger upp dem. Här är sysfont.hpp
    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.02 s (54.9 files/s, 12836.7 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1             49             68            117
    -------------------------------------------------------------------------------
    Senast redigerat av Mattias Gustavsson den 2014-12-12 klockan 08:55.

  4. #4
    Ok, så nästa lib är betydligt mer komplicerat, men också mycket användbart - en supereffektiv sträng pool:

    strpool.h

    Närhelst man arbetar med mycket strängar, så tycker jag att man med fördel bör använda en strängpool - det ger lägre minnesanvändning och högre prestanda än std::string eller liknande lösningar. Tanken är att man istället för att lagra varje sträng-instans för sig, så lagrar man istället alla strängar i en central pool, och skickar bara runt ett handle till orginalsträngen. Det medför en del fördelar, som t.ex. att sträng-jämförelser är supersnabba (bara jämförelse av handles) och att man dramatiskt kan minska de dyra minnesallokeringarna. Konstruktioner som std::string är väldigt lätta att använda på felaktigt sätt - t.ex. läste jag nyligen om att man profilerat allokeringar i Chrome (web-browsern), och upptäckt att för varje tangent-tryckning i adress-fältet, så görs 25000 minnesallokeringar - de flesta dolda inuti std::string.

    Mitt "strpool" bibliotek är nåt jag filat på till och från i olika inkarnationer under rätt lång tid, så det är rejält optimerat. Det är tänkt som en lågnivå, C-lager för sträng pool, med tanken att man skriver en mer högnivå C++ wrapper för vardagligt bruk (kommer att posta en sådan vid ett senare tillfälle), eller använder det direkt från andra libs. Många av mina libs som använder strängar gör det via "strpool" internt, även om det inte syns i interfacet.

    Ett exempel på hur man kan inkludera, sätta upp, och anropa strpool:
    Kod:
        #define  STRPOOL_IMPLEMENTATION
        #include "strpool.h"
    
    
        #include <stdio.h>
    
    
        int main()
            {
            strpool_config_t conf = strpool_default_config;
            //conf.ignore_case = true;
    
    
            strpool_t pool;
            strpool_init( &pool, &conf );
    
    
            u64 str_a = strpool_inject( &pool, "This is a test string", strlen( "This is a test string" ) );
            u64 str_b = strpool_inject( &pool, "THIS IS A TEST STRING", strlen( "THIS IS A TEST STRING" ) );
        
            printf( "%s\n", strpool_cstr( &pool, str_a ) );
            printf( "%s\n", strpool_cstr( &pool, str_b ) );
            printf( "%s\n", str_a == str_b ? "Strings are the same" : "Strings are different" );
        
            strpool_term( &pool );
            return 0;
            }
    Vid körning kommer detta program att rapportera att strängarna är olika, men om man avkommenterar "conf.ignore_case = true;" så kommer det att säga att de är lika.

    strpool_config_t är en struct som man fyller i för att konfigurera hur en strängpools-instans ska bete sig:
    Kod:
    struct strpool_config_t
        {
        void* memctx;
        int ignore_case;
        int counter_bits;
        int index_bits;
        int entry_capacity;
        int block_capacity;
        int block_size;
        int min_data_size;
        };
    Enklast är att använda den fördefinierade instansen "strpool_default_config", som i exemplet, och sen bara modifiera de värden man vill. "ignore_case" är kanske den viktigaste, då den styr case-sensitivity. Det är egentligen bara nåt jag använder om jag vill hantera IDn som strängar. Den defaultar till att inte ignorera case.

    strpool returnerar handles i form av 64-bitars värden. Men man kan konfigurera hur många av dem som faktiskt används. Ett handle är uppdelat på två delar - counter och index. index används för att referera till den interna strängen i poolen, och counter används för att hålla koll på hur många gånger ett visst index har återanvänts för olika strängar (counter används för att kunna detektera om ett handle är giltigt eller ej). Och via index_bits och counter_bits kan man styra hur många bitar man använder till respektive ändamål. T.ex. kan man sätta båda till 16, och då kan man casta handle-värdena från u64 till u32 utan problem. Eller man kanske vet att man kommer att ha mestadels kortlivade strängar, och därmed avsätta fler bits till "counter", eller så kanske man inte behöver handle-validering alls, och då kan man sätta "counter-bits" till 0. Som sagt är det här för mer lågnivå och biblioteks användning, så i vardaglig användning så kan man lika gärna köra 32 bits vardera för index_bits och counter_bits.

    Som jag nämnde i första inlägget, så kan man styra om minnesallokeringen till sina egna funktioner. Detta gör man genom att göra några #define innan man inkluderar definitionen. Om man inte gör några egna defines, så är default vanliga malloc, realloc och free:
    Kod:
        #define sp_malloc( ctx, size ) ( malloc( size ) )
        #define sp_realloc( ctx, ptr, size ) ( realloc( ptr, size ) )
        #define sp_free( ctx, ptr ) ( free( ptr ) )
    Man måste alltså definiera alla tre för att det ska fungera. När de anropas, så är första parametern, ctx, det värde som man skickade in i "memctx" medlemmen i strpool_config_t parametern, och den kan man ignorera om man vill (som i default fallet) eller casta till något relevant som man skickade in.

    "entry_capacity" används för att specificera hur många sträng-slots som strpool ska allokera vid initiering. Default är 4096, och när alla initiala slots använts upp, så dubblas kapaciteten och slotsen allokeras om. Så oavsett vad man sätter detta värde till, så får man korrekt funktionalitet, men man kan få bättre prestanda/minnesanvändning genom att tweaka det för sitt speciella fall.

    "min_data_size" anger hur mycket minne som minst ska allokeras för en sträng, inklusive dess hash-värde (som är 4 bytes). Default är 16, och om man sätter det till mindre än 8 så ignoreras det (8 är minsta giltiga värde). Det här är också en inställning för att tweaka performance, och i regel kan man lämna det på 16 bytes. strpool återanvänder minne från strängar som inte längre används, och för att snabba upp denna reallokering ser den till att varje allokerad chunk är en två-potens. Om man vet med sig att de flesta strängar är väldigt långa, då kan man uppa "min_data_size" och "slösa" lite extra utrymme på de få korta strängar man har, för snabbare reallokering för det typiska fallet. Eller om man har massor av kort-korta strängar, kanske man sätter det till 8 för att minimera minnesanvändningen.

    "block_capacity" och "block_size" har att göra med allokeringen av minne för själva sträng-datat. I en tidigare implementation gjorde jag så att all strängdata lagrades i ett enda minnesblock, som reallokerades när det var fullt. Men strängdata kan vara ganska stort, så det kunde innebära ganska mycket kopiering av data vid reallokering, och dessutom så innebar det att en vanlig "const char*" pekare som man plockat ut från poolen, kunde invalideras om man skapade en ny sträng, vilket ledde till förvirrande buggar i praktiken. Så nu allokerar jag ett "block" i taget, och när detta är fullt, så allokeras ett nytt block. Delar av ett block kan återanvändas om man frigjort strängar, men så länge en sträng finns i systemet kommer dess "char*" pekare att vara densamma. "block_capacity" anger den initiala storleken på arrayen som lagrar block (den växer med det dubbla vid behov), och "block_size" är storleken på varje block (men det kan komma att allokeras större block än så, om man försöker lagra en sträng som är större än block_size").


    Så till själva API:t. strpool_init och strpool_term används för att skapa och städa upp en strängpools-instans (man kan för övrigt ha hur många instanser man vill, med olika konfigurationer - men då kan man förstås inte jämföra strängar från olika pooler genom att bara jämföra deras handles - det funkar bara för strängar från samma pool).

    För att lägga till en sträng i strängpoolen anropar man "strpool_inject", som returnerar ett handle till strängen, vilket används i en del andra anrop. Om strängen redan fanns i poolen, så får man samma handle som förra gången den lades till. Man kan plocka bort en sträng ur poolen med "strpool_discard", vilket kommer att göra strängens handle ogilitig för framtida operationer.

    För att få en vanlig "const char*" pekare till en tillagd sträng, anropar man bara "strpool_cstr". Denna pekare är giltig så länge strängen finns i strängpoolen, men man bör inte lagra den direkt, utan istället lagra dess handle, och anropa "strpool_cstr" varje gång man behöver värdet istället (det är en snabb operation), eftersom ett handle ju kan bli ogiltigt om dess sträng plockas bort ur strängpoolen. strpool_cstr returnerar 0 om man skickar in ett ogilitigt handle.

    Eftersom strängpoolen ändå håller reda på varje strängs längd, så finns även en funktion "strpool_length", som från ett handle kan returnera motsvarande strängs längd. Man använder med fördel denna funktion istället för att anropa "strlen" på resultatet från "strpool_cstr", då det är mer effektivt.

    strpool har även stöd för referensräkning, men den måste hanteras manuellt (det här är som sagt låg-nivå och tänkt att wrappas för vardagsbruk). "strpool_incref" och "strpool_decref" används för att öka/minska räknaren för en sträng, och "strpool_getref" kan användas för att plocka ut dess värde.

    Värt att notera, är att "strpool_discard" inte plockar bort en sträng om dess ref count är större än noll. Så om man aldrig anropar strpool_incref, då kommer refcount att vara 0, och strpool_discard kommer att plocka bort strängen direkt. Men anropar man "strpool_incref" så måste man ha motsvarande "strpool_decref" för att strpool_discard ska fungera. En bra praxis är att alltid anropa "strpool_discard" direkt efter varje "strpool_decref" anrop, för då kommer strängen att plockas bort om, och bara om, dess räknare nått 0.

    Strängpoolen har även en funktion "strpool_defrag", som "tightar till" minnesanvändningen lite. Denna operation invaliderar alla pekare som plockats ut med "strpool_cstr", men alla handles fortsätter vara giltiga. Om en strängpool används ett tag, och man lägger till och tar bort massor med strängar, då kan det hända att alla de interna arrayerna växer sig stora för att rymma "peak usage", och fastän användningen sen går ner, så är minnet fortfarande allokerat. strpool_defrag allokerar om alla strukturer så de är antingen så stora som vid den ursprungliga allokeringen, eller precis så stora som behövs för nuvarand belastning (vilket som nu är störst av de två). Dessutom stoppar den in all strängdata i ett enda block, vilket gör vissa operationer lite snabbare. Man ska egentligen aldrig behöva anropa strpool_defrag, och den är en relativt långsam operation, men vill man så kan man ju anropa den när man byter level eller liknande.

    Slutligen finns också en funktion "strpool_collate", som man kan använda för att plocka ut en fullständig lista över alla strängar som för närvarande hålls i poolen. Man skickar in en pekare till en variabel som man vill ha antalet strängar skrivet till, och får tillbaka ett allokerat minnesblock med varje sträng, den ena efter den andra, med noll-terminator mellan varje. Anroparen har ansvaret för att frigöra minnet - antingen med "free" eller med vad man möjligen omdefinierat "sp_free" till. Det här är en ganska specialiserad funktion - jag lade till den när jag skrev min compiler för ett tag sen, för att enkelt kunna få ut en "string table". Men den känns användbar, så den fick bli en permanent del :-)

    Rent implementationsmässigt, så kan jag nämna att systemet använder en effektiv hashtabell - strukturerad enligt dataorienterade principer så att data som används tillsammans ligger tätt packade i minnet.

    En fiffig sak är att om man skickar in en sträng till strpool_inject, så börjar den funktionen med att kolla om den pekaren kommer från en av sina interna minnesblock (genom att bara kolla om adressen ligger mellan blockets start och slut), vilket då innebär att man skickat in en sträng som man fått via ett anrop till "strpool_cstr". När strängar lagras internt, så läggs strängens hash-nummer in direkt innan själva sträng-datat, så systemet behöver inte räkna om hash-nummer för strängar som den redan känner till - ens om de skickas in som char* pekare. Då kan den istället bara göra en direkt lookup i hashtabellen efter rätt handle. Det här är bra, för om man använder strpool som en intern struktur i ett lib, så behöver man inte exponera strängar i form av handles i det externa API:t - man kan jobba med char* pekare, men ändå få ungefär samma prestanda.

    Det blev längre än jag tänkt, så får räcka så. Man får fråga om man undrar något.

    Senare kommer jag att lägga upp min C++ wrapper för strängpools-strängar, och den är betydligt mer lättanvänd.

    Och slutligen så behöver vi ju en "cloc" också. 703 rader kod, ganska mycket, men helt ok med tanke på allt man får för det :-)

    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.03 s (32.9 files/s, 31022.6 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1            155             85            703
    -------------------------------------------------------------------------------

  5. #5
    Nästa lilla lib är betydligt enklare - skapande och inläsning av INI-filer.

    ini.h

    Det är ju inte så vanligt att använda just INI-filer nu för tiden - ofta ser man format som XML, YAML, JSON eller liknande användas. Men jag tycker att när man bara har en handfull enkla konfigureringsvärden eller inställningar som man vill ladda och/eller spara, då är INI-filer ett trevligt format, just eftersom det är så enkelt att parsea. De flesta XML/JSON/YAML/etc bibliotek är ganska bloated, och har ofta lite för mycket dependencies för min smak. Det är också vanligt att de allokerar en massa minne, eller sköter fil-operationer annorlunda än vad jag skulle vilja. Och visst, om man behöver den extra flexibiliteten i de formaten, då kan det ju vara värt det, men ibland räcker det fint med nåt enklare.

    En INI-fil består ju av key/value par, som man, om man vill, kan organisera under olika sektioner. Här är ett test-exempel:
    Kod:
    FirstSetting=Test
    SecondSetting=2
    
    
    [MySection]
    ThirdSetting=Three
    För att ladda in ovanstående fil, och lösa ut ett par av dess värden, gör man något i följande stil:

    Kod:
    // Loading an ini file and retrieving values
    #define INI_IMPLEMENTATION
    #include "ini.h"
    
    
    #include <stdio.h>
    #include <malloc.h>
    
    
    int main()
        {
        FILE* fp = fopen( "test.ini", "r" );
        fseek( fp, 0, SEEK_END );
        int size = ftell( fp );
        fseek( fp, 0, SEEK_SET );
        char* data = (char*) malloc( size + 1 );
        fread( data, 1, size, fp );
        data[ size ] = '\0';
        fclose( fp );
    
    
        ini_t* ini = ini_load( data );
        free( data );
        int second_index = ini_find_property( ini, INI_GLOBAL_SECTION, "SecondSetting" );
        char const* second = ini_property_value( ini, INI_GLOBAL_SECTION, second_index );
        printf( "%s=%s\n", "SecondSetting", second );
        int section = ini_find_section( ini, "MySection" );
        int third_index = ini_find_property( ini, section, "ThirdSetting" );
        char const* third = ini_property_value( ini, section, third_index );
        printf( "%s=%s\n", "ThirdSetting", third );
        ini_destroy( ini );
    
    
        return 0;
        }
    Som man kan se här, så gör mitt ini-lib inga egna fil-operationer - den anropande koden måste själv läsa in filen, och skicka dess innehåll som en null-terminerad sträng till ini_load. Det här är en genomgående praxis i mina lib - i regel låter jag alltid den anropande koden sköta fil-operationer. Då får man större frihet i hur man hanterar inläsningen (t.ex. att hämta fil-datan från en server, eller läsa från en PAK/WAD/ZIP fil eller liknande). För att förenkla det dagliga användandet har jag en annan hjälp-lib som sköter inläsning av en fil - den postar jag vid senare tillfälle.

    Man kan förstås också skapa en ny INI-fil:
    Kod:
    // Creating a new ini file
    #define INI_IMPLEMENTATION
    #include "ini.h"
    
    
    #include <stdio.h>
    #include <malloc.h>
    
    
    int main()
        {        
        ini_t* ini = ini_create();
        ini_property_add( ini, INI_GLOBAL_SECTION, "FirstSetting", "Test" );
        ini_property_add( ini, INI_GLOBAL_SECTION, "SecondSetting", "2" );
        int section = ini_section_add( ini, "MySection" );
        ini_property_add( ini, section, "ThirdSetting", "Three" );
    
    
        int size = ini_save( ini, 0, 0 );
        char* data = (char*) malloc( size );
        size = ini_save( ini, data, size );
        ini_destroy( ini );
    
    
        FILE* fp = fopen( "test.ini", "w" );
        fwrite( data, 1, size, fp );
        fclose( fp );
        free( data );
    
    
        return 0;
        }
    Inga konstigheter här heller, men värt att notera hur man går till väga för att spara ut datan. Precis som i ladda-fallet, så gör INI libbet inga fil-operationer. Istället får man skicka in en data-buffer som man vill ha filen skriven till, och sedan får man spara ut den bufferten till fil. Hur vet man då hur stor buffert man behöver skicka in? Man skulle ju kunna chansa, och skicka in en buffert som "borde" vara stor nog :P Men ini_save gör två smarta saker för att möjliggöra en bättre lösning. Det första är, att den returnerar hur stor buffert den hade behövt, oavsett hur stor/liten buffer man skickade in. Så man kan därmed veta om man skickade in en buffer som var för liten. Det andra är, att om man skickar in en NULL pekare som buffer, då skriver den ingen data alls - men returnerar fortfarande hur stor buffer den hade behövt. Det används i exempelkoden ovan, för att få veta hur stor buffert som behövs.

    ini_save ser till att skriva en null-terminator till slutet av buffern, så man kan använda den som en sträng vid behov.

    Implementationen är ganska rättfram. Dock så gör jag en ansträngning för att minimera allokeringar (att allokera minne, via new/malloc, är ju sjukt långsamt), genom att dels lagra alla properties i en enda array, istället för att ha en array per section, och dels genom att undvika allokering om namn/värde-strängar är korta ( mindre än 32 tecken för namn, mindre än 64 tecken för värden - om namnen är längre görs en extra allokering). Jag har upprepade gånger förvånats av hur långsamt det kan bli om man gör en massa små-allokeringar hela tiden, så har mer eller mindre gjort det till en vana att göra såhär (eller ännu hellre använda strpool.h, men det hade ju verkligen varit overkill för detta lib).

    EDIT: Jag glömde nämna, att precis som i strpool.h, så kan man styra om minnesallokeringsfunktionerna till sina egna - i detta fall genom att definiera ini_malloc, ini_realloc och ini_free innan man inkluderar implementationen. Defaults är förstås:
    Kod:
    #define ini_malloc( ctx, size ) ( malloc( size ) )
    #define ini_realloc( ctx, ptr, size ) ( realloc( ptr, size ) )
    #define ini_free( ctx, ptr ) ( free( ptr ) )
    En skillnad mot strpool.h är dock, att eftersom vi inte har en "config" struct där vi kan peta in vårt minnescontext, så finns det alternativa versioner av ini_create och ini_load, nämligen ini_create_memctx och ini_load_memctx som tar en extra parameter för minnescontextet.


    Och så kör vi en cloc som vanligt:
    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.02 s (65.1 files/s, 50201.9 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1            154            114            503
    -------------------------------------------------------------------------------
    500 rader kod. Det var mer än jag trodde, det är ju nästan lika mycket som strängpoolen... Oväntat. Men det är ganska många funktioner i det här API:t, med en del olika varianter på vissa anrop (t.ex. versioner för både null-terminerade strängar och icke-terminerade sträng&längd variant, när man hanterar properties) och då byggs det på rätt snabbt.
    Senast redigerat av Mattias Gustavsson den 2015-01-08 klockan 08:38.

  6. #6
    Tackarr! Kommer nog användas till många program.

  7. #7
    Eftersom jag nämnde den i förra inlägget, så tänkte jag att det är lika så bra att posta min "file helper" också:

    file.h

    Det här är ju rent trivial kod - bara en väldigt enkel wrapper runt koncepten "allokera minne och läs in en fil". Men kanske kan komma till användning ändå :-)

    Man använder den så här:
    Kod:
    #define FILE_IMPLEMENTATION
    #include "file.h"
    
    
    #include <stdio.h>
    #include <string.h>
    
    
    int main()
        {
        char const* test = "This is just a test file.\nTest test test.";
        file_save_data( test, strlen( test ), "test.txt", FILE_TEXT );    
    
    
        file_t* file = file_load( "test.txt", FILE_TEXT );
        if( file && file->size > 0 )
            printf( "%s", file->data );
        
        file_save( file, "test_copy.txt", FILE_TEXT );    
        file_destroy( file );
        return 0;
        }
    Inte så mycket att säga om API:t - det sparar och laddar filer. Som vanligt kan man overridea dess allokeringar genom att definiera file_malloc, file_realloc och file_free. Och precis som i ini.h så finns det varianter på funktionerna som kan ta emot ett minnescontext.

    file.h hanterar både text och binärfiler. Skillnaden är dels den vanliga som kommer med "fopen" - att i text mode så kollapsas "carriagereturn-linefeed" par, alltså "\r\n" till bara linefeed, "\n", vid inläsning (och tvärtom vid utskrivning) och dels så ser den till att allokera minne för och lägga till en null-terminator, "\0", efter den inlästa datan (i text mode, inte i binärt). Detta extra tillägg ökar dock inte på "size" fältet i file_t. Men det är väldigt praktiskt om man vill kunna hantera den inlästa datan som en null-terminerad string. När man sparar ut en textfil så ignoreras den avslutande nollan.

    Ett par saker om implementationen: När man ska läsa in en hel fil tilll minnet på det här sättet, så är det ett vanligt pattern att man öppnar filen, gör en fseek till slutet på filen, och gör en ftell för att få reda på dess storlek - sedan allokerar man, gör en fseek till filens början, och läser in datan. Detta funkar ju, men kan bli onödigt sökande och göra det långsammare vid stora filer. Så i min implementation använder jag "stat" kommandot för att direkt efterfråga filens storlek, innan jag ens öppnat den.

    Jag bör även nämna själva file_t structen, som returneras när man anropar load/creatr. Den är definierad såhär:
    Kod:
    struct file_t
        {
        void* memctx;
        size_t size;
        char data[ 1 ]; /* "open" array - it is [size] elements long, not [1]. */
        };
    "data" fältet är en så kallad "öppen" array - ett koncept som man t.ex. stöter på här och där i Windows API. Detta innebär alltså att "data" fältet fortsätter utanför den definierade structen. När "file_t" allokeras, så allokerar jag utrymme för memctx plus size plus så många bytes som behövs för fildatat. Så även fast "data" är deklarerat som en byte, kan man alltså göra "file->data[240] = 10", förutsatt att "file->size" är större än 240 då.

    Fördelen med detta är att informationen om fildatan (dess storlek, "size") och själva datan ("data") lagras som en enhet, och dessutom tillsammans, vilket blir lite effektivare. Det ser kanske lite lustigt ut om man inte är van vid komceptet, och är inget jag använder speciellt ofta, men här kändes det lämpligt.

    Så, en cloc också:
    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.01 s (101.6 files/s, 24073.1 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1             56             74            107
    -------------------------------------------------------------------------------
    Där ser man, 107 friska rader av enkel fil-läsar kod :-)

  8. #8
    Tänkte jag skulle ta nåt lite mer användbart den här gången - ett system för att detektera minnesläckor och mäta minnesanvändning:

    memtrack.hpp

    I C++ är det ju ganska lätt att man glömmer att göra "free" eller "delete" på minne som man allokerat. Med det här systemet får man en sammanställning efter avslutad körning, som berättar hur mycket minne man använde som mest, och vilka allokeringar som aldrig frigjordes. Det finns ju en del olika verktyg för att mäta sånt här, och som har all möjlig avancerad funktionalitet - och för all del, använd dem när det passar. Men när man bara vill ha något enkelt och grundläggande, och som man lätt kan göra små tillägg i vid behov, då är det här ett bra alternativ.

    Systemet är för C++ och trackar alla allokeringar via new/delete, men inte de som görs via malloc/realloc/free. Dock finns det tre hjälpfunktioner, memtrack::Alloc, memtrack::Realloc och memtrack::Free, som man kan använda istället. Jag har inte brytt mig om att göra så att memtrack funkar för både C och C++... men om man verkligen skulle behöva en C version så är det ganska trivialt att strippa bort all C++ (namespace och new/delete operatorerna).

    Exempel på hur det kan användas:
    Kod:
    #define MEMTRACK_ENABLE 1
    #define MEMTRACK_IMPLEMENTATION
    #include "memtrack.hpp"
    
    int main()
        {
        void* a = memtrack::Alloc( 64 );
        int* b = new int[ 256 ];
    
        memtrack::Free( a );
        // Intentionally not deleting b here, to illustrate leak detection.
        return 0;
        }
    Precis som i de andra biblioteken definierar man MEMTRACK_IMPLEMENTATION i en fil för att få med implementationen. Men här har man även en MEMTRACK_ENABLED som man måste definiera, och man kan sätta den till 1 för att slå på tracking, och 0 för att stänga av tracking (vilket man helst bör göra för release-byggen, då tracking gör allokering långsammare och tar mer minne).

    I exemplet ovan, så allokeras minne dels via memtrack::Alloc, och dels via "new". Men det som allokeras via "new" frigörs aldrig, vilket ju leder till en minnesläcka. När man kör ovanstående program, får man följande utskrift i visual studios Output fönster efter körningen (eller till stdout på andra kompilatorer):
    Kod:
    Total memory allocations: 2
    Peak memory use: 1088 bytes
    
    Total still allocated: 1024 bytes
    Memory leak, allocation [ 1 ], size 1024 bytes
    De första två raderna rapporterar hur många allokeringar som gjordes under körningen, och hur mycket minne man som mest hade allokerat samtidigt vid något tillfälle under körningen. Dessa rader skrivs alltid ut om man kör med tracking påslagen. Därefter rapporteras eventuella minnesläckor - först hur mycket minne man totalt läckt, och sedan skrivs det en rad för varje läcka man har, som anger dels hur många bytes den var på, och dels numret på den allokering som man aldrig frigjorde. I det här exemplet, så frigör vi allokering nummer 0, men inte allokering nummer 1.

    Det riktigt fiffiga, är att om man får en minnesläcka som i det här exemplet, så kan man modifiera sitt program till att stanna (med en breakpoint i debuggern) på en allokering med ett visst nummer. Då är det riktigt enkelt att hitta var allokeringen görs, genom att bara köra programmet igen (så länge allokeringarna sker i samma ordning - i praktiken har jag sällan upplevt det som ett problem). Så här gör vi för att bryta vid allokering nummer 1 i vårt exempel:
    Kod:
    #define MEMTRACK_ENABLE 1
    #define MEMTRACK_IMPLEMENTATION
    #include "memtrack.hpp"
    
    int main()
        {
        memtrack::BreakOn( 1 ); // <<<<< Instruct memory tracker to break on allocation number 1
    
        void* a = memtrack::Alloc( 64 );
        int* b = new int[ 256 ];  // <<<<< When program is run, debugger will break on this line
    
        memtrack::Free( a );
        // Intentionally not deleting b here, to illustrate leak detection.
        return 0;
        }
    Så länge man kommer ihåg att använda memtrack::Alloc, memtrack::Realloch och memtrack::Free istället för malloc/realloc/free, så är det här ett väldigt säkert sätt att hitta minnesläckor. "new" och "delete" kan man använda som vanligt, de trackas automatiskt - oavsett om man inkluderar memtrack.hpp i filen som man gör new/delete från eller inte. Det stora problemet är tredjepartsbibliotek som anropar malloc/realloc/free. Detta är en av anledningarna till att jag alltid ger möjlighet att omdefiniera allokeringar i mina egna libs - i regel ger jag dem alltid definitioner som ser ut nåt sånt här:
    Kod:
    #define INI_IMPLEMENTATION
    #define ini_malloc( ctx, size ) memtrack::Alloc( size )
    #define ini_realloc( ctx, ptr, size ) memtrack::Realloc( ptr, size )
    #define ini_free( ctx, ptr ) memtrack::Free( ptr )
    #include "ini.h"
    Inget context behövs ju för memtrack, så jag kan ignorera den parametern. Men detta möjliggör iallafall trackning av allokering i mina libs. Om man är en sån som använder STL (jag gör t.ex. aldrig det), så kan man implementera "custom allocators" för dem, och därigenom lägga till tracking. Men generella tredjepartslibs blir det svårare med, om de inte har stöd för custom allocators på nåt sätt (men alla *bra* libs borde stödja det).

    Apropå omdefiniering av allokerare, så kan man definiera om memtracks interna allokeringsfunktioner. Per default definieras de såhär:
    Kod:
        #define mt_malloc( size ) malloc( size )
        #define mt_realloc( ptr, size ) realloc( ptr, size )
        #define mt_free( ptr ) free( ptr )
    Men till skillnad från de flesta av mina andra libs, så finns här inget stöd för ett minnescontext. Jag ville inte behöva ha nåt specifikt initieringsanrop i det här systemet, då det kändes viktigare att det kickar igång så fort en första allokering görs. Man kan fortfarande ha ett context om det behövs, men då får man ha det i en global variabel istället, i just det här fallet.

    memtrack har även lite andra småfunktioner, för att t.ex. plocka ut "peak" och "current" minnesanvändning när som helst (användbart om man t.ex. vill skriva ut det på skärmen), och att plocka ut en rapport (till stdout/Output window) vid andra tillfällen än vid programslut.

    När det gäller implementering finns väl kanske inte så mycket att säga - är en ganska rättfram implementation, med en dynamisk array som sparar allokeringsinformation, och globalt överlagrade new/delete operatorer. Jag använder "atexit" för att anropa min "report" funktion vid avslut. Det är egentligen inte så mycket av varken komplexitet eller kod i detta system (vilket vår "cloc" här nedanför visar), men det är oerhört användbart, och jag har numera svårt att tänka mig att skriva nån större mängd kod *utan* ett sånt här skyddsnät.

    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.02 s (60.8 files/s, 26319.3 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1            110             60            263
    -------------------------------------------------------------------------------

  9. #9
    En favorit i repris - min lösning för att få fram ett statiskt typ-id, utan att behöva använda C++'s RTTI system.

    type_id.hpp

    Det här kan möjligen vara den mest användbara kod jag nånsin skrivit :P Jag använder aldrig RTTI, eftersom det lägger på en icke försumlig overhead för ALLA klasser i koden, inte bara de som man behöver det för. Men det kan vara otroligt kraftfullt att ha ett abstrakt sätt att beskriva typer, speciellt om man jobbar med template kod. type_id kan anropas t.ex. såhär.

    Kod:
        #include "type_id.hpp"
        #include <assert.h>
    
    
        template< typename A, typename B > void assert_if_not_same_type( A a, B )
            {
            type_id_t type_a = type_id( a );
            type_id_t type_b = type_id<B>();
            assert( type_a == type_b );
            }
    
    
        int main()
            {
            assert_if_not_same_type( 10, 20 ); // SUCCEEDS
            assert_if_not_same_type( 1.0f, 2 ); // FAILS
            return 0;
            }
    Även om det där exemplet kanske inte direkt demonstrerar hur användbart konceptet är. Så, låt oss titta på ett mer gediget exempel. I en annan tråd, skrev jag om ett Game State system , där en specifik funktionalitet jag ville ha var att GameState systemet skulle hållla reda på alla övergripande system, så att individuella states kan hämta dem från GameStateSystemet vid behov, så man slipper ha dem som globala singletons (vilket fortfarande är ett förvånandsvärt vanligt sätt att göra det på). En naiv implementation skulle kunna se ut såhär:

    Kod:
    class Systems
    {
    private:
        InputSystem* inputSystem_;
        ResourceManager* resourceManager_;
        Renderer* renderer_;
    public:
        Systems() : inputSystem_( 0 ), resourceManager_( 0 ), renderer_( 0 ) { }
        void SetInputSystem( InputSystem* inputSystem ) { assert( !inputSystem_ ); inputSystem_ = inputSystem; }
        void SetResourceManager( ResourceManager* resourceManager ) { assert( !resourceManager_ ); resourceManager_ = resourceManager; }
        void SetRenderer( Renderer* renderer ) { assert( !renderer_ ); renderer_ = renderer; }
        InputSystem* GetInputSystem() { assert( inputSystem_ ); return inputSystem_; }
        ResourceManager* GetResourceManager() { assert( resourceManager_ ); return resourceManager_; }
        Renderer* GetRenderer() { assert( renderer_ ); return renderer_; }
    }
    Det här är ju väldigt enkel kod. Pekare till varje system som en privat variabel, som konstruktorn initierar till 0. Accessors för Get/Set, med lite enkla asserts för att se till att man inte försöker skriva över en pekare som redan är satt (för att hitta fel där man av misstag assignar flera system, t.ex) och inte försöker plocka ut en pekare som inte är satt. Och det här funkar ju. Man anropar bara "systems->GetInputSystem()" och så har man sin pekare. Jag har sett flera AAA spel där man gjort just så här.

    Problemet är bara, att med tiden växer sig den filen större och större. Fler och fler system tillkommer, och man får hoppas att alla kommer ihåg alla stegen - att noll-initiera i konstruktorn, att asserta på get och att asserta på set. Men frestelsen blir dock ganska stor att ändra på konventionerna. Kanske man tänker, att nåt system kan vara "optional", så just det ska inte asserta om det är 0. Man kommer på nya koncept som man tycker inte riktigt passar in på det existerande system-tänket, men Systems klassen finns ju redan, och är enkel att bara dumpa in nya "globala" saker i. Jag har sett exempel på såna här klasser som haft många hundra olika funktioner för att hämta eller manipulera data - även fast de börjat som bara en enkel container för att lagra pekare i.

    Så jag skulle hellre vilja implementera det med ett sådant här interface - inte exakt så här, och jag kommer snart att förklara varför, men enligt denna filosofi:
    Kod:
    enum SystemType
    {
        SYSTEM_INPUTSYSTEM,
        SYSTEM_RESOURCEMANAGER,
        SYSTEM_RENDERER,
    };
    
    class ISystem
    {
    public:
        virtual SystemType GetType() = 0;
    }
    
    class Systems
    {
    public:
        Systems();
        void Add( ISystem* system );
        ISystem* Get( SystemType type );
        void Remove( SystemType type );
    }
    Det här är ju ganska standard OOP - man skapar en interface-klass, ISystem, som bl.a. exponerar funktionalitet för att avgöra vilken faktisk typ implementationen är, och så får varje system implementera det. Systems klassen blir då bara en slags container, där man kan stoppa in system, och sedan plocka ut och ta bort dem baserat på deras SystemType. Inte så snyggt att anroparen behöver göra en cast på resultatet, och viss risk att det går fel (som man dock kan kringå genom att lägga till nån slags QueryInterface funktionalitet a la COM), men det funkar i princip, och man har nu en betydligt renare implementation av Systems, som inte direkt inbjuder till att man hackar in en massa specifika fall.

    Det största problemet för mig, är dock ISystem interfacet. Vi har nu helt plötsligt introducerat en basklass som alla system måste ärva från, bara för att vi ska kunna skicka runt dem. Det medför att vi t.ex. inte längre kan skicka runt system-instanser från tredjepartsbibliotek, utan måste wrappa dessa i något som kan ärva från ISystem. Alla system i vår motor är nu knutna till vår ISystem klass. Det känns ju helt onödigt.

    Så med hjälp av lite enkel template kod, skulle jag föreslå något i stil med detta:
    Kod:
    class Systems
    {
    public:
        Systems();
        template< typename T > void Add( T* system );
        template< typename T > T* Get();
        template< typename T > void Remove();
    }
    Notera att vi fortfarande jobbar med pekare och system-typ, men typen är nu en template parameter istället för ett enum-värde, och pekarna är inga pekare till gemensam basklass, utan den faktiska pekaren för systemet. Man behöver inte längre ärva från någon basklass, och GetSystem returnerar nu en pekare till rätt typ, så man behöver ingen cast.

    Om man då kollar på implementationen av systemet:
    Kod:
    class Systems
    {
    private:
        struct System
        {
            void* system;
            type_id_t type;
        };
        
        System* systems_;
        int count_;
        int capacity_; 
    
        void* GetSystem( type_id_t type ) 
        { 
            for( int i = 0; i < count_; ++i )
            {
                if( systems_[ i ].type == type )
                {
                    return systems[ i ].system;
                }
            }
            return 0;
        }
    public:
        Systems() : count_( 0 ), capacity_( 256 )
        {
            systems_ = (System*) malloc( capacity_ * sizeof( System ) );
        }
    
        ~Systems()
        {
            free( systems_ );
        }
    
        template< typename T > void Add( T* system )
        {
            type_id_t type = type_id<T>();
            assert( !GetSystem( type );
            if( count_ >= capacity ) { capacity_ *= 2;  systems_ = (System*) realloc( systems_, capacity_ * sizeof( System ) ); }
            systems_[ count_ ].system = (void*) system;
            systems_[ count_ ].type = type;
            ++count;
        }
        
        template< typename T > T* Get()
        {
            type_id_t type = type_id<T>();
            void* system = GetSystem( type );
            assert( system );
            return (T*) system;
        }
    
        template< typename T > void Remove()
        {
            type_id_t type = type_id<T>();
            for( int i = 0; i < count_; ++i )
            {
                if( systems_[ i ].type == type )
                {
                    systems[ i ] = systems[ count_ - 1 ];
                    --count_;
                    return;
                }
            }
            assert( false );
        }
    }
    I grunden är det ganska enkelt - vi lagrar registrerade system som void-pekare i en array (som vi dynamiskt reallokerar vid behov. Somliga skulle ha använt en std::vector, det går precis lika bra), och vi håller manuellt reda på varje systems typ, så vi kan hitta det senare och även göra en safe cast tillbaka till ursprungstypen.

    Det är just här som det är så användbart att kunna avgöra typen. Att kunna lagra en pekare till ett objekt, vilket som helst, som en void*, och sen ändå kunna göra en säker cast tillbaka till ursprungstypen, är guld värt. Vi behöver inte ha nåt speciellt interface för våra system. Vi behöver inte ärva från någon bas-klass, eller göra wrappers för tredjeparts-system. Anropet för att plocka ut ett system blir lite annorlunda än i ursprungskoden: "systems_->Get<InputSystem>()" istället för "systems_->GetInputSystem()" eftersom vi nu måste ange en explicit template-parameter. Men det känns det ju värt för att få ett så mycket robustare system.

    Och så den sedvanliga "cloc" rapporten:
    Kod:
    http://cloc.sourceforge.net v 1.62  T=0.01 s (75.8 files/s, 10464.1 lines/s)
    -------------------------------------------------------------------------------
    Language                     files          blank        comment           code
    -------------------------------------------------------------------------------
    C/C++ Header                     1             38             72             28
    -------------------------------------------------------------------------------
    Det här är den klart minsta koden jag postat hittills - bara 28 rader. Men oumbärlig funktionalitet, som jag kommer att använda i många framtida libs jag postar.
    Senast redigerat av Mattias Gustavsson den 2015-01-12 klockan 10:17.

  10. #10
    Har nu börjat lägga upp dessa på github, för enklare tracking: https://github.com/mattiasgustavsson/libs

Bokmärken

Behörigheter för att posta

  • Du får inte posta nya ämnen
  • Du får inte posta svar
  • Du får inte posta bifogade filer
  • Du får inte redigera dina inlägg
  •