RL-Engine for CLR hosted languages – ClojureCLR

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:

CLR Repl
CLR 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:

RL Engine w kilku bibliotekach
RL Engine w kilku bibliotekach

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:

Winforms dungeon
Winforms dungeon
Dungeon
Dungeon
Dungeon
Dungeon

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

A penny for your thoughts