C++11 fejlesztőeszközök

Czirkos Zoltán · 2024.08.20.

C++11 fordítók, parancssori fordítás és linkelés.

A mai alkalom a unixos fejlesztőeszközökről szól: parancssorból használható fordítóprogramokról, linkerekről és nyomkövetőkről. Ezért ma minden fordítást szándékosan a parancssorból fogunk elvégezni. Ennek célja az, hogy lássuk, „mi van a motorháztető alatt”, azaz mi az a munka, amelyet egy integrált fejlesztőkörnyezet a háttérben elvégez.

A programunk szegmentálása nem áll meg a függvényekre bontásnál: az összetartozó függvényeket különálló modulokba csoportosítjuk. A könnyű, hatékony kezelhetőség érdekében pedig több forrásfájlból építjük fel a programot. A feldarabolt, külön fordított programrészletet végén a linker segítségével rakjuk össze. Ezen laboralkalom célja a linkelés technikai részleteinek bemutatása is.

A laborokhoz

A laborok mellé minden héten lesz kiírva egy beadandó az admin portálon. Ide óra végén töltsd fel a forráskódokat (*.cpp, *.h) egy ZIP fájlba csomagolva! A feladatokat ezért külön projektben oldd majd meg, ne írd felül a megoldásokat.

Labor otthoni munkában

A feladatok kifejezetten unixos parancssori eszközökről szólnak. A lehetőségeid az alábbiak:

  • MinGW-ben dolgozol Windowson, pl. a CodeBlocks mellé telepített verzióval.
  • Saját Linuxon dolgozol.

A feladatokban nem programokat kell írni, ettől függetlenül keletkeznek majd általad megírt fájlok, azokat kell beadni.

A teljesítéshez az első négy feladatot meg kell oldani.

1. Fejlesztőeszközök

Nyiss egy parancssort vagy egy terminálablakot! A fordítóprogramok verziószáma a -v paraméterrel ellenőrizhető:

Linux
$ g++ -v
gcc version 4.8.2 (Ubuntu 4.8.2-19ubuntu1)
HSZK-ban: Windows és
CodeBlocks
C:\> cd C:\Program Files\MinGW-4.8.1\bin

C:\Program Files\MinGW-4.8.1\bin> g++ -v
gcc version 4.8.1 (tdm-2)

Windowson a telepítés módjától függően a Unix-kompatibilis MinGW fejlesztőkörnyezetek más mappába is települhettek. Az alapbeállítás C:\MinGW-4.8.1. A fenti példa a HSZK-ban használt C:\Program Files\codes.v52\MinGW-4.8.1 mappát mutatja (ez lehet, hogy változott). A konkrét programok az ezen belüli bin mappában vannak.

Ha a HSZK-ban vagy, a mai laboron végzett munkához használd végig ugyanazt a parancssori ablakot, és állítsd be abban a PATH környezeti változót, hogy a GCC programjai bármelyik mappából elérhetőek legyenek:

C:\> PATH=%PATH%;C:\Program Files\MinGW-4.8.1\bin

2. Program fordítása, futtatása parancssorban

Indítsd el a Geany-t (Linuxon) vagy a CodeBlocks-ot (Windowson), és mentsd el a proba.c fájlba ezt a programkódot:

#include <stdio.h>

int main(void) {
    for (int i = 1; i <= 5; ++i)
        printf("Hello %d!\n", i);
}

A Geany-hez érdemes tudni:

  • Ctrl+S mentés.
  • Shift+nyilak kijelölés, Ctrl-C, Ctrl-V szokásos.
  • Ctrl+K sort töröl.
  • Ctrl+D sort vagy kijelölést dupláz, Ctrl+E kommentel.
  • F9 fordít és linkel, F5 futtat.

A programot alapbeállítás szerint valószínűleg nem fogja lefordítani a GCC fordító, mert annak nem alapértelmezése a C99 nyelv. A for() ciklus fejlécében definiált int i változó viszont C99 nyelvi elem. Használd ezért a terminál ablakot (parancssort), és fordítsd le kézzel a programot:

