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.