Accessing .NET via the FFI

Access to .NET functionality is provided via Haskell's FFI declarations. For example, to use the .NET Framework's static method for looking up environment variables, you'd write the following FFI declaration:

module Env where
import Dotnet

foreign import dotnet
  "static System.Environment.GetEnvironmentVariable"
  getEnv :: String -> IO String

It declares the IO action getEnv which given the name of an environment variable will look it up by calling System.Environment's static method. The result is whatever the variable maps to, or the empty string if the variable isn't present in the environment block.

getEnv is bound to its .NET method via a specification string. The qualifier, static, indicates that we're binding to a static method; the second part gives the fully qualified name of the static method we're binding to. To tell the FFI implementation that we're accessing .NET code, the dotnet calling convention qualifier is used (and required.)

Along with the specification string goes the Haskell type given to the method. Clearly, this type has to be compatible with the corresponding .NET type. If not, method invocation will fail and a Haskell IO exception is raised.

Binding isn't limited to methods; to access a field you'd write the following:

module Math where
import Dotnet

foreign import dotnet
  "static field System.Math.PI"
  piVal :: Double

which binds piVal to the value of the static field System.Math.PI -- making the (quite reasonable!) assumption that the value is indeed constant. If it hadn't been constant, giving piVal the type (IO Double) would have been more appropriate.

The above FFI declaration uses the field qualifier in its specification string to indicate that we're binding to a field and not a method. The Hugs98.net implementation looks at the result type of a declaration using field to determine whether the field access is a read or write operation. If the result is of the form (IO ()) (or ()piVal above.) For instance, to provide a way to update the PI field:

foreign import dotnet
  "static field System.Math.PI"
  setPiVal :: Double -> IO ()

This will actually fail should you attempt to use it, as the field is read-only, but demonstrates how to use an FFI declaration to provide field update functions.

Creating and representing objects

Object constructors are accessed along similar lines:

foreign import dotnet
  "ctor System.Object"
  newObject :: IO (Object a)

The ctor in the specification string signals that we're binding to a constructor; in this case the default constructor for System.Object. Object references are represented using the type Object. It is parameterised over the type of the class the reference is an instance of. This typed representation of object references lets us use the standard 'trick' of encoding single-inheritance object type hierarchies:

-- System.Xml.XmlNode
data XmlNode_ a
type XmlNode a = Object (XmlNode_ a)

-- System.Xml.XmlDocument; sub-class of XmlNode
data XmlDocument_ a
type XmlDocument a = XmlNode (XmlDocument_ a)

-- System.Xml.XmlDataDocument; sub-class of XmlDocument
data XmlDataDocument_ a
type XmlDataDocument a = XmlDocument (XmlDataDocument_ a)

obj1 :: XmlDataDocument () -- a reference to an instance of XmlDataDocument
obj2 :: XmlNode a  -- a reference to an object that's at least an XmlNode.

This nesting of the inheritance chain in the type argument helps preserve type safety in Haskell:

foreign import dotnet
  "method System.Xml.XmlDocument.Load"
  load :: String{-URL-} -> XmlDocument a -> IO ()

The this pointer is here constrained to be (Object (XmlNode (XmlDocument_ a)), i.e., it needs to at least implement System.Xml.XmlDocument, which precisely captures the type constraint on the method.

Clearly this is all dependent on the user being precise when encoding the class hierarchies using types like in the above example. To help writing out FFI declarations and give these appropriate Haskell types, the hugs98.net distribution comes with a tool, hswrapgen, for generating FFI declarations and object types for a .NET class (see dotnet/tools in the distribution.)

To return to the above FFI declaration, it also demonstrates how to bind to non-static methods. Apart from using the method qualifier in the specification string, method bindings take the 'this' pointer as their last argument. This is so that you can write OO-looking code using the (#) from the Dotnet library:

createDoc url = do
  d <- newDoc 
  d # load url
  return d

So given the above declaration along with:

foreign import dotnet
  "ctor System.Xml.XmlDocument"
  newDoc :: IO (XmlDocument ())

you're all set to manipulate XML documents. Notice the type argument to XmlDocument for the constructor, (), capturing the fact that the constructor is returning a specific instance.

One final word on constructors; binding to parameterised constructors is straightforward also:

foreign import dotnet
  "ctor System.Drawing.Icon"
  newIcon :: String -> IO (Icon ()) 
     -- assuming you've defined the Icon object type.

FFI summary

The .NET foreign import declarations have the following form:

ffidecl : ...
        | 'foreign' 'import' 'dotnet'
	      "spec-string" varName '::' ffiType

spec-string : ('static')? 
              ('field'|'ctor'|'method')?
	      ('[' assemblyName ']')?
	      .NETName

ffiType : PrimType -> ffiType
        | IO PrimType
	| PrimType

PrimType = standard FFI types + Object a + String

i.e., a standard foreign import in Haskell, but with a specification string that lets you unambiguously declare what .NET entity that's being imported / bound to. The first two (optional) entries in the specification string qualifies what kind of entity we're binding to. If they're both omitted, this is equivalent to using method.

By default, the hugs98.net implementation will consult the .NET 'standard' assemblies (i.e., assemblies installed in the same directory as your copy of mscorlib.dll) when attempting to bind to the .NET entity. If its class is part of some other assembly, you may prefix the fully-qualified class name with the name of the assembly,

foreign import dotnet
  "static [foo.dll]Foo.bar"
  bar :: Int -> IO Int

Assuming foo.dll can be located by the .NET run-time via its assembly search path, foo.dll:Foo.bar can now be accessed.

The range of types supported as arguments to and results of foreign imported .NET entities are those of the Haskell FFI, but extended with support for both passing and returning .NET object references (Object) together with Haskell strings.


<sof@galois.com>
Last modified: Wed Mar 12 07:51:36 Pacific Standard Time 2003