OCaml: kompilatorowy zawrót głowy

W językach, których implementacje wyposażone są w tryb interaktywny (REPL), zazwyczaj podstawowe funkcjonalności danej technologii poznaje się właśnie w tym środowisku. Jest to po prostu najszybszy i najłatwiejszy sposób kontaktu z nowym językiem. Nie inaczej jest z OCamlem – odpalamy utop i już jesteśmy gotowi zgłębiać tajniki programowania funkcyjnego. Jednak w miarę jak pisane skrypty stają się coraz bardziej rozbudowane, przychodzi potrzeba porzucenia trybu interaktywnego na rzecz programów zapisywanych w plikach. Wrzucamy zatem jakiś przykładowy kod do pliku hello.ml i… rozpoczynamy poszukiwanie odpowiedzi na pytanie którego narzędzia użyć do skompilowania naszego programu. Wybór jest bowiem naprawdę spory: ocamlc, ocamlopt. ocamlc.opt, ocamlopt.opt, ocamlfind, ocamlbuild, ocamlrun. Spory jest więc również dylemat, przed którym można w tej sytuacji stanąć.

Bajtowo lub natywnie

Podstawowe rozróżnienie, które można zastosować wobec dostępnych narzędzi to docelowy format plików wyjściowych – źródła napisanych w OCamlu programów można kompilować albo do kodu bajtowego (który musi być później interpretowany przez kolejne narzędzie), albo do kodu natywnego (czyli postaci, która może zostać uruchomiona bezpośrednio przez system operacyjny):

  • do kodu bajtowego kompiluje ocamlc i ocamlc.opt
  • do kodu natywnego kompiluje ocamloptocamlopt.opt

Program skompilowany do postaci natywnej wykonuje się zazwyczaj szybciej, jeśli więc zależy nam na wydajności, powinniśmy wybrać właśnie tę opcję. Na tej zasadzie opiera się też różnica między narzędziami ocamlc ocamlc.opt – funkcjonalnie są to te same kompilatory, ale ocamlc jest kompilatorem skompilowanym do kodu bajtowego, a ocamlc.opt kompilatorem z wersji natywnej (więc będzie kompilował szybciej). Sufiks opt oznacza tu oczywiście wersję zoptymalizowaną (ang. optimized). Analogicznie sytuacja ma się z kompilującym do kodu natywnego programem ocamlopt oraz ocamlopt.opt. Jeśli chcecie samodzielnie zweryfikować prawdziwość moich słów możecie wpisać w konsoli:

ocamlrun to interpreter kodu bajtowego OCamla i, jak widać, potrafi on uruchomić kompilatory ocamlc oraz ocamlopt, ale z ich wersjami natywnymi ma już problem.

Nasza pierwsza kompilacja

Przejdźmy więc do praktyki i sprawdźmy czy pokrywa się ona z piękną teorią. Dla ułatwienia niechaj treścią pliku z kodem źródłowym będzie najprostsze co być może:

Oto jak możemy teraz postąpić z zapisanym programem:

Rezultatem dwóch pierwszych poleceń będzie utworzenie pliku wykonywalnego a.out, zaś ostatnia komenda skompiluje i uruchomi program, nie pozostawiając po sobie żadnych śladów. Proste? Proste. Jak się jednak okazuje, kompilatory OCamla generują również kilka innych plików – nie są one wprawdzie potrzebne do działania skompilowanej aplikacji, ale warto wiedzieć czym w ogóle są.

Plik Opis
.ml Kod źródłowy
.mli Specyfikacja interfejsu
.cmi Skompilowany (z pliku .mli lub .ml jeśli brak tego pierwszego) interfejs
.cmo Plik obiektowy (bajtowy)
.o Plik obiektowy (natywny)
.cmx Zawiera dodatkowe informacje dot. natywnego pliku obiektowego
.cma Biblioteka kodu bajtowego (zbiór plików .cmo)
.a Biblioteka natywna (zbiór plików .o)
.cmxa Informacje związane z biblioteką natywną (zbiór plików .cmx)

Informacje te mogą przydać się zwłaszcza wtedy, kiedy planujemy korzystać z dodatkowych bibliotek, bądź sami chcemy w tej formie dystrybuować nasz kod.

