/ janturon.cz / Výuka / C / Tvorba knihoven

Tvorba knihoven

Statické a dynamické knihovny

Představte si, že třeba funkce numbers1 z předchozí kapitoly je natolik skvělá, že jste se rozhodli použít ji i v jiném programu. To zkušení programátoři často dělají: nelopotí se s megabajty zdrojového kódu, ale mají spousty užitečných funkcí pro programy, na které se specializují a jejich programování není nic jiného, než um tyto funkce znovupoužít, doupravit pár drobností a vytvořit požadované řešení v mnohem kratším čase.

Nemůžeme ale prostě překopírovat soubor numbers1.o, protože ten ke správnému fungování vyžaduje kód putint, který neobsahuje. Závislosti ve větších projektech mohou být složitě provázané a oddělit kód tak může být problém. Proto se znovupoužitelné části kódu, které řeší nějaký konkrétní problém sdružují do větších celků zvané knihovny. Ty mohou podle způsobu použití být:

Objektový formát

MinGW používá univerzální objektový formát ELF, kde bývá zvykem dávat statickým knihovnám příponu .a (archive) a dynamickým .so (shared objects). Ve Windows je běžnější formát PE dávající statickým knihovnám příponu .lib a dynamickým .dll (dynamic linked library).

Tvorba knihovny

Statická knihovna není nic jiného než archiv objektových kódů, který lze vyrobit nástrojem ar:

ar rcs libnumbers1.a numbers1.o putint.o ar rcs libnumbers2.a numbers2.o putint.o

Je důležité, aby knihovna začínala na lib a měla příponu .a. Tyto knihovny (už bez putint.o) pak můžeme kamkoliv zkopírovat, využívat jejich funkce a program sestavit třeba pomocí

gcc main.o -L. -lnumbers1 -lnumbers2 -o main.exe

Kde:

Dynamické knihovny vytvoříme přepínačem -shared (smysluplnější by bylo dát vše do jedné knihovny, zde pro ilustraci budou knihovny dvě):

gcc -shared -o numbers1.so numbers1.o putint.o gcc -shared -o numbers2.so numbers2.o putint.o

a program následně sestavíme:

gcc main.o numbers1.so numbers2.so -o main.exe

Protože jsou knihovny strojový kód, můžeme je teoreticky použít i v jiných jazycích. V C tak můžeme tvořit nízkoúrovňové funkce pro vyšší jazyky, které mají jinou specializaci. C je pro svou efektivnost, a přitom stále relativně (ve srovnání s assemblerem) srozumitelnou syntaxí oblíbeným nástrojem pro tvorbu ovladačů.

Prakticky však nebyl knihovnám stanoven žádný standard, a proto vznikly zmatky způsobené rozdílnými volacími konvencemi a přejmenováváním.

Volací konvence

Určuje, jakým způsobem bude zpracována funkce na úrovni strojového kódu. Dnes se používají:

Někde se lze dodnes setkat se zastaralými __pascal a __fortran, specifické pro tyto jazyky.

Přejmenovávání

C++ narozdíl od C umožňuje přetěžování funkcí. Je možné kupříkladu deklarovat:

int foo() { ... } void foo(int) { ... }

Z pohledu zdrojového kódu je to přetížená funkce, kde si kompilátor vybere verzi, která se hodí do kontextu. Z pohledu objektového kódu jsou to dvě různé funkce, které jsou přejmenovány na foo_v a foo_i. Těmto ozdobám se říká name mangling.

Stejnětak je možné různé funkce volat různou konvencí. __stdcall funkce void foo(int) by se přejmenovala na foo_i@4, kde číslo za @ udává počet bytů nutných alokovat na zásobníku pro argumenty funkce.

Ještě spletitější je to u konvence Thiscall a díky chybějícím standardům to různé kompilátory řeší jinak a je v tom zmatek: knihovna jednoho výrobce velmi pravděpodobně nebude fungovat v kompilátoru jiného výrobce (konflikt může nastat i u kompilátoru jednoho výrobce: u jiné verze nebo jiného OS).

Existují nástroje jako dlltool které umožní funkce objektového kódu přejmenovat. Knihovna je přenositelná, pokud si cílový kompilátor poradí s použitou volací konvencí. V C++ lze převod usnadnit uzavřením kódu do bloku extern "C" zakazující kompilátoru provádět name mangling. V tom případě však nelze použít techniky C++ neznámé v C: přetěžování či šablony (bude o nich v další kapitole). Nekompatibilní přejmenování vám odhalí linker hromadou hlášek unresolved symbol a nějakých podivných jmen obsahující název funkce z knihovny.

Rozhraní knihovny

Všem prvkům v knihovnách se říká symboly. Např. knihovna numbers1 obsahuje symboly number1 a putint. Některé symboly v knihovnách mohou být pomocné (zde putint), je proto vhodné je znepřístupnit pro volání zvenčí (zpřístupnění se zde říká exportování). To se kdysi řešilo přes tzv. importní knihovnu a definiční soubor. Není to jednoduchý postup, naštěstí dnes existuje mechanismus přímého linkování:

MSVC kompilátor umožňuje nestandardní deklaraci __declspec(dllexport), např. __declspec(dllexport) void foo(int);. V knihovně budou viditelné (exportované) pouze symboly takto označené.

MinGW kompilátor standardně exportuje všechny symboly. Je však možné použít následující kompilační parametry:

Rozumí i __declspec(dllexport) a pokud je některý symbol takto označený, exportují se jen takto označené symboly. Potom ale pozor:

Preprocesor

Dilema s import/export umí vyřešit preprocesor pomocí větvení. To lze příkazy #ifdef, #else a #endif:

#ifdef LIBRARY #define DLL __declspec(dllimport) #else #define DLL __declspec(dllexport) #endif

Příkaz #define identifikátor kód definuje makro: preprocesor nahradí identifikátor (bývá zvykem psát ho velkými písmeny) kódem. Můžeme pak funkci deklarovat jako DLL void numbers1(int n). Makro můžeme krom příkazu #define deklarovat parametrem -D při kompilaci. Knihovnu tedy můžeme zkompilovat

gcc -DLIBRARY -shared numbers1.c // v .h souboru knihovny bude dllimport

a potom načíst v kódu jako

gcc numbers1.so main.c -o main.exe // v .h souboru v main.c bude dllexport

Starší kódy místo #pragma once používaly příkaz #ifndef, který je opak #ifdef.

#ifndef _NUMBERS1_H_ #define _NUMBERS1_H_ // kód načtený pouze jednou #endif

Makro s parametry

Makru můžeme dát parametry, např.:

#define FORI(from,to) for(int i=from; i<to; i++)

pak můžeme v kódu volat např.:

FORI(1,10) printf("%d",i); // 123456789

což umožňuje vytvořit si vlastní jazyk. Pozor, preprocesor i zde funguje jako copy&paste, takže například makro

#define SQR(n) n*n SQR(1+1)

nebude fungovat dle očekávání. Správná definice by byla

#define SQR ((n)*(n))

Poznámky ke knihovnám

 

Otázky