$ gcc -std=c99 -Wall -Wdeprecated -pedantic proba.c -o proba

$ ./proba   Windowson: proba
Hello 1!
Hello 2!
Hello 3!
Hello 4!
Hello 5!

A fordításhoz Linuxon használható a clang is. A clang paraméterezését úgy találták ki, hogy teljesen csereszabatos legyen a gcc-vel, így a laboranyagban leírtak érvényesek mindkettőre. A Geany beállításához lásd a fejlesztőkörnyezetekről szóló írást!

A program futtatásakor Linuxban azért kellett megadni az elérési utat: (./ a név elején), mert Linux rendszerekben az aktuális könyvtár biztonsági okokból nem része a PATH-nak. Windowson elérési út beírása nélkül is indíthatóak az aktuális mappa programjai.

A fenti opciók jelentése:

  • -std=c99: C99 módba kapcsolja a fordítót. Szokásos értékei még: c89, c11.
  • -Wall: bekapcsolja a figyelmeztetéseket. (A -W még több figyelmeztetést mutat.)
  • -Wdeprecated: engedélyezi a figyelmeztetéseket az olyan nyelvi elemek használatánál, amelyek elavultnak számítanak, és van helyettük modernebb megoldás.
  • -pedantic: szigorú szabványkövető módba kapcsol. Tanulásnál hasznos!
  • -o proba: megadja a kimeneti fájl, jelen esetben a futtatható (bináris) nevét. Windowson ebből automatikusan proba.exe lesz.

A laborban használni kell majd még ezeket is:

  • -c: csak fordít, a linkelést nem végzi el (lásd lentebb).
  • -g: nyomkövetéshez használható információ elhelyezése a programban (melyik függvény hol van stb.).
  • -O0: optimalizálás nélkül fordítja a programot (nyomkövetéshez).
  • -O2: a legtöbb optimalizálási módszert bekapcsolva fordítja a programot.

3. Fordítás és linkelés

Tárgykód fájlok

Írj egy új programot hello.c néven, amelyben a kiírás külön függvényben van!

#include <stdio.h>

static void hello(int i) {
    printf("Hello %d!\n", i);
}

int main(void) {
    for (int i = 1; i <= 5; ++i)
        hello(i);
}

Emlékezz vissza: a fordítás mindig két lépésből áll. Az első lépésben (fordítás, compile) a fordítóprogram (compiler) a forráskódot gépi kóddá alakítja, és ekkor keletkeznek a tárgykód (object) fájlok. Ezek még a globális változók és függvények név szerinti hivatkozásait tartalmazzák. A tárgykódokból lesz a linkelés (összeállítás, összeszerkesztés, „linkelés”, link) művelet után a futtatható program. A linkelés közben a név szerinti hivatkozásokat a linker feloldja. Ennek az egésznek az (is) az értelme, hogy különálló, kicsi, áttekinthető forrásfájlokból lehessen egy nagy programot építeni.

Ezeket a műveleteket a fordító megfelelő paraméterezésével külön is el lehet végezni. A -c paraméterrel azt lehet jelezni, hogy csak fordítani kell, linkelni nem. Ilyenkor a -o a létrehozandó tárgykód fájl nevét adja meg. Ha paraméterként tárgykód fájlokat kap a fordító, akkor pedig tudja, hogy azokat lefordítani már nem kell, csak linkelni. (A linkelést igazából egy külön program, az ld végzi, de azt bonyolultabb közvetlenül paraméterezni.)

Végezd el külön a fenti projekt fájljainak fordítását és linkelését! Előbb a fordítást:

$ gcc -g -std=c11 -O0 main.c -c -o main.o

Ekkor előállt a main.o tárgykód fájl. A parancssori nm alkalmazás megmutatja, hogy mi van egy tárgykód fájlban. (Linuxon a man nm beírása után olvashatsz róla részletesebben.)

$ nm main.o

