Զրույցներ C ծրագրավորման լեզվի մասին

Զրույց տասնմեկերորդ

Այս զրույցում ես պատմում եմ, թե ինչպես C լեզվով պատրաստել գրադարան և այն օգտագործել ծրագրի մեջ։ Ինչպես նաև պատրաստել այդ գրադարանի՝ դինամիկ և ստատիկ կապակցվող մոդուլները։ Եվ քանի որ նպատակս գրադարանի ստեղծման տեխնիկայի ցուցադրումն է, շատ բարդ բաների հետևից չեմ ընկնի և որպես օրինակ կներկայացնեմ միայն մեկ ֆունկցիա պարունակող գրադարան։

Ենթադրենք ուզում եմ ստեղծել գրադարան, որը նախատեսված է տրված միջակայքում տրված ֆունկցիայի թվային ինտեգրալը հաշվելու համար։ Օգտագործման հեշտության համար ուզում եմ, որ այդ գրադարանի ինտերֆեյսը լինի հնարավորինս պարզ, այն է՝ պարունակի միակ integrate ֆունկցիան։ Այդ ֆունկցիան ստանում է ինտեգրվող ֆունկցիայի ցուցիչն ու ինտեգրման միջակայքը։ Օրինակ, եթե ֆունկցիան սահմանված է հետևյալ կերպ․

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] միջակայքի վրա հաշվել և ֆունկցիաների մոտավոր ինտեգրալը։ Ստեղծեմ 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 մոդուլը։


(դինամիկ գրադարանի օգտագործման մասին, օրինակ)