Connecting Haskell FRP to the Unity3D game framework

This blog post was originally written back in 2011-04-02

_images/Unity-DllTest.png

Abstract

This post describes how to connect Haskell to Unity3D by compiling a Haskell shared library to a Win32 DLL and dynamically loading the DLL in a scripted Unity3D object via DLLImport. The DLL also uses an additional module (Yampa), thus FRP can be used within a sophisticated game framework (with the exception that this concept doesn’t connecte to the Unity3D core yet, which would allow stateful arrows within the game logic… which renders it rather useless right now :/). Unity3D was chosen because it provides a complete game creation workflow and a demo version is available for free. In contrast, there are no full game creation frameworks available for Haskell right now (to my knowledge, but my status is based on ). DLLs were chosen over .NET libraries because to my knowledge it is not (yet) possible in Haskell to compile to a .NET library (and I think it was one of the reasons why F# was born). .NET libraries would of course allow for an easier integration in Unity3D. Unity plugins are only available in the pro version… which is a pity. My personal goal was to connect functional temporal programming (functional reactive programming) to a sophisticated game framework to allow the creation of great games with FRP game logic. Besides, it was a great exercise for foreign function interfaces (FFI) and a test on how well Haskell can be integrated into existing applications.

Instructions

What actually happens when a Haskell program is packed into a shared library and called from an external program? The actual DLL is built from C code, which exports the Haskell functions and also starts up a Haskell runtime, which delegates the external function calls to Haskell. All functions are called within an unsafe environment, like main :: IO () (… this shouldn’t necessarily be the case I think – FFI to Miranda?)

  1. Install at least GHC 7, because Windows DLLs don’t work with GHC 6 (see: GHC 7 change-log, section 1.5.4. DLLs: “Shared libraries are once again supported on Windows.”).

  2. Download and install Unity3D.

  3. Download DllTest.hs (Haskell code for game logic) and adopt to your needs.

  • As these function get evaluated within a unsafe environment, they are all IO Monads.

  • In Windows you have to use ccall (citation needed)

  1. Download DllTest.cs (Unity3D script file) and adopt to your needs.

  • Make sure to start and stop the Haskell runtime correctly. Nothing special really, just tag the external functions with [DllImport] and call.

  1. Visit GHC manual and search for // StartEnd.c and save the code to StartEnd.c.

  • This is the actual C program which is converted to a Windows DLL. It also starts up a Haskell runtime and allows the external program to call functions.

  1. Install all dependent modules with shared library option enabled (f.e. yampa)

>>> cabal install --reinstall --enable-shared yampa.
  1. Copy shared libraries of all used modules to your Haskell project directory C:/Program Files (x86)/Haskell Platform/2011.2.0.0/lib/yampa.../libHSYampa-0.9.2.3-ghc7.0.2.dll.a (…there may be a better solution for this)

  2. Copy the file C:/Program Files (x86)/Internet Explorer/IEShims.dll to your Haskell project directory (… I forgot why)

  3. Compile step 1

>>> ghc -dynamic -fPIC -c DllTest.hs
  • This is an intermediate step (see Shared libraries that export a C API) which produces the object file (.o) from C code (DllTest.o and DllTest_stub.o)

  • The -fPIC flag is required for all code that will end up in a shared library.

  1. Compile step 2

>>> ghc -dynamic -shared DllTest.o DllTest_stub.o StartEnd.o libHSYampa-0.9.2.3-ghc7.0.2.dll.a -o libHsDllTest.dll
  • The object files, the runtime starter and the archive file for the additional module are compiled together into the final DLL.

  1. Use the DependencyWalker tool of the Visual Studio IDE to find errors in the DLL (it may be included in Visual Studio Express or Visual Studio Code which is available for free).

  2. Create a Unity3D project and add an object using the DllTest.cs as a script file (this is out-of-scope of this article).

Notes on Haskell DLLs

  • If you didn’t work with shared libraries before, make sure you understand the Foreign function interface (FFI) and all the different types of shared libraries. What’s used here is a dynamically loaded shared library (DLL) which gets loaded by an external (non-Haskell) programm (Unity3D).

  • Read GHC manual on Building and using Win32 DLLs (Question: What is the option -lfooble good for?).

  • All modules used by the Haskell program have to be compiled into one-big DLL. GHC 7 manual on Win32 DLLs: “Adder is the name of the root module in the module tree (as mentioned above, there must be a single root module, and hence a single module tree in the DLL”.

  • Make sure shared-library versions are installed of all used modules (see above). They get installed somewhere into C:/Program Files (x86)/Haskell Platform/2011.2.0.0/lib…

  • The GHC 7 documentation is outdated (see: GHC Ticket #4825: DLLs not documented consistently).

Notes on Unity3D

  • When the DLL file is recompiled, Unity3D has to be restarted in order to reload the DLL.

  • If Unity3D doesn’t find the DLL file, make sure the correct DLL file (in the directory where ghc is executed) is located in your Unity3D project where the .exe file is located which gets compiled by Unity3D.

  • Unity3D log files are located in: C:/Users/myuser/AppData/Local/Unity.

  • Unity3D crash reports are located in: C:/Users/gerold/AppData/Local/Temp/crash_...

  • stdcall instead of ccall on Windows, this appears to be invalid for Unity3D (also see GHC manual on Win32 DLLs.

  • Unity3D uses the following script languages: C#, JavaScript, Boo.

Description

The actual concept is pretty straight forward. Simply write all Haskell functions and make the function interfacing to the external program an IO monad, which gets called by a potentially inpure program. The program and all depending modules are compiled into one big DLL. This DLL gets imported by the external program (Unity3D) and makes the functions visible. Before an Haskell function can be called, a complete Haskell runtime environment has to be started. In the following example, when an object is created, it starts up a Haskell runtime, runs an embedded arrow and shuts the Haskell runtime down again. This is of course rather useless right now, but it shows an arrow running in Unity3D.

DllTest.hs (short)

{-# LANGUAGE ForeignFunctionInterface, Arrows #-}

module Adder (
  embeddedSF,
  ...
) where

import FRP.Yampa

embeddedSF :: Double -> Double -> Double -> IO Double
embeddedSF v0 v1 v2 = return . last $ embed integral (v0, [(1.0, Just v1), (1.0, Just v2)])

foreign export ccall embeddedSF :: Double -> Double -> Double -> IO Double

DllTest.cs (short)

class DLLTest : MonoBehaviour {
[DllImport ("libHSDllTest")]
private static extern void HsStart();
[DllImport ("libHSDLLTest")]
private static extern void HsEnd();
[DllImport ("libHSDLLTest")]
private static extern double embeddedSF(double v0, double v1, double v2);

void Awake () {
  HsStart();
  print ("embeddedSF: " + embeddedSF(12.0, 23.0, 45.0));
  HsEnd();
}

Resources

Haskell DLLs

Unity3D

Open questions

I don’t know how to integrate stateful arrows into the game logic yet. Probably the Haskell runtime within the DLL has to be started on some OnGameStart-event. Each object has to run its own independent arrow code and the update ticks propagated to something like the reactimate procedure.