A kimeneten azt látod, hogy a tárgykód fájl tartalmaz két lefordított függvényt (ezeket t és T betűk jelölik, ami a text szóra utal). Van benne egy hivatkozás a printf-re, ezt pedig az U csoportba sorolja a program. Az U undefined-ot, definiálatlant jelent: ahhoz, hogy ezt a tárgykód fájlt programként használjuk, elő kell majd kerítenünk a printf() függvény definícióját, lefordított változatát.

Linkelés

A tárgykódot linkelve a fordító ezt a libc-ből (a szabványos C lefordított függvényeit tartalmazó függvénykönyvtár, C Library) előkeresi a printf()-et, és így futtatható programot kapunk:

$ gcc main.o -o program

$ ./program  Windows: program.exe

Unixon a linkeléshez használható a -lc paraméter is, amely azt jelenti, hogy linkelni „-l” kell a „c” nevű könyvtárat is. Ez annyira alapvető, hogy ezt kérés nélkül is meg szokta tenni a linker.

Az ldd paranccsal ellenőrizni lehet, hogy a bináris mely könyvtárakhoz van linkelve. Ez a parancs csak Unixokon van, a MinGW-ből kihagyták. Ugyancsak, a printf() a MinGW-ben kicsit másképp néz ki; van némi hókuszpókusz a háttérben, hogy a MinGW-s kódból a Microsoft Visual C-ben lévő printf() elérhető legyen. Ezek most nem lényegesek.

A statikus függvény

Emlékezz most vissza arra is Prog1-ből, mit jelentett a static kulcsszó a függvény neve előtt. Ennek a többmodulos programoknál volt értelme: a static globális változó vagy függvény esetén egy olyan tárolási osztályt jelent (linkage), amely csak az adott fordítási egységben látszik (internal linkage). A nem statikus nevek, mint pl. a main, a teljes projektben látszanak, így egyediek kell legyenek. A statikus neveknek azonban elég csak fordítási egységenként egyedieknek lenniük; egy másik valami.c fájlban lehetne egy másik hello() függvény is.

A hello() tehát csak a mostani main.c forrásfájlból hívható függvény. Fordítsd le most a programot optimalizálással együtt (-O0 helyett -O2 kapcsolóval), állítsd elő így a tárgykódot és vizsgáld meg az nm paranccsal! Látsz valami különbséget? Mi okozza ezt a különbséget? Magyarázd meg!

$ gcc -g -std=c11 -O2 main.c -c -o main.o

$ nm main.o
Megoldás

A függvény definíciója ilyenkor valószínűleg eltűnik a tárgykód fájlból. Ez azért van, mert kicsi, rövid függvény lévén a fordító úgy dönt az optimalizálás, gyorsabb futás érdekében kihagyja a függvényhívást, és a függvény törzsét beilleszti a hívás helyére, megspórolva így az ide-oda ugrást és a paraméterátadás műveleteit. Mivel a static miatt a fordító látja, hogy sehol máshol nem hívódik a függvény, teljesen kihagyja azt a tárgykódból.

Több forráskód

Bontsd most két forráskódra a programot! A main.c-ben legyen a main() függvény, a hello.c-ben a hello() függvény, és írj az utóbbihoz egy hello.h fejlécfájlt is!

Fordítsd le linkelés nélkül külön a két forrásfájlt, majd vizsgáld meg az nm paranccsal a kapott tárgykódokat! Linkeld össze a két tárgykódot teljes programmá! Hogyan kellett mindehhez a hello() függvényt módosítanod? Azt kell elérni, hogy a tárgykód fájlba úgy kerüljön a hello() függvény, hogy máshonnan is elérhető legyen (exportált függvény; exported function, function with external linkage).

Megoldás

Egyszerű: le kell róla szedni a static jelzőt. Különben a main() nem fogja látni, hiába van deklarálva a hello.h-ban, és hiába van definiálva a hello.c-ben.

A fordítás lépése:

$ gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c main.c -o main.o
$ gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c hello.c -o hello.o
$ gcc -g main.o hello.o -o program

