C/Łączenie z innymi językami

< C

Programista, pisząc jakiś program ma problem z wyborem najbardziej odpowiedniego języka do utworzenia tego programu. Niekiedy zdarza się, że najlepiej byłoby pisać program, korzystając z różnych języków. Język C może być z łatwością łączony z innymi językami programowania, które podlegają kompilacji bezpośrednio do kodu maszynowego (Asembler, Fortran czy też C++). Ponadto dzięki specjalnym bibliotekom można go łączyć z językami bardzo wysokiego poziomu (takimi jak np. Python czy też Ruby). Ten rozdział ma za zadanie wytłumaczyć Ci, w jaki sposób można mieszać różne języki programowania w jednym programie.

Język C i Asembler

edytuj

Łączenie języka C i języka asemblera jest dość powszechnym zjawiskiem. Dzięki możliwości połączenia obu tych języków programowania można było utworzyć bibliotekę dla języka C, która niskopoziomowo komunikuje się z jądrem systemu operacyjnego komputera. Ponieważ zarówno asembler jak i C są językami tłumaczonymi do poziomu kodu maszynowego, za ich łączenie odpowiada program zwany linkerem (popularny ld). Ponadto niektórzy producenci kompilatorów umożliwiają stosowanie tzw. wstawek asemblerowych, które umieszcza się bezpośrednio w kodzie programu, napisanego w języku C. Kompilator, kompilując taki kod wstawi w miejsce tychże wstawek odpowiedni kod maszynowy, który jest efektem przetłumaczenia kodu asemblera, zawartego w takiej wstawce. Opiszę tu oba sposoby łączenia obydwu języków.

Łączenie na poziomie kodu maszynowego

edytuj

W naszym przykładzie założymy, że w pliku f1.S zawarty będzie kod, napisany w asemblerze, a f2.c to kod z programem w języku C. Program w języku C będzie wykorzystywał jedną funkcję, napisaną w języku asemblera, która wyświetli prosty napis "Hello world". Z powodu ograniczeń technicznych zakładamy, że program uruchomiony zostanie w środowisku POSIX na platformie i386 i skompilowany kompilatorem gcc. Używaną składnią asemblera będzie AT&T (domyślna dla asemblera GNU) Oto plik f1.S:

   .text
   .globl _f1
 _f1:
   pushl %ebp
   movl %esp, %ebp
   movl $4, %eax /* 4 to funkcja systemowa "write" */
   movl $1, %ebx /* 1 to stdout */
   movl $tekst, %ecx /* adres naszego napisu */
   movl $len, %edx /* długość napisu w bajtach */
   int $0x80 /* wywołanie przerwania systemowego */
   popl %ebp
   ret
 
   .data
 tekst:
   .string "Hello world\n"
   len = . - tekst

Teraz kolej na f2.c:

 extern void f1 (void); /* musimy użyć słowa extern */
 int main ()
 {
   f1();
   return 0;
 }

Teraz możemy skompilować oba programy:

as f1.S -o f1.o
gcc f2.c -c -o f2.o
gcc f2.o f1.o -o program

W ten sposób uzyskujemy plik wykonywalny o nazwie "program". Efekt działania programu powinien być następujący:

Hello world

Na razie utworzyliśmy bardzo prostą funkcję, która w zasadzie nie komunikuje się z językiem C, czyli nie zwraca żadnej wartości ani nie pobiera argumentów. Jednak, aby zacząć pisać obsługę funkcji, która będzie pobierała argumenty i zwracała wyniki musimy poznać działanie języka C od trochę niższego poziomu.

Argumenty

edytuj

Do komunikacji z funkcją język C korzysta ze stosu. Argumenty odkładane są w kolejności od ostatniego do pierwszego. Ponadto na końcu odkładany jest tzw. adres powrotu, dzięki czemu po wykonaniu funkcji program "wie", w którym miejscu ma kontynuować działanie. Ponadto, początek funkcji w asemblerze wygląda tak:

 pushl %ebp
 movl %esp, %ebp

Zatem na stosie znajdują się kolejno: zawartość rejestru EBP, adres powrotu a następnie argumenty od pierwszego do n-tego.

Zwracanie wartości

edytuj

Na architekturze i386 do zwracania wyników pracy programu używa się rejestru EAX, bądź jego "mniejszych" odpowiedników, tj. AX i AH/AL. Zatem aby funkcja, napisana w asemblerze zwróciła "1" przed rozkazem ret należy napisać:

 movl $1, %eax

