Այս զրույցում ես պատմում եմ, թե ինչպես C լեզվով պատրաստել գրադարան և այն օգտագործել ծրագրի մեջ։ Ինչպես նաև պատրաստել այդ գրադարանի՝ դինամիկ և ստատիկ կապակցվող մոդուլները։ Եվ քանի որ նպատակս գրադարանի ստեղծման տեխնիկայի ցուցադրումն է, շատ բարդ բաների հետևից չեմ ընկնի և որպես օրինակ կներկայացնեմ միայն մեկ ֆունկցիա պարունակող գրադարան։
Ենթադրենք ուզում եմ ստեղծել գրադարան, որը նախատեսված է տրված միջակայքում տրված ֆունկցիայի թվային ինտեգրալը հաշվելու համար։ Օգտագործման հեշտության համար ուզում եմ, որ այդ գրադարանի ինտերֆեյսը լինի հնարավորինս պարզ, այն է՝ պարունակի միակ integrate
ֆունկցիան։ Այդ ֆունկցիան ստանում է ինտեգրվող ֆունկցիայի ցուցիչն ու ինտեգրման միջակայքը։ Օրինակ, եթե x² ֆունկցիան սահմանված է հետևյալ կերպ․
double sqr(double x)
{
return x * x;
}
ապա [-1;1]
միջակայքում դրա թվային ինտեգրալը կարելի է հաշվել հետևյալ հրամանով․
double r0 = integral( &sqr, -1, 1 );
Հիմա ցույց տամ, թե ինչպես է իրականացված integral
ֆունկցիան, ինչպես կարելի է այդ իրականացումը վերածել գրադարանի և օգտագործել այլ ծրագրերում։
Նախ պետք է պատրաստել գրադարանի իրականացումը։ Դրա համար ստեղծում եմ ni.c
(numeric integral) ֆայլը, որում սահմանված են epsilon
հաստատունը և Սիմպսոնի բանաձևը՝ simpson
ֆունկցիան։
static const double epsilon = 1e-5;
static double simpson( double(*f)(double), double a, double b )
{
return ((b - a) / 6) * (f(a) + 4 * f((a + b) / 2) + f(b));
}
Նույն ni.c
ֆայլում է սահմանված նաև integral
ռեկուրսիվ ֆունկցիան, որը «բաժանիր և տիրիր» մոտեցմամբ հաշվում է տրված ֆունկցիայի մոտավոր ինտեգրալը։
double integral( double(*f)(double), double a, double b )
{
if( b - a < epsilon )
return simpson( f, a, b );
double m = (a + b) / 2.0;
return integral( f, a, m ) + integral( f, m, b );
}
Եթե ինտեգրման միջակայքի երկարությունը փոքր է նախապես սահմանված epsilon
թվից, ապա այդ հատվածի վրա կիրառվում է Սիմպսոնի բանաձևը։ Հակառակ դեպքում ինտեգրման միջակայքը տրոհվում է երկու հավասար մասերի, մասերից ամեն մեկի վրա նորից կիրառվում է integral
ֆունկցիան, իսկ ինտեգրման արժեք է համարվում միջակայքի երկու կեսերի վրա ստացված արժեքների գումարը։
Որպեսզի հնարավոր լինի integrate
ֆունկցիան օգտագործել այլ ծրագրում, C լեզվի կոմպիլյատորին պետք է հայտարարության միջոցով տեղեկացնել դրա գոյության մասին։ Գրադարաններ կազմակերպելիս «արտաքին աշխարհին» տրամադրվող անունների (ֆունկցիաների, ստրուկտուրաների և այլն) հայտարարությունները գրվում են վերնագրային (header) ֆայլում։ Իսկ այն տեղում, որտեղ պետք է օգտագործվի գրադարանը, #include
հրահանգով կցվում է վերնագրային ֆայլը։ Թվային ինտեգրալը հաշվող գրադարարանի համար ստեղծեմ ni.h
ֆայլը՝ հետևյալ պարունակությամբ․
#ifndef NUMERIC_INTEGRAL
#define NUMERIC_INTEGRAL
extern double integral( double(*)(double), double, double );
#endif
Ֆունկցիայի հայտարարությունից առաջ գրված extern
ծառայողական բառն ուզում է ասել, թե integral
ֆունկցիան սահմանված է մի որևէ այլ տեղ (գուցե հենց նույն ֆայլում)։
Արդեն պատրաստ է «Numeric integral» գրադարանը․ դրա իրականացումը ni.c
ֆայլում է, իսկ ինտերֆեյսը՝ ni.h
ֆայլում։ Հիմա ցույց տամ, թե ինչպես եմ այս գրադարանն օգտագործելու։ Ենթադրենք ուզում եմ [-1;1]
միջակայքի վրա հաշվել x² և x³ ֆունկցիաների մոտավոր ինտեգրալը։ Ստեղծեմ prog11.c
ֆայլը հետևյալ պարունակությամբ։
#include <stdio.h>
#include "ni.h"
double sqr(double x) { return x * x; }
double cub(double x) { return x * x * x; }
int main()
{
double r0 = integral( &sqr, -1, 1 );
printf( "> %lf\n", r0 );
double r1 = integral( &cub, -1, 1 );
printf( "> %lf\n", r1 );
return 0;
}
Այստեղ ni.h
ֆայլը prog11.c
ֆայլին կցված է #include
հրահանգով, բայց ֆայլի անունը նշելու համար օգտագործված են ոչ թե <
և >
նիշերը, այլ սովորական "
չակերտները։ Երբ ֆայլի անունը վերցրած է "
չակերտների մեջ, C լեզվի նախապրոցեսորը այդ ֆայլը որոնում է առաջին հերթին կոմպիլյացիայի պանակում։ Իսկ եթե օգտագործված են <
և >
նիշերը, ապա որոնումը սկսվում է նախապես որոշված տեղերից, որոնք համակարգում սահմանվում են կոմպիլյատորի տեղադրման հետ։
prog11.c
ֆայլը պետք է կոմպիլյացնել ni.c
ֆայլի հետ։ Օրինակ, հետևյալ հրամանով․
$ clang -o prog11 ni.c prog11.c
Կամ, կարելի է երկու *.c
ֆայլերը կոմպիլյացնել առանձին֊առանձին ու ստանալ օբյեկտային մոդուլները, ապա կապակցել դրանք ու ստանալ կատարվող մոդուլը։ Օրինակ՝ այսպես․
$ clang -c -o ni.o ni.c
$ clang -c -o prog11.o prog11.c
$ clang -o prog11 ni.o prog11.o
Ստացված prog11
մոդուլը կատարելուց հետո կարտածվեն [-1;1]
միջակայքում քառակուսային և խորանարդային ֆունկցիաների ինտեգրալները։ Ահա այն ամբողջ ճանապարհը, որ պետք է անցնել կոդի մակարդակով գրադարան ստեղծելու և օգտագործելու համար։
Բայց միշտ չէ, որ հնարավոր է և պետք է գրադարանն օգտագործել կոդի մակարդակում։ Ծրագրերի մշակման աշխարում գոյություն ունեն գրադարանների ևս երկու հիմնական տեսակներ՝ ստատիկ կապակցվող և դինամիկ կապակցվող։ Ծրագրում օգտագործելիս ստատիկ կապակցվող գրադարանը «կապակցվում» է կատարվող մոդուլի հետ և դառնում է դրա մասը։ Դինամիկ գրադարանը ծրագրում օգտագործելիս այն չի մտնում կատարվող մոդուլի կազմի մեջ և բեռնվում է կատարման ժամանակ։
Նախ ցույց տամ, թե ինչպես պետք է ni.c
ֆայլից ստանալ ստատիկ գրադարան։ Դրա համար պետք է ֆայլը կոմպիլյացնել և ստեղծել օբյեկտային ֆայլ, այնուհետև օբյեկտային ֆայլից (կամ ֆայլերից, եթե դրանք մի քանի հատ են) ստանալ ստատիկ գրադարանի արխիվը։
Քիչ առաջ արդեն ցույց տվեցի, թե .c
ֆայլը կոմպիլյացնելու և համապատասխան օբյեկտային ֆայլը ստանալու համար ինչ հրաման պետք է գրել։ (gcc
և clang
կոմպիլյատորների -c
պարամետրը թարգմանում է ծրագրի տեքստը օբյեկտային մոդուլի։)
$ clang -c -o ni.o ni.c
Ստատիկ գրադարանի ֆայլը, որի անունը պետք է ունենա lib
նախածանցը, իսկ ընդլայնումը պետք է լինի .a
, ստեղծվում է ar
արխիվացման ծրագրի օգնությամբ։
$ ar rcs libni.a ni.o
Այս հրամանի հաջող կատարումով ստեղծվում է libni.a
ստատիկ կապակցվող գրադարանը։ Այդ գրադարանն օգտագործելու համար պետք է ունենալ հայտարարությունների ni.h
ֆայլը, և գրադարանի իրականուցումը պարունակող libni.a
ֆայլը։
Հիմա ենթադրենք ուզում եմ prog11.c
ֆայլից ստանալ կատարվող մոդուլ, օգտագործելով libni.a
ստատիկ գրադարանը։ Դրա համար պետք է կոմպիլյատորի -l
պարամետրով տալ գրադարանի անունը՝ առանց lib
նախածանցի և .a
ընդլայնման, ինչպես նաև -static
պարամետրով նշել, որ կատարվելու է ստատիկ կապակցում։
$ gcc -o prog11 prog11.c -lni -static
Այս հրամանի կատարումից հետո արտածվում է հաղորդագրություն, որում ասվում է, թե կապերի խմբագրիչը չի գտել ni
գրադարանը։
/usr/bin/ld: cannot find -lni
collect2: error: ld returned 1 exit status
Բանն այն է, որ -l
պարամետրով նշված գրադարանները որոնվում են կապերի խմբագրիչին (linker) նախապես հայտնի կոնկրետ տեղերում։ Որպեսզի որոնման ճանապարհներում ներառվեն նաև լրացուցիչ պանակներ, դրանք պետք է թվարկել կոմպիլյացիայի (և կապակցման) հրամանի -L
պարամետրով։ Իմ օրինակում պարզապես պետք է նշել ընթացիկ պանակը։
$ gcc -o prog11 prog11.c -Լ. -lni -static
Արդեն կարող եմ գործարկել prog11
մոդուլը և համոզվել, որ այն աշխատում է իմ սպասածի պես։
Անցնեմ առաջ ու պատմեմ դինամիկ (shared) գրադարան ստեղծելու մասին։ Դինամիկ գրադարան ստեղծելու համար C լեզվի կոմպիլյատորին պետք է հրահանգել, որ գեներացնի տեղաբաշխումից անկախ կոդով (position-independent code ― PIC) օբյեկտային ֆայլ։ Սա նշանակում է, որ այդպիսի կոդը կարող է առանց փոփոխության կատարվել՝ բեռնվելով հիշողության կամայական տիրույթում։ PIC կոդ գեներացնելու համար կոմպիլյատորին պետք է տալ -fpic
հրահանգը։ Ոչ PIC կոդով օբյեկտային ֆայլից տարբերելու համար PIC կոդով օբյեկտային ֆայլին հաճախ տալիս են .lo
ընդլայնումը։
$ gcc -c -fpic ni.c -o ni.lo
Այնուհետև ni.lo
օբյեկտային ֆայլից պետք է կառուցել lib
նախածանց և .so
ընդլայնում ունեցող դինամիկ գրադարանի ֆայլը։
$ gcc -shared -o libni.so ni.lo
Եթե այս հրամանի հաջող կատարումից հետո ls
հրամանով արտածեմ ընթացիկ պանակի պարունակությունը, կտեսնեմ, որ այնտեղ ստեղծվել է libni.so
մոդուլը։
(դինամիկ գրադարանի օգտագործման մասին, օրինակ)