Globális változók

Módosítsd újra a projektet! Hozz most létre egy lang nevű globális változót a hello.c-ben, amelynek a típusa legyen enum Language, a lehetséges értékei legyenek: language_en és language_hu. Add ennek a változónak a language_en kezdeti értéket! Írd át úgy a hello() függvényt, hogy ennek figyelembe vételével jelenítse meg az üzenetet! Írd át a főprogramot is:

int main(void) {
    hello(0);           // angol (kezdeti érték)
    lang = language_hu;
    hello(1);           // magyar
    lang = language_en;
    hello(2);           // megint angol
}

Minek, hol kell lennie ehhez? Mi különbözteti meg egy globális változó deklarációját a definíciójától? Mi a különbség egyáltalán a deklaráció és a definíció között? Nézd meg most is a tárgykód fájlokat!

Megoldás

A felsorolt típus definíciója a fejlécfájlban, a hello.h-ban, mert azt mindkettő fordítási egységnél látnia kell a fordítónak:

typedef enum Language {
    language_en, language_hu
} Language;

A változó definíciója a forrásfájlba, hello.c-be kell:

Language lang = language_hu;

És végül, a változó deklarációja a fejlécfájlba. Ehhez az extern kulcsszót kell használni, ettől válik deklarációvá a sor:

extern Language lang;

Változónál és függvénynél a deklaráció azt jelenti, hogy megadjuk a típusát; a definíció pedig azt, hogy lefoglaljuk hozzá a memóriaterületet. A deklarációra a többi programrész fordítása közben van szükség, a definíció hatására pedig ténylegesen kerül is valami a tárgykódba.

4. A make program

A fordítás és linkelés különválasztásának akkor van igazán értelme, ha nagy projekten kell dolgozni. Ha nem mindegyik forrásfájl változik az újrafordítások között, akkor sok tárgykód fájl változatlan maradhat. A mostani projektben például a main.c változásakor a main.o-t kell újra előállítani, az hello.o viszont változatlan maradhat. Így rengeteg idő megspórolható. Az alábbi gráf élei a függőségeket (dependency) mutatják, vagyis azt, hogy melyik fájl tartalmától melyik másik fájl tartalma függ.

Ha ismertek a függőségek, akkor a keletkező fájlok előállítása automatizálható:pl. ha egy tárgykód fájl újabb, mint a belőle linkelt program, akkor a linkelést újra el kell végezni. Erre képes a make program. A függőségeket ennek egy ún. Makefile írja le, aminek a neve mindig Makefile. Ez szabályokat (rule) tartalmaz. Minden szabály egy cél fájlt ad meg és annak függőségeit, továbbá parancsokat, amelyekkel a cél fájl előállítható. Az egyes szabályok leírása között üres sornak kell szerepelnie. A parancsokat pedig egy tabulátor vezeti be, alább ezt a piros vonalak jelzik:

célfájl1: függőségfájl1 függőségfájl2 függőségfájl3...
_______parancsok...
_______parancsok...
_______parancsok...

célfájl2: függőségfájl1 függőségfájl2...
_______parancsok...
_______parancsok...
_______parancsok...

A helló projektet az alábbi Makefile tudja kezelni:

program: main.o hello.o
	gcc -g main.o hello.o -o program

hello.o: hello.c hello.h
	gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c hello.c -o hello.o

main.o: main.c hello.h
	gcc -std=c99 -Wall -Wdeprecated -pedantic -g -c main.c -o main.o

Mentsd el ezt Makefile néven, és próbáld ki a make parancsot (Windowson lehet, hogy mingw32-make kell)! Töröld le külön-külön az egyes előállított fájlokat (pl. main, hello.o), és figyeld meg, mikor melyik parancsot futtatja a make!

Változók, makrók és minták

A program az alábbi Makefile segítségével is megépíthető. Ez változókat (mint pl. a BINARY), makrókat (pl. $^ és $@) és mintákat (pl. a %.o kezdetű szabály) is tartalmaz. Bogarászd ki, vajon melyik mit jelent!

