packages feed

clr-host-0.2.0: src/Clr/Host/DotNet.hs

{-# LANGUAGE CPP, LambdaCase #-}

module Clr.Host.DotNet (
  startHostDotNet,
  stopHostDotNet
  ) where

import Data.Word
import Data.Int
import Foreign.Ptr
import Foreign.Marshal
import Foreign.C.String
import Foreign.Storable
import System.Win32
import System.IO
import Control.Exception (bracket)
import Unsafe.Coerce (unsafeCoerce)
import System.IO.Unsafe (unsafePerformIO)
import qualified Data.ByteString as S
import Text.Printf

import Clr.Host.BStr.DotNet
import Clr.Host.BStr.Type
import Clr.Host.DotNet.Common
import Clr.Host.DotNet.Guid
import Clr.Host.DotNet.SafeArray
import Clr.Host.DotNet.Variant
import Clr.Host.Driver

type ICorRuntimeHost = InterfacePtr
type ICLRRuntimeHost = InterfacePtr
type ICLRMetaHost    = InterfacePtr
type ICLRRuntimeInfo = InterfacePtr
type IEnumUnknown    = InterfacePtr

foreign import ccall "dotNetHost.c getICorRuntimeHost" getICorRuntimeHost :: IO ICorRuntimeHost
foreign import ccall "dotNetHost.c getICLRRuntimeHost" getICLRRuntimeHost :: IO ICLRRuntimeHost
foreign import ccall "dotNetHost.c setHostRefs"        setHostRefs        :: ICorRuntimeHost -> ICLRRuntimeHost -> IO ()

#if x86_64_HOST_ARCH
foreign import ccall __unregister_hs_exception_handler :: IO ()
unregister_hs_exception_handler = __unregister_hs_exception_handler
#else
-- symbol has extra leading underscore on 32 bit apprently
--foreign import ccall ___unregister_hs_exception_handler :: IO ()
--unregister_hs_exception_handler = ___unregister_hs_exception_handler

-- Infact just leave as NO-OP until a 32 bit dev want to investigate the further linker issues
unregister_hs_exception_handler :: IO ()
unregister_hs_exception_handler = return ()
#endif

-- | 'start_ICorRuntimeHost' calls the Start method of the given ICorRuntimeHost interface.
start_ICorRuntimeHost :: ICorRuntimeHost -> IO ()
start_ICorRuntimeHost corHost = do
  -- Initialise COM (and the threading model)
  coInitializeEx nullPtr coInit_ApartmentThreaded
  -- TODO: Allow the library user to select their desired threading model
  --       (we use an STA for the time being so we can use GUI libraries).
  f <- getInterfaceFunction 10 makeStartCor corHost
  f corHost >>= checkHR "ICorRuntimeHost.Start"
  return ()

type Start_ICorRuntimeHost = ICorRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeStartCor :: FunPtr Start_ICorRuntimeHost -> Start_ICorRuntimeHost

-- | 'start_ICLRRuntimeHost' calls the Start method of the given ICLRRuntimeHost interface.
start_ICLRRuntimeHost :: ICLRRuntimeHost -> IO ()
start_ICLRRuntimeHost clrHost = do
  f <- getInterfaceFunction 3 makeStartCLR clrHost
  f clrHost >>= checkHR "ICLRRuntimeHost.Start"
  return ()

type Start_ICLRRuntimeHost = ICLRRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeStartCLR :: FunPtr Start_ICLRRuntimeHost -> Start_ICLRRuntimeHost

startHostDotNet :: IO (FunPtr (Ptr Word16 -> IO (FunPtr a)))
startHostDotNet = do
  -- Disable the top level VEH. This can interfere with .NET exceptions.
  unregister_hs_exception_handler
  -- Load the 'mscoree' dynamic library into the process.  This is the
  -- 'stub' library for the .NET execution engine, and is used to load an
  -- appropriate version of the real runtime via a call to
  -- 'CorBindToRuntimeEx'.
  hMscoree <- loadLibrary "mscoree.dll"
  -- Attempt .Net 4.0 binding by first obtaining a pointer to 'CLRCreateInstance'
  -- If this fails, fall back to corBindToRuntimeEx which only allows
  -- binding to MS .Net versions < 4.0
  metaHost <- createMetaHost hMscoree
  if metaHost == nullPtr then do
    corHost <- corBindToRuntimeEx hMscoree
    start_ICorRuntimeHost corHost
    setHostRefs corHost nullPtr
  else getRuntimes_ICLRMetaHost metaHost >>= \case
    []           -> error "No runtime versions found"
    (runtime:xs) -> do
      version <- getVersionString_ICLRRuntimeInfo runtime
      --putStrLn $ "attempting to bind to runtime version " ++ version
      clrHost <- getCLRHost_ICLRRuntimeInfo runtime
      start_ICLRRuntimeHost clrHost
      corHost <- getCorHost_ICLRRuntimeInfo runtime
      setHostRefs corHost clrHost
  corHost <- getICorRuntimeHost
  loadDriverAndBoot corHost

-- | 'stop_ICorRuntimeHost' calls the Stop method of the given ICorRuntimeHost interface.
stop_ICorRuntimeHost :: ICorRuntimeHost -> IO ()
stop_ICorRuntimeHost corHost = do
  f <- getInterfaceFunction 11 makeStop_ICorRuntimeHost corHost
  f corHost >>= checkHR "ICorRuntimeHost.Stop"
  return ()

type Stop_ICorRuntimeHost = ICorRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeStop_ICorRuntimeHost :: FunPtr Stop_ICorRuntimeHost -> Stop_ICorRuntimeHost

-- | 'stop_ICLRRuntimeHost' calls the Stop method of the given ICLRRuntimeHost interface.
stop_ICLRRuntimeHost :: ICLRRuntimeHost -> IO ()
stop_ICLRRuntimeHost clrHost = do
  f <- getInterfaceFunction 4 makeStop_ICLRRuntimeHost clrHost
  f clrHost >>= checkHR "ICLRRuntimeHost.Stop"
  return ()

type Stop_ICLRRuntimeHost = ICLRRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeStop_ICLRRuntimeHost :: FunPtr Stop_ICLRRuntimeHost -> Stop_ICLRRuntimeHost

stopHostDotNet :: IO ()
stopHostDotNet = do
  clrHost <- getICLRRuntimeHost
  corHost <- getICorRuntimeHost
  if clrHost == nullPtr then
    if corHost == nullPtr then
      return ()
    else
      stop_ICorRuntimeHost corHost
  else
    stop_ICLRRuntimeHost clrHost
  return ()

clsid_CorRuntimeHost = Guid 0xCB2F6723 0xAB3A 0x11D2 0x9C 0x40 0x00 0xC0 0x4F 0xA3 0x0A 0x3E
iid_ICorRuntimeHost  = Guid 0xCB2F6722 0xAB3A 0x11D2 0x9C 0x40 0x00 0xC0 0x4F 0xA3 0x0A 0x3E

clsid_CLRRuntimeHost = Guid 0x90F1A06E 0x7712 0x4762 0x86 0xB5 0x7A 0x5E 0xBA 0x6B 0xDB 0x02
iid_ICLRRuntimeHost  = Guid 0x90F1A06C 0x7712 0x4762 0x86 0xB5 0x7A 0x5E 0xBA 0x6B 0xDB 0x02

-- | 'corBindToRunTimeEx' loads the CLR execution engine into the process and returns
--   a COM interface for it.
corBindToRuntimeEx :: HINSTANCE -> IO ICorRuntimeHost
corBindToRuntimeEx hMscoree = do
  -- Obtain a pointer to the 'CorBindToRuntimeEx' function from mscoree.dll
  corBindToRuntimeExAddr <- getProcAddress hMscoree "CorBindToRuntimeEx"
  let corBindToRuntimeEx = makeCorBindToRuntimeEx $ castPtrToFunPtr corBindToRuntimeExAddr

  -- Request the shim (mscoree.dll) to load a version of the runtime into the
  -- process, returning a pointer to an implementation of the ICorRuntimeHost
  -- for controlling the runtime.
  with (nullPtr :: ICorRuntimeHost) $ \clrHostPtr -> do
    -- Call 'corBindToRuntimeEx' to obtain an ICorRuntimeHost
    with clsid_CorRuntimeHost $ \refCLSID_CorRuntimeHost ->
      with iid_ICorRuntimeHost $ \refIID_ICorRuntimeHost ->
        corBindToRuntimeEx nullPtr nullPtr 0 refCLSID_CorRuntimeHost
          refIID_ICorRuntimeHost clrHostPtr >>= checkHR "CorBindToRuntimeEx"
    peek clrHostPtr

type CorBindToRuntimeEx = LPCWSTR -> LPCWSTR -> DWORD -> Ptr CLSID -> Ptr IID -> Ptr ICorRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeCorBindToRuntimeEx :: FunPtr CorBindToRuntimeEx -> CorBindToRuntimeEx

clrCreateInstance :: HINSTANCE -> CLSID -> IID -> IO InterfacePtr
clrCreateInstance hMscoree clsid iid = do
  clrCreateInstanceAddr <- getProcAddress hMscoree "CLRCreateInstance"
  if clrCreateInstanceAddr == nullPtr then
    return nullPtr
  else do
    let clrCreateInstanceRaw = makeCLRCreateInstance $ castPtrToFunPtr clrCreateInstanceAddr
    with (nullPtr :: InterfacePtr) $ \interfacePtr ->
      with clsid $ \refCLSID -> with iid $ \refIID -> do
        hr <- clrCreateInstanceRaw refCLSID refIID interfacePtr
        if hr == 0 then
          peek interfacePtr
        else
          return nullPtr

type CLRCreateInstance = Ptr CLSID -> Ptr IID -> Ptr InterfacePtr -> IO HResult
foreign import stdcall "dynamic" makeCLRCreateInstance :: FunPtr CLRCreateInstance -> CLRCreateInstance

-- | 'createMetaHost'
createMetaHost :: HINSTANCE -> IO ICLRMetaHost
createMetaHost hMscoree = clrCreateInstance hMscoree clsid_CLRMetaHost iid_ICLRMetaHost
                            where clsid_CLRMetaHost = Guid 0X9280188D 0X0E8E 0X4867 0XB3 0X0C 0X7F 0XA8 0X38 0X84 0XE8 0XDE
                                  iid_ICLRMetaHost  = Guid 0XD332DB9E 0XB9B3 0X4125 0X82 0X07 0XA1 0X48 0X84 0XF5 0X32 0X16

-- | 'enumInstalledRuntimes_ICLRMetaHost'
enumInstalledRuntimes_ICLRMetaHost :: ICLRMetaHost -> IO IEnumUnknown
enumInstalledRuntimes_ICLRMetaHost this = do
  f <- getInterfaceFunction 5 makeEnumInstalledRuntimes_ICLRMetaHost this
  with (nullPtr :: IEnumUnknown) $ \enumUnknownPtr -> do
    f this enumUnknownPtr >>= checkHR "EnumerateInstalledRuntimes_ICLRMetaHost"
    peek enumUnknownPtr

type EnumInstalledRuntimes_ICLRMetaHost = ICLRMetaHost -> Ptr IEnumUnknown -> IO HResult
foreign import stdcall "dynamic" makeEnumInstalledRuntimes_ICLRMetaHost :: FunPtr EnumInstalledRuntimes_ICLRMetaHost -> EnumInstalledRuntimes_ICLRMetaHost

-- | 'next_IEnumUnknown'
next_IEnumUnknown :: IEnumUnknown -> IO InterfacePtr
next_IEnumUnknown this = do
  f <- getInterfaceFunction 3 makeNext_IEnumUnknown this
  with (0 :: Word32) $ \fetched ->
    with (nullPtr :: InterfacePtr) $ \interfacePtr -> do
      hr <- f this 1 interfacePtr fetched
      if hr == 0 then
        peek interfacePtr
      else
        return nullPtr

type Next_IEnumUnknown = IEnumUnknown -> Word32 ->  Ptr InterfacePtr -> Ptr Word32 -> IO HResult
foreign import stdcall "dynamic" makeNext_IEnumUnknown :: FunPtr Next_IEnumUnknown -> Next_IEnumUnknown

-- | 'all_IEnumUnknown'
all_IEnumUnknown :: IEnumUnknown -> [InterfacePtr] -> IO [InterfacePtr]
all_IEnumUnknown this xs = do
  next <- next_IEnumUnknown this
  if next == nullPtr then
    return xs
  else
    all_IEnumUnknown this (next : xs)

-- | 'getRuntimes_ICLRMetaHost'
getRuntimes_ICLRMetaHost :: ICLRMetaHost -> IO [ICLRRuntimeInfo]
getRuntimes_ICLRMetaHost this = do
  enum <- enumInstalledRuntimes_ICLRMetaHost this
  all_IEnumUnknown enum []

-- | 'getVersionString_ICLRRuntimeInfo'
getVersionString_ICLRRuntimeInfo :: ICLRRuntimeInfo -> IO String
getVersionString_ICLRRuntimeInfo this = do
  f <- getInterfaceFunction 3 makeGetVersionString_ICLRRuntimeInfo this
  with 512 $ \sizePtr ->
    allocaArray 512 $ \stringPtr -> do
      f this stringPtr sizePtr
      peekCWString stringPtr

type GetVersionString_ICLRRuntimeInfo = ICLRRuntimeInfo -> LPCWSTR -> Ptr DWORD -> IO HResult
foreign import stdcall "dynamic" makeGetVersionString_ICLRRuntimeInfo :: FunPtr GetVersionString_ICLRRuntimeInfo -> GetVersionString_ICLRRuntimeInfo

-- | 'getCorHost_ICLRRuntimeInfo'
getCorHost_ICLRRuntimeInfo :: ICLRRuntimeInfo -> IO ICorRuntimeHost
getCorHost_ICLRRuntimeInfo this = getInterface_ICLRRuntimeInfo this clsid_CorRuntimeHost iid_ICorRuntimeHost

-- | 'getCLRHost_ICLRRuntimeInfo'
getCLRHost_ICLRRuntimeInfo :: ICLRRuntimeInfo -> IO ICLRRuntimeHost
getCLRHost_ICLRRuntimeInfo this = getInterface_ICLRRuntimeInfo this clsid_CLRRuntimeHost iid_ICLRRuntimeHost

-- | 'getInterface_ICLRRuntimeInfo'
getInterface_ICLRRuntimeInfo :: ICLRRuntimeInfo -> Guid -> Guid -> IO InterfacePtr
getInterface_ICLRRuntimeInfo this clsid iid = do
  f <- getInterfaceFunction 9 makeGetInterface_ICLRRuntimeInfo this
  with (nullPtr :: InterfacePtr) $ \interfacePtr -> do
    with clsid $ \refCLSID ->
      with iid $ \refIID ->
        f this refCLSID refIID interfacePtr >>= checkHR "GetInterface_ICLRRuntimeInfo"
    peek interfacePtr

type GetInterface_ICLRRuntimeInfo = ICLRRuntimeInfo -> Ptr CLSID -> Ptr IID -> Ptr ICorRuntimeHost -> IO HResult
foreign import stdcall "dynamic" makeGetInterface_ICLRRuntimeInfo :: FunPtr GetInterface_ICLRRuntimeInfo -> GetInterface_ICLRRuntimeInfo

-- | 'getDefaultDomain_ICorRuntimeHost' calls the GetDefaultDOmain method of the given
--   ICorRuntimeHost interface.
getDefaultDomain_ICorRuntimeHost this = do
  f <- getInterfaceFunction 13 makeGetDefaultDomain this
  with (nullPtr :: InterfacePtr) $ \appDomainPtr -> do
    f this appDomainPtr >>= checkHR "ICorRuntimeHost.GetDefaultDomain"
    peek appDomainPtr

type GetDefaultDomain = ICorRuntimeHost -> Ptr InterfacePtr -> IO HResult
foreign import stdcall "dynamic" makeGetDefaultDomain :: FunPtr GetDefaultDomain -> GetDefaultDomain

type AppDomain = InterfacePtr -- mscorlib::_AppDomain

-- | 'load_AppDomain' calls mscorlib::_AppDomain.Load_3(SafeArray* rawAssembly, _Assembly** result).
load_AppDomain this rawAssembly = do
  f <- getInterfaceFunction 45 makeLoad_AppDomain this
  with (nullPtr :: Assembly) $ \assemblyPtr -> do
    f this rawAssembly assemblyPtr >>= checkHR "AppDomain.Load"
    peek assemblyPtr

type Load_AppDomain = AppDomain -> SafeArray -> Ptr Assembly -> IO HResult
foreign import stdcall "dynamic" makeLoad_AppDomain :: FunPtr Load_AppDomain -> Load_AppDomain

-- | 'load_AppDomain_2' calls mscorlib::_AppDomain.Load_2(BStr assemblyString, _Assembly** result).
load_AppDomain_2 this assemblyString = do
  f <- getInterfaceFunction 44 makeLoad_AppDomain_2 this
  withBStr assemblyString $ \assemblyString' -> do
    with (nullPtr :: InterfacePtr) $ \assemblyPtr -> do
      f this assemblyString' assemblyPtr >>= checkHR "AppDomain.Load"
      peek assemblyPtr

type Load_AppDomain_2 = AppDomain -> BStr -> Ptr InterfacePtr -> IO HResult
foreign import stdcall "dynamic" makeLoad_AppDomain_2 :: FunPtr Load_AppDomain_2 -> Load_AppDomain_2

type Assembly = InterfacePtr -- mscorlib::_Assembly

-- | 'getType_Assembly' calls mscorlib::_Assembly.GetType_2(BStr name, _Type** result).
getType_Assembly this name = do
  f <- getInterfaceFunction 17 makeGetType_Assembly this
  withBStr name $ \name' ->
    with (nullPtr :: CorLibType) $ \typePtr -> do
      f this name' typePtr >>= checkHR "Assembly.GetType"
      t <- peek typePtr
      if t == nullPtr then error "Assembly.GetType failed"
              else return t

type GetType_Assembly = Assembly -> BStr -> Ptr CorLibType -> IO HResult
foreign import stdcall "dynamic" makeGetType_Assembly :: FunPtr GetType_Assembly -> GetType_Assembly

type CorLibType = InterfacePtr -- mscorlib::_Type

-- | 'invokeMember_Type' calls mscorlib::_Type.InvokeMember_3(BStr name,
--   BindingFlags invokeAttr, _Binder* binder, Variant target, SafeArray* args,
--   Variant* result) to invoke a method without a binder, target or any arguments.
invokeMember_Type this memberName = do
  f <- getInterfaceFunction 57 {- _Type.InvokeMember_3 -} makeInvokeMember_Type this
  withBStr memberName $ \memberName' ->
    with emptyVariant $ \resultPtr -> do
#if x86_64_HOST_ARCH
      with emptyVariant $ \target ->
              f this memberName' 256 nullPtr target nullPtr resultPtr >>= checkHR "Type.InvokeMember"
#else
      f this memberName' 256 nullPtr 0 0 nullPtr resultPtr >>= checkHR "Type.InvokeMember"
#endif
      peek resultPtr

-- Portability note: Type.InvokeMember accepts a by-value variant argument (as its fourth
--                   argument). GHC doesn't let us use that as an argument however, so
--                   there's a calling convention dependent hack in the declarations below.
--                   For Stdcall, this is encoded as two Word64 arguments.
--                   For Win64, it would expect an argument of this size to be passed by reference.
#if x86_64_HOST_ARCH
type InvokeMember_Type = CorLibType -> BStr -> Word32 -> Ptr () -> Ptr Variant -> Ptr () -> Ptr Variant -> IO HResult
#else
type InvokeMember_Type = CorLibType -> BStr -> Word32 -> Ptr () -> Word64 -> Word64 -> Ptr () -> Ptr Variant -> IO HResult
#endif
foreign import stdcall "dynamic" makeInvokeMember_Type :: FunPtr InvokeMember_Type -> InvokeMember_Type


-- | 'loadDriverAndBoot' loads the Salsa driver assembly into the application domain from
--   memory (the binary data is originally stored in 'driverData'), and then invokes the
--   Boot method (from the Salsa.Driver class) to obtain a function pointer for invoking
--   the 'GetPointerToMethod' method.
loadDriverAndBoot :: ICorRuntimeHost -> IO (FunPtr (Ptr Word16 -> IO (FunPtr a)))
loadDriverAndBoot clrHost = do
  -- Obtain an _AppDomain interface pointer to the default application domain
  withInterface (getDefaultDomain_ICorRuntimeHost clrHost) $ \untypedAppDomain -> do
    let iid_AppDomain = Guid 0x05F696DC 0x2B29 0x3663 0xAD 0x8B 0xC4 0x38 0x9C 0xF2 0xA7 0x13
    withInterface (queryInterface untypedAppDomain iid_AppDomain) $ \appDomain -> do

      -- Create a safe array for the contents of the driver assembly binary
      bracket (prim_SafeArrayCreateVector varType_UI1 0 (fromIntegral $ S.length driverData))
          (prim_SafeArrayDestroy)
        (\sa -> do
          -- Copy the driver assembly data into the safe array
          saDataPtr <- with (nullPtr :: Ptr Word8) $ \ptr ->
                                      prim_SafeArrayAccessData sa ptr >> peek ptr
          S.useAsCStringLen driverData $ \(bsPtr,bsLen) -> do
            copyBytes saDataPtr (unsafeCoerce bsPtr) bsLen
          prim_SafeArrayUnaccessData sa

          -- Load the driver assembly into the application domain
          withInterface (load_AppDomain appDomain sa) $ \assembly -> do

            -- Obtain a _Type interface pointer to the Salsa.Driver type
            withInterface (getType_Assembly assembly "Salsa.Driver") $ \typ -> do

              -- Invoke the Boot method of Salsa.Driver to obtain a function
              -- pointer to the 'GetPointerToMethod' method
              (Variant _ returnValue) <- invokeMember_Type typ "Boot"

              -- Return a wrapper function for the 'GetPointerToMethod' method
              return $ unsafeCoerce returnValue)