I pierwsze problemy

Wszystko idzie gładko do momentu, w którym nie postanowimy skorzystać z zewnętrznych modułów, takich jak chociażby popularny Core (tworzony przez Jane Street). W ramach przykładu skomplikujmy odrobinę nasz kod, aby korzystał on z funkcji pochodzącej z modułu Core.Std:

Kiedy spróbujemy ów kod skompilować, pojawi się prosty do przewidzenia błąd:

Oczywiście – trzeba wszak poinformować kompilator o lokalizacji dodatkowej biblioteki. Dla ułatwienia dokonamy stworzenia pliku wykonywalnego w dwóch etapach – najpierw tylko skompilujemy kod źródłowy (odpowiada za to flaga -c), a dopiero później, za pomocą osobnej komendy, zlinkujemy go. Metodą prób i błędów (gdyż okazuje się, że zaincludowanie samego tylko Core nie wystarczy) dochodzimy do skonstruowania następującego polecenia:

Hurra, udało się! Kompilator wygenerował plik obiektowy (hello.cmo) oraz skompilowany interfejs (hello.cmi). Teraz czas na fazę ostateczną, czyli linkowanie. Tutaj niestety sprawa znacznie się komplikuje, bo liczba bibliotek, które musimy wskazać robi się naprawdę spora. Moduł Core wymaga Core_kernel, z kolei Core_kernel korzysta z Variantslib i tak dalej, i tak dalej. W rezultacie aby dokończyć dzieła musimy wpisać:

Wychodzi na to, że polecenie kompilacji jest dłuższe niż sam kod źródłowy, co zakrawa już na pewien absurd. Na szczęście nie tędy droga i aby skompilować OCamlowy program nie musimy wcale poświęcać czasu na ręczne znajdowanie zależności.

Niezastąpiony ocamlfind

Narzędziem, które wybawia nas z tarapatów jest ocamlfind. Chcąc skompilować program do kodu bajtowego wystarczy że wpiszemy:

Bibliotekę thread musimy zalinkować, ponieważ wymaga tego Core , ale to wszystko, nie musimy przejmować się żadnymi innymi pośrednimi zależnościami. Chcąc zamiast ocamlc użyć ocamlopt wystarczy natomiast że wywołamy polecenie:

A jak wywołać kompilację przy pomocy np. ocamlopt.opt? Wystarczy zedytować plik ~/.opam/system/lib/findlib.conf  (choć prawdopodobnie już domyślnie ustawione jest, że wywołanie ocamlfind ocamlopt... użyje narzędzia ocamlopt.opt).

Do większych projektów – ocamlbuild

Opisane w poprzednich akapitach metody kompilacji są dobre, ale ich stosowanie w przypadku projektu składającego się z wielu plików z kodem źródłowym byłoby nieporozumieniem. Pracując nad tego typu projektem zazwyczaj nie chcemy kompilować za każdym razem całego kodu, a jedynie te fragmenty, które uległy zmianie od ostatniej kompilacji. Dobrze byłoby też mieć możliwość budowania różnych targetów (np. i szybkiego puszczania testów jednostkowych), a do tego wszystkiego na pewno wygodniej byłoby posługiwać się jakimś mniej skomplikowanym poleceniem. Doskonałym narzędziem, które ułatwia budowanie projektów jest oczywiście Makefile.

A jak to się ma do OCamla? Otóż jednym z programów, które łatwo można zaprząc do współpracy z Makefile’ami jest ocamlbuild. Odpowiednikami wcześniej używanych komend ocamlfind ocamlopt oraz ocamlfind ocamlc są:

Funkcjonalności ocamlbuilda jest naprawdę sporo i znajdując się na etapie, na którym zachodzi konieczność skorzystania z niego, najlepiej jest po prostu sięgnąć do dokumentacji [5], która jasno tłumaczy możliwe opcje i obrazuje je licznymi przykładami.

Źródła

  1. OCaml Manual: Batch compilation (ocamlc)
  2. OCaml Manual: Native-code compilation (ocamlopt)
  3. Developing Applications With OCaml: Compilation
  4. Compiling OCaml projects
  5. The Ocamlbuild manual