BINARY = program
OBJECTS = main.o hello.o
HEADERS = hello.h

CC = gcc
CFLAGS = -std=c11 -O0 -Wall -Wdeprecated -pedantic -g
LDFLAGS = -g

.PHONY: all clean

all: $(BINARY)

clean:
	rm -f $(BINARY) $(OBJECTS)

$(BINARY): $(OBJECTS)
	$(CC) $(LDFLAGS) $^ -o $@

%.o: %.c $(HEADERS)
	$(CC) $(CFLAGS) -c $< -o $@

5. C++ fordítás és linkelés, a „name mangling”

Térjünk most át a C++ nyelvre! C-ben minden függvényt egyértelműen azonosított a neve. C++-ban ez már nincs így, több ugyanolyan nevű függvényünk is lehet.

Tekintsük az alábbi, (khm) demonstrációs céllal készült programkódot:

foo.cpp
void foo() {} // 1

void foo(int i) {} // 2

class Foo {
  public:
    void foo(); // 3
};
void Foo::foo() {}

namespace FooNS {
    void foo() {} // 4

    class Foo {
    public:
        void foo(); // 5
    };
    void Foo::foo() {}
}

int main(int argc, char *argv[]) {
    /* 1 */ foo();
    /* 2 */ foo(1);
    /* 3 */ Foo().foo(); 
    /* 4 */ FooNS::foo(); 
    /* 5 */ FooNS::Foo().foo(); 
}

Az egyforma nevű függvények közül a kiválasztott példányt a hívás helye és módja határozza meg: a választásnál a fordító figyelembe veszi a paraméterek típusát, a névteret, tagfüggvények esetén pedig az objektum típusát is. Csak egy gond van: a fordítás után keletkező tárgykódban már nem léteznek a deklarációk, ott már csak nevek vannak. Csakis és kizárólag a nevek alapján meg kell tudni különböztetni a függvényeket. Ezért találták ki a name mangling nevű módszert (más néven: function name decoration). Ez azt jelenti, hogy a tárgykódba írt függvénynevekbe a fordító szisztematikusan belekódolja a paraméterek típusát, az osztályok és a névterek neveit is.

Az egyes fordítók egészen eltérő „name mangling” módszereket használnak. A Wikipédián van egy jó táblázat erről: a void h(int) függvénynek a különféle fordítók egész változatos neveket adnak, pl. _Z1hi, h__Fi, ?h@@YAXH@Z, CXX$__7H__FI0ARG51T és hasonlók.

Próbáld ezt ki! Fordítsd le a fenti programot, állítsd elő a tárgykód fájlt! A C++ fordítónak használhatod a g++-t vagy Linuxon a clang++-t; a paraméterezésük megegyezik a C fordítóéval. Írasd ki a keletkező tárgykód fájl szimbólumait! A -C paraméter hatására az nm „demangle”-eli a neveket; hasonlítsd össze a kimenetet enélkül és ezzel együtt is! Mi a helyzet a main() függvénnyel? Mit gondolsz, miért?

Megoldás

Az előállított tárgykód fájl tartalma G++ fordítóval:

0000000000000050 T main                     0000000000000050 T main
0000000000000010 T _Z3fooi                  0000000000000010 T foo(int)
0000000000000000 T _Z3foov                  0000000000000000 T foo()
0000000000000020 T _ZN3Foo3fooEv            0000000000000020 T Foo::foo()
0000000000000040 T _ZN5FooNS3Foo3fooEv      0000000000000040 T FooNS::Foo::foo()
0000000000000030 T _ZN5FooNS3fooEv          0000000000000030 T FooNS::foo()

