> Kütüphaneler : Kütüphane (library) kavramı genel olarak "kullanıma hazır halde bulunan fonksiyonlar ve sınıfların oluşturduğu" topluluklar için kullanılan bir terimdir. Ancak aşağı seviyeli dünyada kütüphane "içerisinde derlenmiş bir biçimde fonksiyonların (ve sınıfların) bulunduğu dosyalara denilmektedir. Kütüphaneler statik ve dinamik olmak üzere ikiye ayrılmaktadır. Statik kütüphanelerin Windows sistemlerinde uzantıları ".lib"", UNIX/Linux ve macOS sistemlerindeki uzantıları ".a" biçimindedir. Dinamik kütüphanelerin ise Windows sistemlerindeki uzantıları ".dll", UNIX/Linux ve macOS sistemlerindeki uzantıları ise ".so" biçimindedir. ".lib" uzantısı "library" sözcüğünde, ".a" uzantısı "archive" sözcüğünden, "dll" uzantısı "dynamic link library" sözcüklerinden ve ".so" uzantısı da "shared object" sözüklerinden kısaltılmıştır. Kütüphane konusunun aslında aşağı seviyeli oldukça ayrıntıları vardır. Biz bu kursumuzda temel düzeyde bu kavramlar üzerinde uygulamalı bilgiler edineceğiz. Derleyicilerin ürettiği dosyalara "amaç dosyalar (object files)" ya da "yeniden yüklemebilen dosyalar (relocatable object files)" denilmektedir. Bağlayıcılar bir grup amaç dosyayı (object files) girdi olarak alıp çeşitli kütüphanelere de başvurarak çalıştırılabilir doysalar oluşturmaktadır. Bu sayede programlar farklı ekipler tarafından parça parça yazılıp bağlama aşamasında birleştirilebilmektedir. Örneğin tipik bir C projesi şöyle derlenip oluşturulmaktadır: a1.c ----> Derleyici ----> a1.obj ----> \ a2.c ----> Derleyici ----> a2.obj ----> \ a3.c ----> Derleyici ----> a3.obj ----> ===> Bağlayıcı ------> Çalıştırılabilir Dosya a4.c ----> Derleyici ----> a4.obj ----> / / a5.c ----> Derleyici ----> a5.obj ----> / / Kütüphane Dosyaları Amaç dosyaların Windows sistemlerindeki uzantıları ".obj" biçiminde UNIX/Linux ve macOS sistemlerindeki uzantıları ise ".o" biçimindedir. Pekiyi bağlayıcı tam olarak ne yapmaktadır? Farklı amaç dosyaları birleştirmek basit bir işlem değildir. Örneğin iki kaynak dosyadan oluşan bir program söz konusu olsun: /* a1.c */ extern int g_x; void foo(void); int main(void) { g_x = 10; foo(); CALL adres return 0; } /* a2.c */ int g_x; void foo(void) { printf("foo\n"); } Proje oluşturan bu "a1.c" ve "a2.c" dosyalarının birbirinden habersiz bağımsız bir biçimde derlendiğine dikkat ediniz. "a1.c" programı derlendiğinde foo fonksiyonu o dosya yoktur. g_x global değişkeni de o dosyada yoktur. Pekiyi oradaki makine komutları nasıl üretilmektedir? İşte derleyiciler bu yüt durumlarda makine kodlarının ilgili kısımlarını boş bırakıp bağlayıcının orayı doldurmasını istemektedir. Bu işleme "relocation" denilmektedir. "a1.c" dosyası içerisindeki fonksiyon çağrısına dikkat ediniz: foo(); Bir fonksiyonun çağrılabilmesi için CALL makine komutu kullanılmaktadır. CALL makine komutu da operand olarak çağrılacak fonksiyonu adresini almaktadır. Örneğin: CALL Oysa derleyici derleme aşamasında foo fonksiyonunu görmediği için onun adresini bilememktedir. İşte derleyici bağlayıcıdan "foo fonksiyonun yerini bulunup ilgili adresin düzeltilmesini" talep etmektedir. Bu relocation bilgileri amaç dosyanın içerisinde bulunmaktadır. Yani bağlayıcı yalnızca amaç dosyaları birleştirmemekte aynı zamanda çeşitli makine komutlarını da düzeltmektedir. Pekiyi yukarıdaki örnekte foo fonksiyonunu bağlayıcı nerelrde arayacaktır? Bağlayıcılar öncelikle fonksiyonları ve global nesneleri kendilerine girdi olarak verilmiş olan kütğphane amaç dosyalarda aramaktadır. Onlar amaç dosyalarda bulunamazsa bağlayıcılar programcının belirlediği kütüphane dosyalarına da bakmaktadır. Standart C fonksiyonları, Windows API fonksiyonları, POSIX fonksiyonları çeşitli kütüphane dosyalarının içerisindedir. Bunlar bağlama aşamasında bağlayıcı tarafından ele alınmaktadır. Şimdi de statik ve dinamik kütüphaneleri sırasıyla inceleyelim: >> Statik Kütüphaneler : Statik kütüphane dosyaları "amaç dosyaları tutan kap" gibidir. Yani statik kütüphane dosyalarının içerisinde fonksiyonlar ya da sınıflar değil amaç dosyalar bulunmaktadır. Yani örneğin biz bir grup fonksiyonu statik kütüphaneye yerleştirmek istesek önce onları bir C dosyasının içerisine yazarız. Sonra dosyayı derleyip amaç dosya oluştururuz. Bu amaç dosyayı da statik kütüphane içerisine yerleştiririz. Microsoft C derleyicilerinde dosyayı yalnızca derlemek için "/c" seçeneği, gcc ve clang derleyicilerinde ise "-c" seçeneği kullanılmaktadır. Örneğin cl /c myutil.c gcc -c myutil.c Şimdi de sistem özelinde incelemelerimize devam edelim: >>> Windows Sistemlerinde : Microsoft Windows sistemlerinde statik kütüphane dosyaları özel bir formata sahiptir. Bunu oluşturmak için Microsoft "lib.exe" isimli yardımcı bir program bulundurmuştur. "lib.exe" programı Microsoft C derleyicisi kurulduğunda (tabii Visual Studio IDE'si de kurulduğunda) otomatik olarak kurulmaktadır. lib.exe programının en önemli birkaç kullanımı şöyledir: lib /OUT:myutil.lib a.obj b.obj Burada "myutil.lib" dosyası yeniden yaratılır ve onun içerisine "a.obj" ve "b.obj" dosyaları eklenir. Eğer bu dosya varsa içerği sıfırlanarak yeniden oluşturulmaktadır. lib myutil.lib c.obj Burada "myutil.lib" "dosyasının var olması gerekir. Onun içerisinde eğer "c.obj" dosyası yoksa eklenir varsa dosya değiştirilir. lib /LIST myutil.lib Burada "myutil.lib" içerisindeki amaç dosyalar görüntülenir. lib /REMOVE:a.obj myutil.lib Burada "myutil.lib" içerisinden "a.obj" dosyası silinir. lib /EXTRACT:a.obj myutil.lib Burada "myutil.lib" içerisindeki "a.obj" dosyası dışarıda "a.obj" "olarak save edilir. Fakat kütüphanenin içeriisinden silinmez. "lib.exe" hakkında ayrıntılı açıklamalar için aşağıdaki MSDN bağlantısından faydalanabilirsiniz: https://learn.microsoft.com/en-us/cpp/build/reference/overview-of-lib?view=msvc-170 Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, Aşağıdaki örnekte "a.c" ve "b.c" dosyaları aşağıdaki gibi derlenip "myutil.lib" isimli bir statik kütüphane dosyası oluşturulmuştur: cl /c a.c cl /c b.c lib /OUT:mmyutil.lib a.obj b.obj Programın kodları ise şu şekildedir; /* a.c */ #include int g_x; double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } Bir statik kütüphaneden bir fonksiyon çağrıldığında bağlayıcı çağrılmış olan fonksiyonun statik kütüphane içerisindeki hangi amaç dosyada olduğunu belirleri, o amaç dosyayı çalıştırılabilen dosyaya ekler. Böylece statik kütüphane kullanan bir program çalıştırılırken artık o statik kütüphaneye gereksinim duyulmaz. Şu noktalara dikkat ediniz: -> Statik kütüphane içerisinde bir fonksiyon bile çağırsak bağlayıcı onun bulunduğu amaç dosyanın tamamını çalıştırılabilen dosyaya yazmaktadır. Amaç dosya içerisindeki tek bir fonksiyonun çalıştırılabilen dosyaya yazılması mümkün değildir. -> Statik kütüphane kullanımında statik kütüphane içerisidnen çağrılan fonksiyonlar nihayetinde çalıştırılabilen dosyanın içerisine yazıldığı için aynı fonksiyonları kullanan farklı programlar aynı fonksiyon kodlarını çalıştırılabilen dosya içerisinde barındırmış olacaklardır. Bu da çalıştırılabilen dosyaların diskte daha fazla yer kaplamasına yol açmaktadır. -> Statik bağlanmış fonksiyonları kullanan farklı programlar birlikte çalıştırıldığında işletim sistemi mecburen onların ortak kullandığı fonksiyonları tekrar belleğe yüklemektedir. Bu da yalnızca disk kullanımı bakımından değil ana bellek yönetini bakımından da etkin olmayan bir durum oluşturmaktadır. -> Statik kütüphanelerin en önemli avantajı onları kullanan programların konuşlandırılmasının (deployment) kolay olmasıdır. Tek yapılacak şey tek bir çalıştırılabilir dosyanın hedef makineye taşınmasıdır. Aslında C ve C++ IDE'lerinde de hiç komut satırı işlemi yapmadan statik kütüphane oluşturulabilmektedir. Örneğin Visual Studio IDE'sinde yeni boş bir bir proje oluşturulduktan sonra proje seçeneklerinden "Configuration Type" için "Application" yerine "Static library" seçilirse proje build edildiğinde statik kütüphane dosyası oluşturulmaktadır. Visual Studio derleyicisi default durumda standart C kütüphanesinin dinamik versiyonunu kullanmaktadır. Eğer projenizde standart C kütüphanesinin statik versyionunu kullanmak istiyorsanız komut satırında "/MT" seçeneği eklenmelidir. Aynı işlem proje seçeneklerinde "C-C++/Code Generation/Runtime Library" sekmesinden de ayarlanabilmektedir. Visual Studio IDE'sinde (diğer IDE'lerde de benzer) bir projede başka bir statik kütüphanenin içerisindeki fonksiyonları çağırmak istediğimizi düşünelim. Pekiyi bu statik kütüphaneye IDE içerisinden nasıl referans ederiz? İşte kullanılacak ek kütüphanelere bağlayıcının bakmasını sağlamak için Visual Studio IDE'sinde proje seçeneklerinden "Linker/Input/Additional Dependencies" edit alanına kütüphanenin ismi eklenmelidir. (Bu alandaki kütüphane isimleri ';' karakterleriyle birbirinden ayrılmaktadır.) Burada kütüphane yalnızca ismi girilebilir. Bir yol ifadesi girilmez. Eğer kütüphane özel bazı dizinlerde ya da projenin çalışma dizininde değilse ayrıca kütüphanenin bulunduğu dizin "Linker/General/Additional Library Directories" edit alanına girilmelidir. Burada dizinler mutlak ya da göreli biçimde girilebilir. >> UNIX/Linux Sistemlerinde : UNIX/Linux sistemlerinde statik kütüphane dosyalarını oluşturmak ve onlarla ilgili işlem yapmak için "ar" isimli program kullanılmaktadır. UNIX/Linux sistemlerindeki "ar" programı aslında Microsoft sistemlerindeki "LIB.EXE" programının karşılığı olarak düşünülebilir. Bu "ar" programının birkaç önemli komut satırı argümanı vardır. Bu program çok eskiden tasarlandığı için seçenekler '-' karakteri ile değil'-' karakteri olmadan doğrudan belirtilmektedir. "r" komut satırı argümanı ("replace" sözcüğünden geliyor) amaç dosyaları kütüphaneye eklemek için kullanılmaktadır. Bu seçenekte kütüphane dosyası yoksa aynı zamanda yaratılmaktadır. Kullanımı şöyledir: ar r <.o dosyaları> Eğer eklenecek amaç dosya zaten kütüphanenin içerisinde varsa o değiştirilmektedir. Örneğin: $ gcc -c a.c $ gcc -c b.c $ ar r libmyutil.a a.o $ ar r libmyutil.a b.o "t" seçeneği kütüphane içerisindeki amaç dosyaları görüntülemektedir. Kullanımı şöyledir: ar t Örneğin: $ ar t libmyutil.a a.o b.o "d" seçeneği ("delete" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmek için kullanılmaktadır. Kullanımı şöyledir: ar d <.o dosyaları> Örneğin: $ ar t libmyutil.a a.o b.o $ ar d libmyutil.a a.o $ ar t libmyutil.a b.o "x" seçeneği ("extract" sözcüğünden geliyor) kütüphane içerisindeki bir amaç dosyayı kütüphaneden silmeden dışarıya save etmekte kullanılmaktadır. Kullanımı şöyledir: ar x <.o dosyaları> Örneğin: $ ar x libmyutil.a a.o UNIX/Linux sistemlerinde geleneksel olarak kütüphane dosyaları "lib" öneki başlatılarak isimlendirilmektedir. UNIX/Linux sistelerinde bir statik kütüphaneye başvuru işlemi gcc komut satırında ".a" dosyasının belirtilmesi yoluyla yapılabilmektedir. Örneğin: $ gcc -o app app.c libmyutil.a Burada gcc programı ".a" uzantılı dosyaları "ld" bağlayıcısını çalıştırırken ona gendermektedir. Tabii aynı işlem iki parça halinde şöyle de yapılabilirdi: $ gcc -c app.c $ gcc -o app app.o libmyutil.a Daha önceden de belirttiğimiz gibi standart C fonksiyonları ve POSIX fonksiyonları "libc" isimli bir kütüphanede bulunmaktadır. Bu kütüphanenin static ve dinamik biçimleri vardır. Default durumda bu kütüphanenin dinamik versiyonu kullanılmaktadır. Ancak komut satırında "-static" seçeneği ile bu kütüphanenin statik versiyonunun kullanılması sağlanabilir. "-static" komut satırı seçeneği "tüm kütüphanelerin statik versiyonlarının" kullanılacağı anlamına gelmektedir. (Yani biz "-static" seçeneğini beelirttikten sonra başka bir dinamik kütüphaneyi de bağlama aşamasında kullanamayız.) Örneğin: $ gcc -o app app.c -static UNIX/Linux sistemlerinde kütüphane dosyası başka bir dizindeyse bu durumda o dosyaya referans ederken yol ifadesi kullanılabilir. Öneğin: $ gcc -o app app.c xxx/libmyutil.a Eğer kütüphanenin ismi "lib" öneki ile başlatılmışsa kütüphaneye referans etme (yani kütüphanenin bağlayıcı tarafından işleme sokulmasını sağlama) "-l" seçeneği ile yapılabilmektedir. Ancak bu "-l" seçeneğinde kütüphanenin başındaki "lib" öneki ve ".a" ya da ".so" sonekleri kullanılmamaktadır. Örneğin: $ gcc -o app app.c -lmyutil Burada bağlama işlemine "libmyutil.a" dosyası sokulacaktır. Ancak "-l" seçeneği ile kütüphane bu biçimde belirtildiğinde bu kütüphane "/lib" ve "/usr/lib" dizinlerinde aranmaktadır. Yani bu durumda buradaki "libmyutil.a" dosyasının bu dizinlerden birine çekilmesi gerekir. Ancak "-l" seçeneğine ek olarak "-L" seçeneği ile ek arama dizi belirtilebilmektedir. Örneğin: $ gcc -o app app.c -lmyutil -L/home/kaan Burada "libmyutil.a" dosaysı yukarıda belirttiğmiz dizinlerin yanı sıra "/home/kaan" dizininde de aranacaktır. Tabii biz "-L" seçeneğinde o anda bulunulan dizini "." ile de belirtebiliriz. Örneğin: $ gcc -o app app.c -lmyutil -L. Örneğin daha önceden de kullandığımız thread kürüphanesi aslında "libpthread.a" ve "libpthread.so" isimli dosyalardır. Biz de bu ktüphaneiyi bağlama aşamasında şöyle devreye sokuyorduk: $ gcc -o app app.c -lpthread >> Dinamik Kütüphaneler : Dinamik kütüphaneler statik kütüphanelerden oldukça farklıdır. Windows'ta dinamik kütüphanelerin uzantısı ".dll", UNIX/Linux sistemlerinde ".so" biçimindedir. ".dll" uzantısı "dynamic link library" sözcüklerinden ".so" uzantısı ise "shared object" sözcüklerinden kısaltılmıştır. Bir program dinamik kütüphaneden bir fonksiyon çağırdığında bağlayıcı o fonksiyonun kodunu dinamik kütüphaneden çekip çalıştırılabilen dosyaya yazmaz. Bağlayıcı bunun yerine çalıştırılabilen dosyaya "falanca dinamik kütüphaneden filanca fonksiyonlar çağrıldı" biçiminde bir bilgi yazmaktadır. Dinamik kütüphane kullanan bir program çalıştırılmak istendiğinde ise işletim sistemi bu programla birlikte bu programın kullandığı dinamik kütüphaneleri prosesin sanal bellek alanına bütünsel bir biçimde yükler. Program çalışırken akış fonksiyonun çağrılma noktasına geldiğinde bellekte dinamik kütüphanenin yüklü olduğu alana atlayarak fonksiyonu çalıştırır. Dinamik kütüphane kullanımının şu avantajları vardır: -> Dinamik kütüphane kullanan programlar diskte daha az yer kaplarlar. -> Dinamik kütüphane kullanan programlarda dinamik kütüphane içerisindeki fonksiyonlarda değişiklikler yapıldığında çalıştırılabilen programın yeniden derlenmesine ve link edilmesine gerek kalmaz. -> Farklı proseslerin aynı dinamik kütüphaneyi kullanması durumunda bu dinamik kütüphane fiziksel RAM'e tekrar tekrar yüklenmemektedir. Bu da aslında toplamda daha iyi bir RAM optimizasyonu sağlamaktadır. Ancak dinamik kütüphane kullanan programlar başka bir makineye konuşlandırılırken (deploy edilirken) yalnızca çalıştırılabilen dosya değil onun kullandığı bütün dinamik kütüphaneler de o makineye çekilmek zorundadır. Ayni artık program tek bir çalıştırılabilen dosyadan oluşmamaktadır. O dosyanın kullandığı dinamik kütüphaneler de programın bir parçası durumundadır. Dinamik kütüphane kullanımının şu dezavantajları söz konusu olabilmektedir: -> Dinamik kütüphane kullanan programların hedef makineye konuşlandırılması daha zahmetlidir. -> Dinamik kütüphane kullanan programların yüklenmesi daha uzun zaman alma eğilimindedir. -> Dinamik kütüphane kullanan programlar prosesin sanal bellek alanında daha fazla yer kaplamaktadır. Şimdi de sistemler genelinde incelemelerimize devam edelim: >>> Windows Sistemleri : Microsoft Windows sistemlerinde bir DLL oluşturmak için tek yapılacak şey link aşamasında "/DL"L seçeneğini kullanmaktır. Örneğin a.c ve b.c dosyalarını derleyerek bunlardan DLL yapmak isteyelim. Bu işlemi parça paça şöyle gerçekleştirebiliriz: cl /c a.c cl /c b.c link /DLL a.obj b.obj Buradan ilk dosyanın ismine ilişkin "a.dll" dosyası elde edilecektir. Tabii "/DL"L seçeneği kullanılmasaydı default durumda bu dosyalardan ".exe" dosyası oluşturulmaya çalıştırılacaktı. Tabii bu durumda bir main fonksiyonun buluması gerekecekti. Hedef dosyanın ismi "/OUT" seçeneği ile belirlenebilmektedir. Örneğin: cl /c a.c cl /c b.c link /OUT:myutil.dll /DLL a.obj b.obj Burada "myutil.dll" dosyası oluşturulacaktır. Yukarıdaki işlem aslında tek hamlede "cl" programının komut satırında "/LD" seçeneği kullanılarak aşağıda gibi de yapılabilmektedir: cl /LD a.c b.c Burada ilk dosyanın ismine ilişkin DLL dosyası (yani "a.dll" dosyası) oluşturulacaktır. Ancak dinamik kütüphaneye istediğimiz ismi vermek istiyorsak "cl" komut satırında "/Fe" kullanılması gerekir. "/OUT" seöeneği bir bağlayıcı seçeneğidir. "/Fe" seçeneği ise bir "cl" derleyicisinin seçeneğidir. (Başka bir deyişle biz "cl" derleyicisini "/Fe" seçeneği ile çalıştırdığımızda aslında "cl" derleyicisi "link" isimli bağlayıcıyı "/OUT" seçeneği ile çalıştırmaktadır.) Örneğin: cl /Fe:myutil.dll /LD a.c b.c Bura artık "myutil.dll" isimli dosya oluşturulacaktır. Teknik anlamda Microsoft Windows sistemlerinde ".EXE" dosyası ile ".DLL" dosyası arasında format bakımından farklılık yoktur. He iki dosya da "PE (Portable Executable)" dosya formatına ilişkindir. ".EXE" doaysının ".DLL" dosyasından tek farkı bir "entry point" yani main fonksiyonuna sahip olmasıdır. Bir DLL içerisinde main fonksiyonunun olması o DLL dosyasını EXE dosya yapmaz. DLL dosyasının içerisindeki main fonksiyonu sıradan bir fonksiyon gibi DLL'in içerisinde bulunur. Windows'ta bir DLL içerisindeki fonksiyonun ya da global nesnenin dışarıdan kullanılabilmesi için o fonksiyonun ya da global nesnenin adresinin PE formatının "export tablosu" denilen bir yerinde bulunması gerekir. İşte DLL içerisindeki bir global nesnenin ya da fonksiyonun adresinin export tablosuna yazılması için Micrososft derleyicilerine özgü __declspec(dllexport) belirleyicisinin kullanılması gerekmektedir. Aksi takdirde o fonksiyonlar ve nesneler başka bir modülden (yani başka bir DLL ya da EXE programdan) kullanılamazlar. Aşağıdaki örnekte DLL içerisinde foo ve g_a dışarıdan kullanılbilir ancak bar g_b dışarıdan kullanılmaz. Tabii bu bar ve g_b DLL'in kendi içeriisndeki fonksiyonlardan kullanılabilir. Örneğin: #include __declspec(dllexport) int g_y; __declspec(dllexport) void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } Buarada g_y global değişkeni ve foo fonksiyonu dışarıdan (örneğin bir exe dosya tarafından) kullanılabilir. Ancak bar fonksiyonu dışarıdan kullanılamaz. Aslında C++'taki sınıf (class) kavramı yapay ve mantıksal bir kavramdır. Aşağı seviyeli dünyada sınıf diye bir kavram yoktur. Aslında C++'ta sınıflardaki üye fonksiyonlar global fonksiyonlar gibi derlenirler. Dolayısıyla C++'ta bir sınıfı DLL'e yerleştirmek için yine üye fonksiyonların önünde hangi üye fonksiyonlar dışarıdan kullanılacaksa __declspec(dllexport) bildirimlerinin yapılması gerekir. Örneğin: class Sample { public: __declspec(dllexport) Sample(int a); __declspec(dllexport) void foo() const; __declspec(dllexport) void disp() const; void bar() const; private: int m_a; }; Bu örnekte sınıfın "yapıcı fonksiyonu (constructor)" foo ve disp fonksiyonları dışarıdan kullanılabilir. Ancak bar fonksiyonu dışarıdan kullanılamaz. __declspec(dllexport) belirleyicisi tanımlama sırasında kullanılmak zorunda değildir. * Örnek 1, #include using namespace std; class Sample { public: __declspec(dllexport) Sample(int a); __declspec(dllexport) void foo() const; __declspec(dllexport) void disp() const; void bar() const; private: int m_a; }; Sample::Sample(int a) { m_a = a; } void Sample::foo() const { cout << "foo" << endl; } void Sample::disp() const { cout << m_a << endl; } void Sample::bar() const { cout << "bar" << endl; } * Örnek 2, Bir sınıfın bütün üye fonksiyonlarının dışarıdan kullanılması isteniyorsa Windows sistemlerinde __declspec(dllexport) belirleyicisi class anahtar sözcüğü ile sınıf ismi arasına getirilebilir. class __declspec(dllexport) Sample { public: Sample(int a); void foo() const; void disp() const; void bar() const; private: int m_a; }; Burada sınıfın bütün üye fonksiyonları dışarıdan kullanılabilir. #include using namespace std; class __declspec(dllexport) Sample { public: Sample(int a); void foo() const; void disp() const; void bar() const; private: int m_a; }; Sample::Sample(int a) { m_a = a; } void Sample::foo() const { cout << "foo" << endl; } void Sample::disp() const { cout << m_a << endl; } void Sample::bar() const { cout << "bar" << endl; } Windows'ta bir DLL içerisindeki fonksiyonlar dışarıdan kullanılırken yalnızca prototip yazılması aslında yeterlidir. Benzer biçimde bir DLL içerisindeki global nesneler dışarıdan kullanılırken yalnızca extern bildirimi yeterlidir. Ancak ilgili fonksiyonun ve global nesnenin DLL içerisinde olduğu derleyiciye bildirilirse derleyici daha etkin kod üretebilmektedir. (Bu konudaki ayrıntılar "Windows Sistem Programlama Kursu" içerisinde ele alınmaktadır.) İşte derleyiciye ilgili fonksiyonun ya da global nesnenin bir DLL içeirisinde olduğunu anlatabilmek için fonksiyonun prototipinin önüne __declspec(dllimport) belirleyicisi getirilir. * Örnek 1, #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ __declspec(dllimport) double add(double a, double b); __declspec(dllimport) double sub(double a, double b); __declspec(dllimport) double multiply(double a, double b); __declspec(dllimport) double divide(double a, double b); __declspec(dllimport) void foo(void); /* extern declarations */ __declspec(dllimport) extern int g_x; __declspec(dllimport) extern int g_y; #endif Burada DLL içerisindeki fonksiyonların prototiplerinin başına __declspec(dllimport) belirleyicisi getirilmiştir. Ancak DLL içerisinden export edilmiş fonksiyonlar ve global nesneler kullanılırken ayrıca bağlama aşamasında bağlayıcının hangi DLL'den hangi sembollerin kullanıldığını bilmesi gerekir. İşte DLL kullanan programlar derlenirken bağlama aşamsında "DLL'in import kütüphanesi" denilen bir kütüphanenin de bulundurulması gerekmektedir. DLL'in import kütüphanesi DLL yaratılırken bağlayıcı tarafından zaten yaratılmaktadır. Örneğin: cl /Fe:myutil.dll /LD a.c b.c Burada biz "myutil.dll" isimli dinamik kütüphaneyi yaratmak istemekteyiz. İşte bu kütüphane yaratılırken aynı zamanda DLL'in import kütüphanesi de yaratılmaktadır. DLL'in import kütüphanesinin uzantısı ".lib" biçimindedir. Yukarıdaki derleme işleminin sonucunda aynı zamanda "myutil.lib" isimli DLL'in import kütüphanesi de oluşturulmaktadır. Her ne kadar import kütüphanelerinin uzantıları da ".lib" biçiminde olsa da bu dosyalar statik kütüphane dosyaları değildir. Bu dosya içerisinde yalnızca "DLL içerisindeki fonksiyonlara ilişkin bazı bilgiler" vardır. İşte bu dosya dinamik kütüphaneyi kullanan program bağlanırken bağlama aşamasında kulanılmalıdır. Örneğin: cl app.c myutil.lib Windows'ta bir DLL'i kullanabilmek için yalnızca DLL dosyası değil aynı zamanda o DLL'in import kütüphanesinin de elimizde bulunuyor olması gerekir. DLL'in import kütüphanesi yalnızca bağlama aşamasında kullanılmaktadır. Programın hedef makineye konuşlandırılması sırasında import kütüphanesinin hedef makineye kopyalanmasına gerek yoktur. Aşağıdaki örnekte "a.c" ve "b.c" dosyalarından "myutil.dll" dosyası oluşturulmuştur. Sonra bu DLL'i kullanan "app.c" programı derlenerek çalıştırılmıştır. Bu işlemleri komut satırında şöyle yapabilirsiniz: cl /OUT:myutil.dll /LD a.c b.c cl app.c myutil.lib Aşağıda program kodları da verilmiştir: /* app.c */ #include __declspec(dllexport) int g_x; __declspec(dllexport) double add(double a, double b) { return a + b; } __declspec(dllexport) double sub(double a, double b) { return a - b; } __declspec(dllexport) double multiply(double a, double b) { return a * b; } __declspec(dllexport) double divide(double a, double b) { return a / b; } /* b.c */ #include __declspec(dllexport) int g_y; __declspec(dllexport) void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ __declspec(dllimport) double add(double a, double b); __declspec(dllimport) double sub(double a, double b); __declspec(dllimport) double multiply(double a, double b); __declspec(dllimport) double divide(double a, double b); __declspec(dllimport) void foo(void); /* extern declarations */ __declspec(dllimport) extern int g_x; __declspec(dllimport) extern int g_y; #endif /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); return 0; } Kütüphaneler için birer başlık dosyası oluşturmanın iyi bir teknik olduğunu belirtmiştik. Bu başlık dosyası hem kütüphane derlenirken hem de kütüphane kullanılırken include edilir. Pekiyi bu başlık dosyasının içerisinde fonksiyon prototiplerinin önünde __declspec(dllexport) belirleyicisi mi yoksa __declspec(dllimport) belirleyicisi mi bulunmalıdır? İşte kütüphanenin kendisi derlenirken __declspec(dllexport) belirleyicisi kütüphaneyi kullanan kodlar derlenirken __declspec(dllimport) belirleyicisi bulunmalıdır. Bu işlem basit bir sembolik sabite dayalı olarak gerçekleştirilebilir. * Örnek 1, #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif Burada DLLBUILD isimli sembolik sabit define edilmişse DLLSPEC makrosu yerine __declspec(dllexport) belirleyicisi define edilmemişse DLLSPEC makrosu yerine __declspec(dllimport) belirleyicisi yerleştirilecektir. Bu durumda fonksiyon prototipleri şöyle belirtilebilir: DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); DLLSPEC void foo(void); İşte eğer DLL'in kendisi oluşturulacaksa bu başlık dosyasının include edildiği yerin başına DLLBUILD sembolik sabiti define edilir, eğer DLL kullanılacaksa bu sembolik sabit define edilmez. Böylece tek bir başlık dosyası hem DLL'deki dosyalar tarafından hem de o DLL'i kullanan dosyalar tarafından include edilebilir. Aşağıda program kodları verilmiştir: /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif /* Function Prototypes */ DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); DLLSPEC void foo(void); /* extern declarations */ DLLSPEC extern int g_x; DLLSPEC extern int g_y; #endif /* a.c */ #define DLLBUILD #include #include "myutil.h" int g_x; double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* b.c */ #define DLLBUILD #include #include "myutil.h" int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); return 0; } Bir DLL'i kullanan bir program çalıştırılmak istendiğinde işletim sistemi o programın kullandığı DLL'i sırasıyla belirli bazı dizinlerde aramaktadır. Çalıştırılabilen dosyanın içerisine kullanılan DLL'in yalnızca ismi yazılmaktadır. Onun bulunduğu yer yazılmamaktadır. Windows'ta çalıştırılabilen dosyanın kullandığı DLL'ler sırasıyla şu dizinlerde aranmaktadır: -> Çalıştırılabilen program dosyasının bulunduğu dizin -> Windows\System32 dizini -> Window\System dizini -> Windows dizininin kendisi -> Programı çalıştırmak isteyen prosesin (yani CreateProcess uygulayan prosesin) çalışma dizini -> PATH çevre değişkeni ile belirtilen dizinlerde sırasıyla Dinamik kütüphaneler Windows sistemlerinde IDE'lerle daha kolay yaratılabilirler. Visual Studio IDE'sinde proje oluşturulurken proje türü olarak "Dynamic Link Library" seçilirse bu proje build edildiğinde DLL dosyası elde edilecektir. Ancak bu proje seçeneği projeye içinde yalnızca DllMain fonksiyonu bulunan bir C++ dosyası barındırmaktadır. Tabii boş olarak da bir DLL projesi oluşturulabilir. Bunun için boş bir console projesi yaratılır. İçerisine kaynak dosyalar yerleştirilir. Sonra proje ayarlarından "General/Build Type" seçeneği "Application" yerine "Dynamic Link Library" olarak seçilir. Artık proje build edildiğinde EXE dosyası yerine DLL dosyası dosyası oluşturulacaktır. Tabii DLL'in import kütüphanesi de aynı dizinde yaratılmış olacaktır. Dinamik kütüphaneler programın çalışma zamanı sırasında programcının istediği bir noktada programcı tarafından da yüklenebilmektedir. Bu duruma "dinamik kütüphanelerin dinamik yüklenmesi" denilmektedir. Dinamik yükleme özelliği hem Windows sistemlerinde hem de UNIX/Linux ve macOS sistemlerinde bulunmaktadır. Biz önce Windows sistemlerinde sonra da UNIX/linux sistemlerinde bunun yapılacağını göreceğiz. macOS sistemlerinde işlemler tamamen UNIX/Linux sistemlerinde olduğu gibi yapılmaktadır. Windows sistemlerinde dinamik kütüphanelerin dinamaik biçimde yüklenerek kullanılması sırasıyla şu aşamalardan geçilerek yapılmaktadır: -> Önce LoadLibrary API fonksiyonuyla DLLprosesin sanal bellek alanına yüklenir. LoadLibrary API fonksiyonunun prototipi şöyledir : HMODULE LoadLibraryA( LPCSTR lpLibFileName ); Fonksiyon yüklenecek olan DLL'in yol ifadesini parametre olaraj almaktadır. Eğer yol ifadesinde en az bir '\' karakteri varsa DLL yalnızca o dizinde aranmaktadır. Eğer yol ifadesinde hiçbir '\' karakteri yoksa bu durumda DLL daha önce açıkladığımız gibi bazı dizinlerde sırasıyla aranır. Bu fonksiyon dinamik kütüphaneyi yükleyerek yüklenen sanal bellek adresine geri dönmektedir. Geri dönüş değeri HMODULE türündendir. Bu tür de aslında "void *" olarak typedef edilmiştir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. Örneğin: HMODULE hModule; if ((hModule = LoadLibrary("MyUtil.dll")) == NULL) ExitSys("LoadLibrary"); -> DLL içerisinden çağrılacak fonksiyonun ya da kullanılacak global değişkenin adresi GetProcAddress fonksiyonuyla elde edilir. Fonksiyonun prototipi şöyledir: FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName ); Fonksiyonun birinci parametresi LoadLibrary fonksiyonundan elde edilen modül yükleme adresini, ikinci parametresi ise adresi elde edlecek fonksiyonun ya da global değişkenin ismini almaktadır. Fonksiyon başarı durumunda ilgili fonksiyonun ya da global değişkenin adresine başarısızlık durumunda NULL adrese gerei dönmektedir. Fonksiyonun geri dönüş değeri olan FARPROC 32 bit Windows sistemlerinde aşağıdaki typedef edilmiştir: typedef int (WINAPI *FARPROC)(); 64 bit Windows sistemlerinde ise şöyle typedef edilmiştir: typedef INT_PTR (WINAPI *FARPROC)(); Bu geri dönüş değeri genel bir tür biçiminde bulundurulmuştur. Uygun fonksiyon türüne dönüştürrülmelidir. Örneğin: typedef double (*PROCADDR)(double, double); ... PROCADDR padd; if ((padd = (PROCADDR)GetProcAddress(hModule, "add")) == NULL) ExitSys("GetProcAddress"); GetProcAddress fonksiyonunda bir noktaya dikkat edilmesi gerekir. Derleyiciler C'deki değişken isimlerini amaç dosyaya dekore ederek yazabilmektedir. Bizim GetProcAddress fonksiyonunda bu dekore edilmiş isimleri kullanmamız gerekmektedir. Microsoft C derleyicilerinde cdecl çağırma biçiminde sembollerin başına '_' karakteri getirerek bir dekorasyon uygulamamaktadır. Fakat Microsoft bağlayıcıları sembolleri DLL'in export tablosuna yazarken bu baştaki '_' karakterlerini atmaktadır. C++'ta farklı parametrik yapılara ilişkin aynı isimli fonksiyonlar bulunabileceği için C++ derleyicilerinin hepsi meecburen bir isim dekorasyonu uygulamaktadır. İşte programcının export sembol isimlerin emin olabilmesi için dumpbin programı ile DLL dosyasının export tablosuna bakabilir. Örneğin: C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutil.dll Microsoft (R) COFF/PE Dumper Version 14.39.33523.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file myutil.dll File Type: DLL Section contains the following exports for myutil.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 7 number of functions 7 number of names ordinal hint RVA name 1 0 00001000 add 2 1 00001060 divide 3 2 00001080 foo 4 3 0001B39C g_x 5 4 0001B398 g_y 6 5 00001040 multiply 7 6 00001020 sub Summary 2000 .data 7000 .rdata 1000 .reloc 12000 .text Ancak eğer biz bir C++ dosyasını derleyip ondan dinmaik kütüphane yapmış olsaydık sembol isimleri oldukça farklılaşacaktı. Örneğin: C:\Dropbox\Shared\Kurslar\SysProg-1\Src\Libraries\Dynamic>DUMPBIN /EXPORTS myutilcpp.dll Microsoft (R) COFF/PE Dumper Version 14.39.33523.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file myutilcpp.dll File Type: DLL Section contains the following exports for myutilcpp.dll 00000000 characteristics FFFFFFFF time date stamp 0.00 version 1 ordinal base 7 number of functions 7 number of names ordinal hint RVA name 1 0 00001000 ?add@@YANNN@Z 2 1 00001060 ?divide@@YANNN@Z 3 2 00001080 ?foo@@YAXXZ 4 3 0001A968 ?g_x@@3HA 5 4 0001A96C ?g_y@@3HA 6 5 00001040 ?multiply@@YANNN@Z 7 6 00001020 ?sub@@YANNN@Z Summary 2000 .data 7000 .rdata 1000 .reloc 12000 .text -> Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir. Örneğin: result = padd(10, 20); -> Dinamik kütüphanenin kullanımı bittikten sonra kütüphane FreeLibrary API fonksiyonu ile prosesin adres alanından boşaltılabilir. FreeLibrary fonksiyonunun prototipi şöyledir: BOOL FreeLibrary( HMODULE hLibModule ); Fonksiyon modülün yüklenme adresini parametre olarak alır, başarı durumunda sıfır dışı bir değere başarısızlık durumunda sıfır değerine geri döner. Tabii proses sonlaadığında bütün dinamik kütüphaneler zaten adres alanından boşaltılmaktadır. Aşağıda Windows sistemlerinde dinamik kütüphanenin dinamik yüklenmesine ilişkin bir örnek verilmiştir. Bu örnekteki "myutil.dll" dosyasının proje dizininde bulunduğundan emin olunuz. DLL'in derlenmesini komut satırından şöyle yapabilirsiniz. cl /Fe:myutil.dll a.c b.c /LD Aşağıda programın kodları verilmiştir: /* app.c */ #include #include #include void ExitSys(LPCSTR lpszMsg); typedef double (*PROCADDR)(double, double); int main(void) { HMODULE hModule; PROCADDR padd, psub, pmultiply, pdivide; double result; if ((hModule = LoadLibrary("myutilcpp.dll")) == NULL) ExitSys("LoadLibrary"); if ((padd = (PROCADDR)GetProcAddress(hModule, "?add@@YANNN@Z")) == NULL) ExitSys("GetProcAddress"); result = padd(10, 20); printf("%f\n", result); if ((psub = (PROCADDR)GetProcAddress(hModule, "sub")) == NULL) ExitSys("GetProcAddress"); result = psub(10, 20); printf("%f\n", result); if ((pmultiply = (PROCADDR)GetProcAddress(hModule, "multiply")) == NULL) ExitSys("GetProcAddress"); result = pmultiply(10, 20); printf("%f\n", result); if ((pdivide = (PROCADDR)GetProcAddress(hModule, "divide")) == NULL) ExitSys("GetProcAddress"); result = pdivide(10, 20); printf("%f\n", result); FreeLibrary(hModule); return 0; } void ExitSys(LPCSTR lpszMsg) { DWORD dwLastErr = GetLastError(); LPTSTR lpszErr; if (FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, dwLastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpszErr, 0, NULL)) { fprintf(stderr, "%s: %s", lpszMsg, lpszErr); LocalFree(lpszErr); } exit(EXIT_FAILURE); } /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ #ifdef DLLBUILD #define DLLSPEC __declspec(dllexport) #else #define DLLSPEC __declspec(dllimport) #endif /* Function Prototypes */ DLLSPEC double add(double a, double b); DLLSPEC double sub(double a, double b); DLLSPEC double multiply(double a, double b); DLLSPEC double divide(double a, double b); /* extern declarations */ DLLSPEC extern int g_x; #endif /* myutil.c */ #define DLLBUILD #include #include "myutil.h" int g_x; double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } >>> UNIX/Linux Sistemleri : UNIX/Linux sistemlerinde dinamik kütüphane oluşturmak için önce dinamik kütüphane oluşturulacak ".c" dosyaları "position indepenedent code" tekniği ile derlenmelidir. "Position Independent Code" ilgili dinamik kütüphanenin yüklenme yerinden bağımsız çalışabilmesini sağşamaktadır. Windows bu tekniği tercih etmemiştir. Windows'un yükleyicisi "relocation" işlemi ile DLL'leri farklı yerlere yükleyebilmektedir. "Position Independent Code" tekniği ile derlenmiş olan dosyalar "-shared" bağlama seçeneği ile bağlanırsa dinamik kütüphane oluşturulabilmektedir. UNIX/Linux sistemlerinde kütüphane içerisindeki global sembollerin hepsi otomatik olarak export edilmektedir. Dolayısıyla bu sistemlerde Windows sistemlerinde olduğu gibi __declspec(dllexport) ve __declspec(dllimport) biçiminde bildirimler yoktur. Tabii bu konuda da bazı ayrıntılar bulunmaktadır. gcc ve clang derleyicilerinde bir dosyanın "konumdan bağımsız" biçimde derlenmesi için "-fPIC" seçeneğinin kullanılması gerekir. Örneğin: $ gcc -c -fPIC a.C $ gcc -c -fPIC b.C Burada "a.o" ve "b.o" dosyaları oluşturulacktır. Ayrıca yukarıda da belirttiğimiz Bağlama işleminde "-shared" seçeneğinin kullanılması gerekmektedir. Örneğin: $ gcc -o libmyutil.so -shared a.o b.o Aslında bu iki işlem tek hamlede de aşağıdaki gibi yapılabilir: $ gcc -o libmyutil.so -shared -fPIC a.c b.c Yine UNIX/Linux sistemlerinde dinamik kütüphane dosyaları başına "lib" öneki getirilerek isimlendirilmelidir. * Örnek 1, Aşağıda dinamik kütüphaneyi oluşturan "a.c" ve "b.c" dosyaları verilmiştir. Dinamik kütüphaneyi yukarıda belirttiğimiz biçimde oluşturmayı deneyiniz. /* a.c */ #include int g_x; double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } UNIX/Linux sistemlerinde "dinamik kütüphanelerin import kütüphanesi" denilen kütüphaneleri yoktur. Dinamik kütüphane kullanan programlar doğrudan bu dinamik kütüphane dosyasının kendisini bağlama aşamasında kullanmaktadır. Örneğin "app.c" programı "libmyutil.so" dinamik kütüphanesini kullanıyor olsun. Bu "app.c" programının derlenmesi şöyle yapılmaktadır: gcc -o app app.c libmyutil.so Aşağıdaki "app.c" programını bu biçimde derleyiniz. * Örnek 1, /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern declarations */ extern int g_x; extern int g_y; #endif /* app.c */ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); bar(); return 0; } UNIX/Linux sistemlerinde yine çalıştırılabilen dosyanın içerisine yalnızca dinamik kullanılan dinamik kütüphanelerin isimleri yazılmaktadır. Dinamik kütüphaneler yine bu sistemlerde de belli yerlerde aranmaktadır. Ancak bu sistemlerde dinamik kütüphaneler çalıştırılabilen dosyanın bulunduğu dizinde ya da onu çalıştıran prosesin çalışma dizininde aranmamaktadır. Bu nedenle örneğin Linux sistemlerinde dinamik kütüphane kullanan bir programla dinamik kütüphanenin kendisi aynı dizinde bulunuyor olsa bile program çalıştırıldığında dinamik kütüphane bulunamayacaktır. UNIX/Linux sistemlerinde dinamik kütüphanelerin aranma prosedürlerine ilişkin bazıı ayrıntılar vardır. Biz burada bu ayrıntılar üzerinde durmayacağız. Ancak kabaca arama için üç önemli adımı şöyle belirtebiliriz: -> Programı çalıştıran (yani exec yapan) prosesin LD_LIBRARY_PATH çevre değişkeni ile belirtilen dizinlerinde tek tek arama yapılmaktadır. Bu eçvre değişkeninin değeri ':' karakterleriyle ayrılan dizinler biçiminde olabilir. Örneğin: $ export LD_LIBRARY_PATH=/home/kaan:/home/kaan/Study:. Burada dinamik kütüphaneler sırasıyla "/home/kaan" dizininde, "/home/kaan/Study" dizininde ve o anda exec yapan prosesin çalışma dizininde aranacaktır. Tabii kabuk üzerinde çalıtırmayı aşağıdaki gibi de yapabiliriz: $ LD_LIBRARY_PATH=. ./app -> Çalıştırılacak programın ".dynamic" bölümünde DT_RUNPATH özelliği varsa oradaki dizinde aranmaktadır. -> "/lib" ve "/usr/lib" dizinlerine bakılır. Bazı 64 bit Linux dağıtımlarında "/lib64" ve "/usr/lib64" dizinlerine de bakılmaktadır. Eğer bu sistemlerde oluşturduğumuz dinamik kütüphaneler başka programlar tarafından da kullanılıyorsa onun yerleştirilmesi gereken en doğal "/usr/lib" dizinidir. "/lib" dizini daha aşağı seviyeli işletim sistemi tarafıdan kullanılan dinamik kütüphanelere ayrılmıştır. Dinamik kütüphanelerin UNIX/Linux sistemlerinde dinamik yüklenmesi aslında Windows sistemlerindekine benzemektedir. İşlemler sırasıyla şöyle yürütülür: -> Önce dinamik kütüphane ldopen fonksiyonuyla adres alanına yüklenir. Bu fonksiyonu Windows sistemlerindeki LoadLibrary fonksiyonuna benzetebilirsiniz. Bu fonksiyon yine dinamik kütüphanenin bellekteki yükleme adresine geri dönmektedir. Fonksiyonun prototipi şöyledir: #include void *dlopen(const char *filename, int flags); Fonksiyonun birinci parametresi yüklenecek dinamik kütüphane dosyasının yol ifadesini belirtmektedir. Burada yol ifadesinde hiçbir '/' karakteri kullanılmazsa dosya yukarıda belirtilen dizinlerde sırasıyla aranmaktadır. Ancak buradaki yol ifadesinde en az bir '/' kullanılırsa dosya yalnızca o yol ifadesinde belirtilen dizinde aranır. İkinci parametre sembol çözümlemesinin ne zaman yapılacağına ilişkin bayraklardan oluşmaktadır. Bu parametre RTLD_LAZY ya da RTLD_NOW değerlerinden biri biçiminde girilebilir. RTLD_LAZY sembol çözümlemesinin başvuru (yani fonksiyonun çağrılması ya da global değişkenin kullanılması sırasında) sırasında yapılacağını RTLD_NOW ise yükleme sırasında yapılacağını belirtmektedir. Diğer bayraklar için dokümanalara başvurulabilir. Fonksiyon başarı durumunda yükleme adresine, başarısızlık durumunda NULL adrese geri döner. Başarısızlık nedeni için errno değişkeni set edilmez. Başarısızlık nedeni yazısal olarak dlerror fonksiyonuyla elde edilmelidir. dlerror fonksiyonun prototipi şöyledir: char *dlerror(void); Fonksiyon statik bir biçimde tahsis edilmiş olan hata yazısının adresine geri dönmektedir. Örneğin: void *soaddr; ... if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } Tıpkı Windows sistemlerinde olduğu gibi dlopen fonksiyonu dinamik kütüphanenin yol ifadesi belirtilirken eğer hiç '/' karakteri kullanılmazsa bu durumda dosya Linux sistemlerindeki yukarıda belirttiğimiz dizinlerde aranmaktadır. (Linux sistemlerinde arama sırasında çalışma dizinine bakılmadığını anımsayınız). Eğer yol ifadesinde en az bir tane '/' karakteri kullanılmışsa dosya belirtilen yol ifadesinde aranmaktadır. -> dlsym fonksiyonuyla dinamik kütüpahane içerisindeki herhangi bir fonksiyon ya da nesnenin adresi elde edilebilmektedir. dlsym fonksiyonun prototipi şöyledir: #include void *dlsym(void *handle, const char *symbol); Fonksiyonun birinci parametresi dlopen fonksiyonundan elde edilen adresi, ikinci parametresi ise adresi elde edilecek fonksiyon ya da global nesnenin ismini belirtmektedir. Fonksiyon başarı dıurumunda ilgili nesnenin adresine başarısızlık durumunda NULL adrese geri dönmektedir. Yine başarısızlık nedeni dlerror fonksiyonuyla elde edilmelidir. C'de data adreslerinden fonksiyon adreslerine, fonksiyon adreslerinden data adreslerine tür dönüştürme operatör ile bile dönüştürmenin geçerli olmadığına dikkat ediniz. (Ancak derleyicilerin çoğu buna izin vermektedir.) Anımsayanacağınız gibi "void *" türü bir data adresi kabul edilmektedir. Örneğin: typedef double (*PROCADDR)(double); ... PROCADDR padd; ... if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } dlsym fonksiyonunu Windows sistemlerindeki GeProcAddress fonksiyonuna benzetebilirsiniz. -> Artık adresi elde edilmiş olan fonksiyon çağrılabilir, global değişken kullanılabilir. -> Dinamik kütüphanenin kullanımı bittiğinde kütüphane dlclose fonksiyonuyla boşaltılır. Fonksiyonun prototipi şöyledir: #include int dlclose(void *handle); Fonksiyon parametre olarak dinamik kütüphanenin yükleme adres,n, alıp onu adres alanından yok etmektedir. Fonksiyon başarı durumunda sıfır değrine başarısızlık durumunda sıfır dışı bir değere geri dönemektedir. Yine başarısızlığın nedeni dlerror fonksiyonuyla yazdırılabilir. Örneğin: dlclose(soaddr); Bütün bu fonksiyonlar "libdl.so" kütüphanesi içerisinde bulunmaktadır. Bu nedenle derleme yaparken komut satırında "-ldl" argümanını bulundurunuz. Örnek bir kullanım aşağıda verilmiştir. Derlemeleri aşağıdaki gibi yapabilirsiniz: $ gcc -fPIC -o libmyutil.so libmyutil.c -shared $ gcc -o app app.c -ldl Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, /* app.c */ #include #include #include void exit_sys(const char *msg); typedef double (*PROCADDR)(double, double); int main(void) { void *soaddr; PROCADDR padd, psub, pmultiply, pdivide; double result; if ((soaddr = dlopen("./libmyutil.so", RTLD_LAZY)) == NULL) { fprintf(stderr, "dlopen: %s\n", dlerror()); exit(EXIT_FAILURE); } if ((*(void **)&padd = dlsym(soaddr, "add")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = padd(10, 20); printf("%f\n", result); if ((*(void **)&psub = dlsym(soaddr, "sub")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = psub(10, 20); printf("%f\n", result); if ((*(void **)&pmultiply = dlsym(soaddr, "multiply")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pmultiply(10, 20); printf("%f\n", result); if ((*(void **)&pdivide = dlsym(soaddr, "divide")) == NULL) { fprintf(stderr, "dlsym: %s\n", dlerror()); exit(EXIT_FAILURE); } result = pdivide(10, 20); printf("%f\n", result); return 0; } void exit_sys(const char *msg) { perror(msg); exit(EXIT_FAILURE); } /* libmyutil.c */ #include double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } Bir kütüphanedeki fonksiyonlar çağrılabilmesi için çağrılan kaynak dosyada o fonksiyonların prototipilerinin bulundurulması gerekir. Benzer biçimde kütüphane içerisindeki global değişkenlerin kullanılabilmesi için de o global değişkenlerin extern bildirimlerinin bulundurulması gerekir. Her kütüphane için o kütüphanedeki bildirimleri barındıran bir başlık dosyasının bulundurulması iyi bir tekniktir. * Örnek 1, /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern Declarations */ extern int g_x; extern int g_y; #endif Böylece bu kütüphaneyi kullanacak kişiler bu başlık dosyasını include ederek bütün kütüphane ile ilgili bildirimleri bulundurmuş olurlar. Eğer kütüphane çok fazla öğeden oluşyorsa tek bir başlık dosyası önişlemci (preprocessor) zamanını uzatabilir. Bu durumda bir tane değil birden çok başlık dosyası bulundurulabilir. Örneğin aslında standart C fonksiyonlarının hepsi tek bir kütüphane içerisindedir. Ancak onlara ilişkin bildirimler çeşitli başlık dosyalarına yaydırılmıştır. Bağlayıcılar onlara verilen amaç dosyalar dışında ayrıca temel bazı kütüphanelere otomatik olarak bakmaktadır. Microsoft'un bağlayıcı programı olan "link.exe" hiç belirtilmese bile standart C fonksiyonlarının bulunduğu, Windows API fonksiyonlarının bulunduğu kütüphanelere zaten bakmaktadır. Ancak programcı bağlayıcının kendi kütüphanelerine de bakmasını istiyorsa bunu bağlayıcıya komut satırından söylemelidir. Anımsanacağı gibi Microsoft'un Cve C++ derleyicisi olan "cl.exe" programı "/c" seçeneği belirtilmezse derleme sonrasında "link.exe" bağlayıcısını kendisi çalıştırmaktadır. İşte biz de "cl.exe" programının komut satırında derlenecek kaynak dosyalardan sonra uzantısı ".lib" olan kütüphane dosyalarını belirtirsek "link.exe" bağlayıcısı o kütüphane dosyalarına da bağlama aşamasında bakmaktadır. Örneğin: cl app.c myutil.lib Burada "app.c" programı derlenerek "app.obj" dosyası oluşturulacak sonra bağlama aşamasında "myutil.lib" dosyasına da bakılacaktır. Oluşturulacak çalıştırılabilir dosyanın ismi ilk kaynak dosyanın ismi olarak (örneğimizde "app.exe") alınacaktır. Tabii istenirse "cl.exe" komut satırında /Fe: argümanı ile çalıştırılabilir dosyaya arzu edilen bir isim de verilebilmektedir. Örneğin: cl /Fe:project.exe app.c myutil.lib Tabii istersek derleyici ve bağlayıcıyı ayrı ayrı da çalıştırabiliriz. Örneğin: cl /c app.c link app.obj myutil.lib Burada yine default olarak "link.exe" programı ilk amaç dosyanın ismini çalıştırılabilir dosyaya vermektedir. Ancak çalıştırılabilen dosyanın ismi /OUT: seçeneği ile de bağlayıcıya verilebilmektedir. Örneğin: cl /c app.c link /OUT:project.exe app.obj myutil.lib Aşağıda bu konuya ilişkin bir örnek verilmiştir: * Örnek 1, örnekte "a.c" ve "b.c" dosyalarından "myutil.lib" statik kütüphanesi oluiştuurulmuş ve bu statik kütüphane de "app.c" programından kullanılmıştır. İşlemleri komut satırından aşağıdaki sırada yapabilirsiniz: cl -c a.c cl -c b.c lib /OUT:myutil.lib a.obj b.obj cl app.c myutil.lib Programın kodları ise şu şekildedir: /* a.c */ #include int g_x; double add(double a, double b) { return a + b; } double sub(double a, double b) { return a - b; } double multiply(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } /* b.c */ #include int g_y; void foo(void) { printf("foo\n"); } void bar(void) { printf("bar\n"); } /* myutil.h */ #ifndef MYUTIL_H_ #define MYUTIL_H_ /* Function Prototypes */ double add(double a, double b); double sub(double a, double b); double multiply(double a, double b); double divide(double a, double b); void foo(void); void bar(void); /* extern declarations */ extern int g_x; extern int g_y; #endif /* app.c*/ #include #include "myutil.h" int main(void) { double result; result = add(10, 20); printf("%f\n", result); result = sub(10, 20); printf("%f\n", result); result = multiply(10, 20); printf("%f\n", result); result = divide(10, 20); printf("%f\n", result); foo(); bar(); return 0; }