Kilka postów temu wspominałem, że clojure jest łatwo portowalny.
Spróbujmy zatem zportować RL-Engine do środowiska .NET.
W teorii wystarczy tylko wziąć instniejący kod clojure i skompilować go za pomocą ClojureCLR.
Uwierzmy w magię i spróbujmy.
ClojureCLR
ClojureCLR jest to natywna implementacja Clojure dla CLR.
Umożliwia kompilację kodu w clojure do bibliotek (dll) i plików wykonywalnych (exe) obsługiwanych przez .NET framework bądź mono.
Obecnie dostępną wersją jest 1.7 – jeden numerek do tyłu w porównaniu z implementacją originalną (1.8).
Kompilacja do ClojureCLR
Zasysamy sobie ClojureCLR stąd.
Wybieramy sobie wersję:
- Debug 4.0
- Release 4.0
- Debug 3.5
- Release 3.5
Po wypakowaniu możemy odpalić Clojure.Main.exe i naszym oczom powinien pokazać się REPL:
Zakładając, że mamy już leiningena i trochę kodu w clojure, możemy przystąpić do instalacji lein-clr.
lein-clr
Lein-clr jest pluginem do leiningena który upraszcza kompilację do CLR.
Można go konfigurować na kilka sposobów – spójrzmy na ten bardziej skomplikowany.
Dodajemy zmienne środowiskowe:
CLOJURE_CLR = <ścieżka_do_clojureCLR> CLOJURE_LOAD_PATH = %CLOJURE_CLR% Path = %CLOJURE_CLR%
CLOJURE_CLR może mieć dowolną nazwę np. CLOJURE_CLR_DEBUG_3.5 itp.
Teraz kilka zmian w pliku naszego projektu (project.clj):
:warn-on-reflection true :min-lein-version "2.0.0" :plugins [[lein-clr "0.2.2"]] :clr {:cmd-templates {:clj-exe [[?PATH "mono"] [CLOJURE_CLR %1]] :clj-dep [[?PATH "mono"] ["target/clr/clj/Debug 3.5" %1]] :clj-url "https://sourceforge.net/projects/clojureclr/files/clojure-clr-1.7.0-Debug-3.5.zip/download" :clj-zip "clojure-clr-1.7.0-Debug-3.5.zip" :curl ["curl" "--insecure" "-f" "-L" "-o" %1 %2] :nuget-ver [[?PATH "mono"] [*PATH "nuget.exe"] "install" %1 "-Version" %2] :nuget-any [[?PATH "mono"] [*PATH "nuget.exe"] "install" %1] :unzip ["unzip" "-d" %1 %2] :wget ["wget" "--no-check-certificate" "--no-clobber" "-O" %1 %2]} :main-cmd [:clj-exe "Clojure.Main.exe"] :compile-cmd [:clj-exe "Clojure.Compile.exe"]})
U mnie klucze :clj-dep, :clj-url, :clj-zip mają wartości ustawione na wersję Debug 3.5.
Kompilacja
Kompilujemy nasz kod wywołaniem komendy “lein clr compile rl-engine.dungeon-generator” (na razie ograniczymy się tylko do generatorów lochów).
Co ciekawe – działa!
I to za pierwszym razem (nie licząc wywołań compile bez argumentów :P).
Popatrzmy co nam to wygenerowało:
Hmmm, czyli generuje się dll per plik. Trochę to niewygodne – ale z tym sobie poradzimy innym razem.
ClojureCLR w C#
Tworzymy sobie prostą aplikację konsolową, dodajemy referencje do naszego zestawu dllek oraz instalujemy NuGet package z ClojureCLR.
class Program { static void Main(string[] args) { IFn require = Clojure.var("clojure.core", "require"); require.invoke(Clojure.read("rl-engine.dungeon-generator")); var genratorFactory = Clojure.var("rl-engine.dungeon-generator", "get-generator"); var generator = genratorFactory.invoke("bsp") as IFn; var map = generator.invoke(5, 5); Console.WriteLine(map); Console.Read(); } }
Po odpaleniu powinniśmy dostać:
[[1 1 1 1 1] [1 0 0 0 1] [1 0 0 0 1] [1 1 1 1 1]]
Wow! Kolejny punkt dla ClojureCLR – interop działa bez problemów.
Jedyna rzecz jaką trzeba pamiętać to o załadowaniu naszej biblioteki do runtime’u clojure’a:
RT.load("rl_engine.dungeon_generator");
RL-Engine + WinForms
Teraz pobawimy się czymś bardziej interesującym.
Wygenerujemy sobie poziom w clojure a renderować będziemy go w WinFormsach.
Najpierw zrefaktoryzujemy nasza aplikację konsolową aby wyglądała bardziej jak biblioteka opakowująca clojure:
namespace RLEngine { public static class DungeonFactory { static DungeonFactory() { RT.load("rl_engine.dungeon_generator"); } public static int[,] GenerateDungeon(int height, int width) { return To2D(GenerateDungeonFromClojure(height, width)); } private static int[][] GenerateDungeonFromClojure(int height, int width) { var genratorFactory = Clojure.var("rl-engine.dungeon-generator", "get-generator"); var generator = genratorFactory.invoke("bsp") as IFn; var map = generator.invoke(height, width); var arrayConverter = Clojure.var("clojure.core", "into-array"); var vectors = arrayConverter.invoke(map) as PersistentVector[]; var array = vectors.Select(x => x.Cast().Select(Convert.ToInt32).ToArray()).ToArray(); return array; } private static T[,] To2D<T>(T[][] source) { ... } } }
A teraz wykorzystamy ją w projekcie WinForms:
namespace WinFormsDungeon { public partial class Form1 : Form { private readonly Graphics _graphics; private readonly Brush _blackBrush; private readonly Brush _whiteBrush; private int _size = 30; public Form1() { InitializeComponent(); _graphics = this.pictureBox1.CreateGraphics(); _blackBrush = new SolidBrush(Color.Black); _whiteBrush = new SolidBrush(Color.Bisque); } private void pictureBox1_Click(object sender, EventArgs e) { var dungeon = RLEngine.DungeonFactory.GenerateDungeon(20, 20); for (int height = 0; height < dungeon.GetLength(0); height++) { for (int width = 0; width < dungeon.GetLength(1); width++) { var pen = dungeon[height, width] == 1 ? _blackBrush : _whiteBrush; _graphics.FillRectangle(pen, height * _size, width * _size, 30, 30); } } } } }
A oto i efekty:
Konkluzja
Naprawdę jestem bardzo pozytywnie zaskoczony, że wszystko tak po prostu działa.
Żadnych dziwnych pierdół czy problemów konfiguracyjnych.
Ludzie od ClojureCLR zrobili kawał porządnej roboty – tylko tak dalej!
Be First to Comment