A main() előtt is van élet: a fordítók további programrészeket tesznek a lefordított programba, amelyek létrehozzák a globális változókat, átveszik a paramcssori argumentumokat, és így tovább. Ezek a programrészek hívják meg a main() függvényt. Mivel ezek lefordítva, készen járnak a fejlesztőkörnyezethez, bennük a main() függvény már név szerint van hivatkozva. Ezért annak nevét változatlanul hagyja a fordító. Ebben is különleges ez a függvény, nem csak abban, hogy kiegészíti a fordító return 0-val, ha nem lát benne ilyet.

Egészítsd ki az előbbi Makefile-t úgy, hogy C++ programok lefordítására is alkalmas legyen. Ehhez tedd a következőket:

  • Hozz létre a CC mellett egy CXX nevű változót is. Ez fogja megadni a C++ fordító nevét: g++.
  • Hozz létre egy CXXFLAGS nevű változót is, a C++ fordító paraméterezéséhez. A szabvány évjáratát a g++-nál is a -std= kapcsoló adja meg; legyen ez c++11. (A szokásos értékei: c++98 és c++11.)
  • A linkelés helyén írd át a CC változót CXX-re, hogy a lefordított programhoz a C++-specifikus könyvtárak is linkelődjenek.
  • Hozz létre egy szabály mintát a %.o: %.c mellett: ez legyen a %.o: %.cpp minta. Add meg értelemszerűen a parancsot.

Állítsd be a tetején úgy a változókat, hogy a foo.cpp adja meg a teljes programot: foo. Próbáld ki egy make clean után, hogy működik-e a Makefile-od! Ezután jön a varázslat: össze kell majd linkelned egy C-ben és egy C++-ban írt programrészletet. Erről szól a következő feladat.

Megoldás
BINARY = foo
OBJECTS = foo.o
HEADERS = 

CC = gcc
CXX = g++
CFLAGS = -std=c11 -O0 -Wall -Wdeprecated -pedantic -g
CXXFLAGS = -std=c++11 -O0 -Wall -Wdeprecated -pedantic -g
LDFLAGS = -g

.PHONY: all clean

all: $(BINARY)

clean:
	rm -f $(BINARY) $(OBJECTS)

$(BINARY): $(OBJECTS)
	$(CXX) $(LDFLAGS) $^ -o $@

%.o: %.c $(HEADERS)
	$(CC) $(CFLAGS) -c $< -o $@

%.o: %.cpp $(HEADERS)
	$(CXX) $(CXXFLAGS) -c $< -o $@

6. C és C++ linkelése

Hozz létre egy üres mappát a következő projektnek, és másold bele azt a Makefile-t, amit az előbb okosítottál fel C++-hoz. Hozz létre két forrásfájlt és egy fejlécfájlt: main.cpp, hello.c és hello.h, és add meg ezeket a fájlokat a Makefile-ban, ahol kell.

Írj a hello.c-ben egy hello_c() nevű függvényt. Csináljon ez csak annyit, hogy kiír egy üzenetet a képernyőre. Tegyél bele valami nagyon C-specifikus dolgot, pl. hozz létre egy class nevű változót (ilyet C++-ban garantáltan nem lehet).

Írd meg a főprogramot a main.cpp-ben! Hívd meg innen a hello_c() nevű függvényt, ezen felül pedig csinálj valami nagyon C++-specifikus dolgot, pl. definiálj egy osztályt a class kulcsszóval!

Próbáld meg lefordítani és linkelni a programot. Mit tapasztalsz? Mi az oka? Elvileg odáig el kellett jutnia a fordításnak, hogy létrejön a main.o és a hello.o. Vizsgáld meg ezeket az nm paranccsal!

Megoldás

A hibát az okozza, hogy a C++ fordító használ name mangling-et, a C fordító pedig nem. A C fordító ezért egy hello_c nevű függvényt tesz a tárgykódba, a C++ fordító pedig a _Z7hello_cv függvényre hivatkozik:

$ nm main.o
0000000000000000 T main
                 U _Z7hello_cv nem ugyanaz,

$ nm hello.o
0000000000000000 T hello_c mint ez
                 U printf

Az extern "C" hívási konvenció

