czyli nie używać starożytengo już Fortrana.
Kilka słów o języku FORTRAN.
Dawno dawno temu w tej galaktyce...
gdy nie było C++, bardzo szeroko używano języka FORTRAN.Choć C++ pojawił się w latach 80. to należy wspomnieć, że nie od razu przedarł się do świata fizyki. Poprzednik ROOT'a - PAW (Physics Analysis Workstation) zaczął być pisany w 1986 a więc już po pojawieniu się C++. Do dziś wiele kodów napisanych jest w Fortranie - jak chociażby Geant3 czy UrQMD. O ile kompletny program w Fortranie nie stanowi problemu o ile czasem potrzebujemy jedynie jakiegoś fragmentu kodu napisanego w tym języku. W takim przypadku można sobie napisać interefejs który pozwala używać kodu fortranowego z poziomu C++.
Pierwsze co jest ważne w łączeniu kodu to COMMON-y. COMMON jak sama nazwa wskazuje ma coś wspólnego z komuną:). A jeśli komuna to coś musi być wspólne, i tutaj faktycznie tak jest - COMMON to odpowiednik zmiennych globalnych - nie przypisanych do konkretnego fragmentu kodu a do całego programu. Oprócz COMMONów istnieją również zmienne lokalne jak w C++.
Zacznijmy od przykładowego fortranowego kodu. Zawiera on plik heron.f i fortran.f. Pierwszy to główny program, drugi zaś to plik zawierający procedury i funkcje. Kod heron.f wygląda następująco:
PROGRAM SUPER COMMON/KOMON/TABLE(2,2) REAL WYNIK TABLE(1,1) = 1 TABLE(1,2) = 2 TABLE(2,1) = 0 TABLE(2,2) = 1 WYNIK = MATRIX WRITE(*,*) WYNIK CALL PRINTUJ CALL MULTIPLY(2.0) CALL PRINTUJ RETURN END
Program ten wypełnia tablicę COMMON następnie zaś przy pomocy funkcji MATRIX oblicza jej wyznacznik. Funkcją PRINTUJ wyświetla macierz a funkcją MULTIPLY mnoży macierz przez skalar. Plik fortran.f wygląda następująco:
FUNCTION MATRIX () COMMON/KOMON/TABLE(2,2) MATRIX = TABLE(1,1) *TABLE(2,2)-TABLE(1,2)*TABLE(2,1) return END SUBROUTINE MULTIPLY(Z) COMMON/KOMON/TABLE(2,2) INTEGER I,J DO I =0, 2 DO J = 0, 2 TABLE(I,J)=TABLE(I,J)*Z END DO END DO RETURN END SUBROUTINE PRINTUJ COMMON/KOMON/TABLE(2,2) INTEGER I,J DO I =1, 2 WRITE(*,*), TABLE(I,1) ," ", TABLE(I,2) END DO RETURN END
Aby skompilować taki plik możemy użyć Makefile postaci:
LD = gfortran FF = gfortran #FORTRAN Files FSOURCES= heron.f fortran.f FOBJECTS=$(FSOURCES:.f=.o) EXECUTABLE=test.exe all: $(EXECUTABLE) $(EXECUTABLE): $(FOBJECTS) $(LD) -o $@ $^ $(LDOPTS) #FORTRAN files .f.o: $(FF) -O3 -lgfortran -o $@ $^ -c $(COPTS) clean:; @rm -f $(OBJECTS) $(EXECUTABLE) *.o *.d
Wszystko jest fajnie dopóki chcemy taki kod używać jako kompletna aplikacja, jeśli jednak chcemy go użyć z poziomu C++ musimy nauczyć się jak łączyć C++ z fortranem. Generalnie idea jest bardzo prosta musimy napisać funkcje w C++ które pozwolą wywołać funkcje z kodu fortranowego.
Reguły łączenia kodów C++ i Fortran
#Zasada 1 - dodawanie funkcji
Da się je przedstawić w kilku liniach:
#define funkcja F77_NAME(funkcja, FUNKCJA) extern "C" typ type_of_call F77_NAME(funkcja, FUNKCJA)(argumenty)
To co widzimy powyżej to szablon, który kopiujemy i zamieniamy pewne słowa tj.
- typ zamieniamy na typ który zwraca funkcja (np. void czy int)
- funkcja to nazwa funkcji fortranowej którą chcemy użyć (z małych liter)
- FUNKCJA to nazwa funkcji fortranowej którą chcemy użyć ale z dużych liter
- argumenty - to lista argumentów, tutaj uwaga - argumentami są stałe referencje (np. const float &x) a nie wartości, Fortran generalnie nie przyjmuje wartości jako argumentu funkcji
W Fortranie | W definicji | W kodzie klasy (C++) |
PRINTUJ |
#define printuj F77_NAME(printuj, PRINTUJ) |
printuj_() |
#Zasada 2 - dodawanie COMMONów
Typy są ważne - w języku FORTAN istnieją generalnie odpowiedniki typów C++, ale nie są one identycznie nazwane. Ponadto sami musimy znać te typy, aby opowiednio zadeklarować funkcje. Poniżej zamieszczam tabelkę tłumaczącą jeden typ na drugi.
Zmienna w C++ | Zmienna fortranowa |
float | REAL |
int | INTEGER |
char | CHAR |
struct{float re; float im;} | DCOMPLEX |
Drobna uwaga - należy zwracać na specyfikację w nazywaniu zmiennych w FOTRANie, zwykle bowiem ich typ nie jest deklarowany jawnie jak w C++, a stosuje się konwencję zgodnie z którą, typ jest zależny od pierwszej litery nazwy zmiennej. Skoro potrafimy już się odwołać do funkcji i zmiennych czas odezwać się do COMMONów:
extern struct C_table{ float table[2][2]; } _komon;
Tutaj mówimy kompilatorowi, że chcemy uwspólnić pewien obszar pamięci - dzięki temu możemy z poziomu C++ zapisywać dane do pamięci programu fortanowego i je czytać. Podobnie jak w nazwie funkcji w kodzie C++ do tak zadeklarowanej tablicy odwołujemy się z "podłogą" tzn. fortranowa tablica "KOMON" w C++ staje się "_komon". Tutaj należy również wspomnieć o tym, iż w Fortran tablice są inaczej zorganizowane, tablica Fortranowa 3x4 to w C++ tablica 4x3, dodatkowo pierwszy element tablicy w języku C++ jest numerowany od zera a w FORTAN od 1. Z tych powodów można będzie potem zobaczyć, że macierz będzie przez to obrócona. Ważną uwagą jest też stosowanie tych samych nazw w Fortan i C++ dla COMMONów, inaczej kompilator nie będzie wiedział co z czym połączyć.
Piszemy kod w C++
Znając reguły łączenia kodów możemy wreszcie napisać nasz kod. W kodzie pliku nagłówkowego zamieściłem wszystkie łączenia C++ z Fortran, ale możliwe jest także zamieszczenie tych kodów w pliku z kodem źródłowym (cpp, cxx).
#ifndef CALL_H #define CALL_H #include #ifdef CERNLIB_QXCAPT # define F77_NAME(name,NAME) NAME #else # if defined(CERNLIB_QXNO_SC) # define F77_NAME(name,NAME) name # else # define F77_NAME(name,NAME) name##_ # endif #endif #ifndef type_of_call # define type_of_call #endif #define matrix F77_NAME(matrix,MATRIX) extern "C" float type_of_call F77_NAME(matrix,MATRIX)(); #define multiply F77_NAME(multiply,MULTIPLY) extern "C" void type_of_call F77_NAME(multiply,MULTIPLY)(const float &mult); #define printuj F77_NAME(printuj, PRINTUJ) extern "C" void type_of_call F77_NAME(printuj, PRINTUJ)(); #define _komon F77_NAME(komon,KOMON) extern struct C_table{ float table[2][2]; } _komon; class Call{ public: Call(float **Table); void Mnoz(float val); void Printuj(); float Wyznacznik(); virtual ~Call(){} ClassDef(Call,1) };
Kod pliku Call.cpp.
#include "Call.h" Call::Call(float **Table){ for(int i=0;i<2;i++){ for(int j=0;j<2;j++){ _komon.table[i][j]=Table[i][j]; } } } void Call::Mnoz(float val){ multiply_(val); } void Call::Printuj(){ printuj_(); } float Call::Wyznacznik(){ return matrix_(); }
Kod głownego programu w C++ którego będziemy używać zaś wygląda następująco:
#include ; #include "Call.h" using namespace std; int main(){ float **tablica; tablica = new float*[2]; tablica[0] = new float[2]; tablica[1] = new float[2]; tablica[0][0]=1.0; tablica[1][0]=2.0; tablica[0][1]=1.0; tablica[1][1]=2.0; Call *dzwonek = new Call(tablica); dzwonek->Printuj(); for(int i =0;i<2;i++){ cout<<tablica[i][0]<<" "<<tablica[i][1]<<endl; } float pwyz = dzwonek->Wyznacznik(); cout<<pwyz<<endl; dzwonek->Mnoz(3.0); cout<<"Po mnożeniu"<<endl; dzwonek->Printuj(); float wyzn = dzwonek->Wyznacznik(); cout<<wyzn<<endl; return 0; }
Dodatkowo załączam też plik linkdef.h który jest niezbędny do utworzenia słownika:
#ifdef __CINT__ #pragma link off all globals; #pragma link off all classes; #pragma link off all functions; //tutaj idzie lista klas #pragma link C++ class Call+; #endif
Makefile
Makefile z C++ należy zmodyfikować aby współgrał z kodem Fortan. Wynika to z faktu iż kompilujemy kody w dwóch różnych językach, dopiero po "wyprodukowaniu" kodu maszynowego możemy kazać je złączyć w jeden spójny kod.
CC = g++ LD = gfortran FF = gfortran #COPTS = `root-config --cflags` -I/usr/local/root/include -g #LDOPTS = `root-config --libs` -g COPTS = -g -O `root-config --cflags` -Wall LDOPTS = -g -O `root-config --libs` -Wall #C++ Files SOURCES = Main.cpp SOURCES += Call.cpp Dict.cpp OBJECTS = $(SOURCES:.cpp=.o) #Dictionary classes HEADERS = Call.h linkdef.h #FORTRAN Files FSOURCES= fortran.f FOBJECTS=$(FSOURCES:.f=.o) EXECUTABLE=test.exe all: $(EXECUTABLE) $(EXECUTABLE): $(OBJECTS) $(FOBJECTS) $(LD) -o $@ $^ $(LDOPTS) #C++ files .cpp.o: $(CC) -o $@ $^ -c $(COPTS) #FORTRAN files .f.o: $(FF) -O3 -lgfortran -o $@ $^ -c $(COPTS) #Dictionary for ROOT classes Dict.cpp: $(HEADERS) @echo "Generating dictionary ..." @rootcint -f Dict.cpp -c -p -P -I$ROOTSYS -I/usr/local/include $(HEADERS) clean:; @rm -f $(OBJECTS) $(EXECUTABLE) *.o *.d Dict.cpp Dict.h
Załączony powyżej Makefile dodatkowo tworzy słownik ROOTowy i linkuje klasy ROOTowe, nie jest to jednak potrzebne o ile nie używamy ROOTa w naszej aplikacji. Po utworzeniu przy pomocy g++ kodu maszynowego z C++, a przy pomody gfortran kodu maszynowego z FORTRAN obie grupy kodów maszynowych są łączone w jeden plik wykonywalny przy pomocy gfortran
Przejdźmy teraz do kodu głównego. W nim użyjemy klasy Call która to właśnie dzwoni pod FORTRAN. Kod główny znajduje się w pliku Main.cpp.