Nazewnictwo

edytuj

Kompilatory języka C/C++ dodają podkreślnik "_" na początku każdej nazwy. Dla przykładu funkcja:

void funkcja();

W pliku wyjściowym będzie posiadać nazwę _funkcja. Dlatego, aby korzystać z poziomu języka C z funkcji zakodowanych w asemblerze, muszą one mieć przy definicji w pliku asemblera wspomniany dodatkowy podkreślnik na początku.

Łączymy wszystko w całość

edytuj

Pora, abyśmy napisali jakąś funkcję, która pobierze argumenty i zwróci jakiś konkretny wynik. Oto kod f1.S:

 .text
 .globl _funkcja
 _funkcja:
   pushl %ebp
   movl %esp, %ebp
   movl 8(%esp), %eax /* kopiujemy pierwszy argument do %eax */
   addl 12(%esp), %eax /* do pierwszego argumentu w %eax dodajemy drugi argument */
   popl %ebp
   ret /* ... i zwracamy wynik dodawania... */

oraz f2.c:

 #include <stdio.h>
 extern int funkcja (int a, int b);
 int main ()
 {
 printf ("2+3=%d\n", funkcja(2,3));
 return 0;
 }

Po skompilowaniu i uruchomieniu programu powinniśmy otrzymać wydruk: 2+3=5

Wstawki asemblerowe

edytuj

Oprócz możliwości wstępnie skompilowanych modułów możesz posłużyć się także tzw. wstawkami asemblerowymi. Ich użycie powoduje wstawienie w miejsce wystąpienia wstawki odpowiedniego kodu maszynowego, który powstanie po przetłumaczeniu kodu asemblerowego. Ponieważ jednak wstawki asemblerowe nie są standardowym elementem języka C, każdy kompilator ma całkowicie odmienną filozofię ich stosowania (lub nie ma ich w ogóle). Ponieważ w tym podręczniku używamy głównie kompilatora GNU, więc w tym rozdziale zostanie omówiona filozofia stosowania wstawek asemblera według programistów GNU.

Ze wstawek asemblerowych korzysta się tak:

 int main ()
 {
   asm ("nop");
 }

W tym wypadku wstawiona zostanie instrukcja "nop" (no operation), która tak naprawdę służy tylko i wyłącznie do konstruowania pętli opóźniających.

Język C++ z racji swojego podobieństwa do C będzie wyjątkowo łatwy do łączenia. Pewnym utrudnieniem może być obiektowość języka C++ oraz występowanie w nim przestrzeni nazw oraz możliwość przeciążania funkcji. Oczywiście nadal zakładamy, że główny program piszemy w C, natomiast korzystamy tylko z pojedynczych funkcji, napisanych w C++. Ponieważ język C nie oferuje tego wszystkiego, co daje programiście język C++, to musimy "zmusić" C++ do wyłączenia pewnych swoich możliwości, aby można było połączyć ze sobą elementy programu, napisane w dwóch różnych językach. Używa się do tego następującej konstrukcji:

 extern "C" {
 /* funkcje, zmienne i wszystko to, co będziemy łączyć z programem w C */
 }

W zrozumieniu teorii pomoże Ci prosty przykład: plik f1.c:

 #include <stdio.h>
 extern int f2(int a);
 
 int main ()
 {
   printf ("%d\n", f2(2));
   return 0;
 }

oraz plik f2.cpp:

 #include <iostream>
 using namespace std;
 extern "C" {
   int f2 (int a)
   {
     cout << "a=" << a << endl;
     return a*2;
   }
 }

Teraz oba pliki kompilujemy:

gcc f1.c -c -o f1.o
g++ f2.cpp -c -o f2.o

Przy łączeniu obu tych plików musimy pamiętać, że język C++ także korzysta ze swojej biblioteki. Zatem poprawna postać polecenia kompilacji powinna wyglądać:

gcc f1.o f2.o -o program -lstdc++

(stdc++ - biblioteka standardowa języka C++). Bardzo istotne jest tutaj to, abyśmy zawsze pamiętali o extern "C", gdyż w przeciwnym razie funkcje napisane w C++ będą dla programu w C całkowicie niewidoczne.

Gnuplot

edytuj



Przypisy