A C-ben írt kódrészletek igen nagy hányada változtatás nélkül C++-ban is lefordítható, és ugyanúgy működik is. Ez azonban nem azt jelenti, hogy ugyanaz a tárgykód fájl keletkezik, és a legnyilvánvalóbb különbséget épp a tárgykódban megjelenő függvénynevek jelentik a C++-os name mangling miatt. Mivel azonban a C/C++ programok ahol lehet, binárisan is kompatibilisek, a két tárgykódot össze lehet linkelni, ha szólunk a C++ fordítónak, hogy a hello_c() függvényt az eredeti nevén keresse. Ezt pedig az extern "C" jelzéssel lehet megtenni.

Első lépésként töröld ki az #include "hello.h" sort a mainből, és tedd be helyette ezt a sort:

extern "C" void hello_c(void);

Próbáld meg így lefordítani a programot – így már működnie kell. Vizsgáld meg a tárgykód fájlokat, látni fogod, mindkét helyen már hello_c néven látszik a hivatkozás.

Ez a jelzés a fordító számára a függvény ún. hívási konvencióját (calling convention) adja meg. Általában akkor kell ilyet használni, amikor külső, más fordítóval lefordított, esetleg más nyelven íródott függvényt kell hívni. Azt adja meg a fordító számára, hogy a hívott függvény hogyan, milyen sorrendben, milyen elhelyezéssel várja a paramétereket a veremben, esetleg máshol – mindezt bit szinten pontosan. Ebben az esetben arra is utal a C++ fordítónak, hogy ez egy C-ben írt függvény lesz, ezért a nevét pontosan így lehet majd megtalálni a tárgykódban, nem szabad name mangling-et használni.

A hívási konvenciót egyszerre több függvény deklarációjára is lehet alkalmazni, ehhez egyszerűen egy blokkba kell tenni a függvényeket:

extern "C" {
    void hello_c(void);
    double masik_fuggveny(int, char*);
}

Legjobb lenne ezt ilyen formán betenni a hello.h fejlécfájlba, de azt nem lehet, mert annak a fájlnak a C fordító által is értelmezhetőnek kell lennie. Ezt a problémát az előfordító segítségével szokás megoldani. A C++ biztosítja, hogy előfordítás közben definiálva van egy __cplusplus nevű makró, C-ben pedig nincs ilyen. Ezt a tényt kihasználva lehet megjeleníteni vagy eltüntetni a hívási konvenció megadását:

#ifdef __cplusplus
extern "C" {
#endif

void hello_c(void);

#ifdef __cplusplus
}
#endif

Ha így írjuk meg a fejlécfájlt, akkor az már azt fogja jelenteni mindkét nyelven, amit kell. A C++ előfordító beteszi az extern "C"-t, ezért a name mangling nem lesz érvényes a függvényre; a C fordító pedig az egészből nem lát semmit, mert az előfordítója kitörölte azt a sort.

Javítsd a fejlécfájlt ilyenre, és fordítsd le a programot! Ellenőrizd ezután újra a tárgykód fájlokat!

Megoldás
main.cpp
#include "hello.h"

class Osztaly {};

int main() {
    hello_c();
}
hello.c
#include <stdio.h>
#include "hello.h"

void hello_c(void) {
    int class = 2;
    printf("Hello C, class = %d!\n", class);
}
hello.h
#ifndef HELLO_H
#define HELLO_H

#ifdef __cplusplus
extern "C" {
#endif

void hello_c(void);

#ifdef __cplusplus
}
#endif

#endif

7. Ha marad időd: dinamikus linkelés

Normál, „statikus” linkelés közben a futtatható programba bemásolódnak a lefordított függvények. Ennek több hátránya van:

  • Ha egy függvényt átírunk, nem csak a forrásfájlját kell újrafordítani, hanem újra is kell linkelni a programot.
  • Ha több programban is használjuk ugyanazokat a függvényeket, többször lesznek bent a memóriában.

Ezeket a problémákat oldja meg a dinamikus linkelés (dynamic linking). Ez lényegében annyit jelent, hogy némely forrásfájlokat nem a futtatható programfájl előállításakor, hanem csak annak elindításakor linkelünk a programhoz. Ezek Unix operációs rendszeren az SO-k (shared object), Windowson pedig a DLL-ek (dynamic link library).

Az SO-kból (DLL-ekből) csak egy változat töltődik be a memóriába; ha több program használja azokat, akkor osztoznak rajta. Így kisebb a memóriafelhasználás. Könnyebb a cseréjük is: ha az SO-ból új verzió készül, az azt használó programokat nem kell újralinkelni (amíg persze a benne lévő függvények kompatibilisek maradtak). Még az is megoldható, hogy egy program a futása közben töltsön be és dobjon el egy SO-t; így működnek a programok bővítményei (pluginek) és a hardvereszközök meghajtóprogramjai (driver v. kernel module).

Az SO-k létrehozása és programhoz linkelése majdnem ugyanúgy megy, mint a szokásos fordítás, csak néhány extra paramétert kell adni a fordítónak, amivel megadjuk, mit szeretnénk. Ezek közül az egyetlen, aminek elméleti jelentősége van, a -fPIC paraméter. Ezzel kell jelezni fordítás közben azt, hogy „PIC = position independent code”-ot szeretnénk létrehozni. Az így lefordított programba a fordító a globális változók és függvények elérését indirekción keresztül végzi, ami lehetővé teszi azt, hogy ezek elmozdíthatók legyenek a memóriában.

Próbáld ki a dinamikus linkelést! Hozd létre az alábbi mini-projektet, és utána kövesd az utasításokat!

test.c
#include <stdio.h>
#include "test.h"

void test(void) {
    printf("Dynamic linking test 1.0\n");
}
test.h
#ifndef TEST_H_INCLUDED
#define TEST_H_INCLUDED
void test(void);
#endif
main.c
#include "test.h"

int main(void) {
    test();
}

Először fordítsd le a test.c fájlt! Az ebben lévő függvény fog a dinamikusan linkelt részbe kerülni. Ennél a fordítási lépésnél kell megadni a -fpic paramétert:

$ gcc -fPIC test.c -c -o test.o

Ezután hozd létre az SO (DLL) fájlt! Itt meg lehetne adni több tárgykódot, amik beépülnek az SO-ba, de most csak egy van:

$ gcc -shared test.o -o libtest.so  Windowson: -o libtest.dll

Végül fordítsd le a főprogramot, és linkeld hozzá dinamikusan a -l paraméterrel a függvénykönyvtárat! A -L. paraméter azt adja meg, hogy a .so (.dll) fájlt az aktuális mappában kell keresni; mert alapértelmezetten csak a rendszerszinten definiált mappákban keresné. Az -ltest paraméternél nem kell a név elején lévő lib-et, és a név végén lévő .so-t odaírni; azokat automatikusan kezeli. Ezeket megadva a linkelés közben ellenőrzi, hogy megtalálhatók-e a függvények. Az rpath a linkernek átadott paraméter (-Wl: a linkernek, rpath : a -rpath paramétert, az értéke: .); azt a mappát adja meg, ahol az SO fájlt futási időben keresni fogja a program. Most ez is az aktuális mappa lesz:

$ gcc main.c -L. -ltest -Wl,-rpath,. -o prog

Ezután a program elindítható. Az ldd paranccsal ellenőrizni tudod, hogy a létrejött futtatható milyen könyvtárakhoz kapcsolódik, ezek között látszódnia kell a libtest.so-nak is, és nem lehet mellette „not found” jelzés:

MinGW-ben
nincs ldd
$ ldd prog

Próbáld ki, hogy tényleg dinamikus-e a linkelés! Változtasd meg a test() függvényt, írjon ki ez új verziószámot! Fordítsd utána a test.c-t újra, és állítsd elő a libtest.so új verzióját is. A ./prog programot futtatva ezután rögtön az új verziószám kell látszódjon.

8. Irodalom

  1. Name mangling (Wikipedia).