packages feed

wasmedge-hs-0.17.1.0: src/WasmEdge/Function.hs

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}

{- |
Module      : WasmEdge.Function
Description : WasmEdge function types and host function instances
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : stable

Provides types and functions for WebAssembly function types and host function instances:

* 'FunctionType' — parameter and return type signatures
* 'FunctionInstance' — a callable function in WebAssembly or the host
* 'HostFunc' — raw low-level callback interface (@CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal])@)
* 'ToHostFunc' — high-level typeclass allowing normal Haskell functions (e.g. @Int32 -> Int32 -> IO Int32@
  or @CallingFrame -> Word32 -> IO ()@) to be registered directly as host functions without manual value unpacking.
* 'CallingFrame' — execution frame passed to host functions providing access to caller memory and module instances.
-}
module WasmEdge.Function
  ( -- * High-level host functions
    ToHostFunc (..)
  , WasmValType (..)
  , WasmReturn (..)
  , createHostFunction
  , withHostFunction

    -- * Function type
  , FunctionType
  , createFunctionType
  , deleteFunctionType
  , withFunctionType
  , getFunctionTypeParameters
  , getFunctionTypeReturns

    -- * Calling frame
  , CallingFrame (..)
  , callingFrameGetExecutor
  , callingFrameGetModuleInstance
  , callingFrameGetMemoryInstance

    -- * Raw host function callback
  , HostFunc

    -- * Function instance
  , FunctionInstance
  , createFunction
  , deleteFunction
  , withFunction
  , getFunctionType

    -- * Internal / borrowed pointers
  , unsafeFunctionPtr
  , borrowFunction
  , unsafeFunctionTypePtr
  , borrowFunctionType
  , disarmFunction
  ) where

import Control.Exception (bracket, throwIO)
import Control.Monad (forM_)
import Data.Int (Int16, Int32, Int64, Int8)
import Data.Proxy (Proxy (..))
import Data.Word (Word16, Word32, Word64, Word8)
import Foreign.Marshal.Utils (copyBytes)
import Foreign.Ptr (FunPtr, Ptr, castPtr, freeHaskellFunPtr, nullPtr, plusPtr)

import WasmEdge.Error
import WasmEdge.Internal.FFI.Basic (c_ResultGen, c_Result_Success)
import WasmEdge.Internal.FFI.Instance
import WasmEdge.Internal.Ownership
  ( BorrowedCtx (..)
  , OwnedCtx
  , disarmOwned
  , finalizeOwned
  , mkOwned
  , withOwned
  )
import WasmEdge.Internal.Storable (withCValTypeArray)
import WasmEdge.Internal.Types
  ( CallingFrameTag
  , ExecutorTag
  , FunctionInstanceTag
  , FunctionTypeTag
  , MemoryInstanceTag
  , ModuleInstanceTag
  )
import WasmEdge.Types
import WasmEdge.Value (encodeVal, peekValArray, peekValTypes, valueSize, withValTypeArray)

-- ---------------------------------------------------------------------------
-- FunctionType
-- ---------------------------------------------------------------------------

-- | A WebAssembly function type describing parameter and return types.
newtype FunctionType = FunctionType (OwnedCtx FunctionTypeTag)

-- | Create a 'FunctionType' from lists of parameter and return types.
createFunctionType :: [ValType] -> [ValType] -> IO FunctionType
createFunctionType params rets =
  withValTypeArray params $ \pArr pLen ->
    withValTypeArray rets $ \rArr rLen -> do
      p <- c_FunctionTypeCreate pArr pLen rArr rLen
      if p == nullPtr
        then throwIO $ WasmEdgeError ErrCategory_WASM 0 "WasmEdge_FunctionTypeCreate returned NULL"
        else FunctionType <$> mkOwned (appFunPtr c_FunctionTypeDelete) p

-- | Delete a 'FunctionType'.
deleteFunctionType :: FunctionType -> IO ()
deleteFunctionType (FunctionType o) = finalizeOwned o

-- | Scoped 'FunctionType'.
withFunctionType :: [ValType] -> [ValType] -> (FunctionType -> IO a) -> IO a
withFunctionType params rets = bracket (createFunctionType params rets) deleteFunctionType

-- | Get the parameter types of a 'FunctionType'.
getFunctionTypeParameters :: FunctionType -> IO [ValType]
getFunctionTypeParameters (FunctionType o) =
  withOwned o $ \p -> do
    len <- c_FunctionTypeGetParametersLength p
    if len == 0
      then pure []
      else withCValTypeArray (fromIntegral len) $ \arr -> do
        _ <- c_FunctionTypeGetParameters p arr len
        peekValTypes arr len

-- | Get the return types of a 'FunctionType'.
getFunctionTypeReturns :: FunctionType -> IO [ValType]
getFunctionTypeReturns (FunctionType o) =
  withOwned o $ \p -> do
    len <- c_FunctionTypeGetReturnsLength p
    if len == 0
      then pure []
      else withCValTypeArray (fromIntegral len) $ \arr -> do
        _ <- c_FunctionTypeGetReturns p arr len
        peekValTypes arr len

-- ---------------------------------------------------------------------------
-- CallingFrame
-- ---------------------------------------------------------------------------

-- | Context passed to a host function callback representing the caller's frame.
newtype CallingFrame = CallingFrame (BorrowedCtx CallingFrameTag)

-- | Get the executor from a 'CallingFrame'.
callingFrameGetExecutor :: CallingFrame -> IO (Ptr ExecutorTag)
callingFrameGetExecutor (CallingFrame (BorrowedCtx p)) =
  c_CallingFrameGetExecutor p

-- | Get the module instance from a 'CallingFrame'.
callingFrameGetModuleInstance :: CallingFrame -> IO (Ptr ModuleInstanceTag)
callingFrameGetModuleInstance (CallingFrame (BorrowedCtx p)) =
  c_CallingFrameGetModuleInstance p

-- | Get the memory instance by index (typically 0) from a 'CallingFrame'.
callingFrameGetMemoryInstance :: CallingFrame -> Word32 -> IO (Ptr MemoryInstanceTag)
callingFrameGetMemoryInstance (CallingFrame (BorrowedCtx p)) =
  c_CallingFrameGetMemoryInstance p

-- ---------------------------------------------------------------------------
-- HostFunc & FunctionInstance
-- ---------------------------------------------------------------------------

-- | Raw low-level Haskell implementation of a host function callable from WebAssembly.
type HostFunc = CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal])

-- | A WebAssembly function instance.
newtype FunctionInstance = FunctionInstance (OwnedCtx FunctionInstanceTag)

-- | Create a host 'FunctionInstance' from a 'FunctionType' and raw Haskell 'HostFunc'.
createFunction :: FunctionType -> HostFunc -> IO FunctionInstance
createFunction (FunctionType ftOwned) hostFn =
  withOwned ftOwned $ \ftPtr -> do
    -- Allocate C callback wrapper
    fptr <- mkHostFuncWrapper $ \_data framePtr paramsPtr pLen returnsPtr _rLen resOutPtr -> do
      vals <- peekValArray (castPtr paramsPtr) pLen
      let frame = CallingFrame (BorrowedCtx framePtr)
      result <- hostFn frame vals
      case result of
        Right retVals -> do
          -- Encode return values into returnsPtr
          forM_ (zip [0 ..] retVals) $ \(i, v) ->
            encodeVal v $ \encodedPtr -> do
              let dst = returnsPtr `plusPtr` (i * valueSize)
              copyBytes dst (castPtr encodedPtr) valueSize
          c_Result_Success resOutPtr
        Left err -> do
          let catCode = case weErrCategory err of
                ErrCategory_WASM -> 0 :: Word32
                ErrCategory_UserDefined -> 1 :: Word32
                ErrCategory_Unknown c -> c
          c_ResultGen resOutPtr catCode (weErrCode err)

    p <- c_FunctionInstanceCreateBinding ftPtr fptr nullPtr 0
    if p == nullPtr
      then do
        freeHaskellFunPtr fptr
        throwIO $ WasmEdgeError ErrCategory_WASM 0 "WasmEdge_FunctionInstanceCreateBinding returned NULL"
      else
        FunctionInstance
          <$> mkOwned
            ( \ptr -> do
                c_FunctionInstanceDelete `appFunPtr` ptr
                freeHaskellFunPtr fptr
            )
            p

-- | Delete a 'FunctionInstance'.
deleteFunction :: FunctionInstance -> IO ()
deleteFunction (FunctionInstance o) = finalizeOwned o

-- | Scoped 'FunctionInstance' with raw 'HostFunc'.
withFunction :: FunctionType -> HostFunc -> (FunctionInstance -> IO a) -> IO a
withFunction ft hostFn = bracket (createFunction ft hostFn) deleteFunction

-- | Get the 'FunctionType' of a 'FunctionInstance'.
getFunctionType :: FunctionInstance -> IO FunctionType
getFunctionType (FunctionInstance o) =
  withOwned o $ \p -> do
    tp <- c_FunctionInstanceGetFunctionType p
    if tp == nullPtr
      then
        throwIO $ WasmEdgeError ErrCategory_WASM 0 "WasmEdge_FunctionInstanceGetFunctionType returned NULL"
      else borrowFunctionType tp

-- ---------------------------------------------------------------------------
-- High-Level Typeclass-driven Host Function Registration (ToHostFunc)
-- ---------------------------------------------------------------------------

-- | Typeclass for Haskell values that can be converted to and from a single WebAssembly value.
class WasmValType a where
  toWasmVal :: a -> WasmVal
  fromWasmVal :: WasmVal -> Maybe a
  wasmValType :: Proxy a -> ValType

instance WasmValType Int32 where
  toWasmVal = WasmI32
  fromWasmVal (WasmI32 v) = Just v
  fromWasmVal _ = Nothing
  wasmValType _ = I32

instance WasmValType Word32 where
  toWasmVal = WasmI32 . fromIntegral
  fromWasmVal (WasmI32 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I32

instance WasmValType Int64 where
  toWasmVal = WasmI64
  fromWasmVal (WasmI64 v) = Just v
  fromWasmVal _ = Nothing
  wasmValType _ = I64

instance WasmValType Word64 where
  toWasmVal = WasmI64 . fromIntegral
  fromWasmVal (WasmI64 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I64

instance WasmValType Float where
  toWasmVal = WasmF32
  fromWasmVal (WasmF32 v) = Just v
  fromWasmVal _ = Nothing
  wasmValType _ = F32

instance WasmValType Double where
  toWasmVal = WasmF64
  fromWasmVal (WasmF64 v) = Just v
  fromWasmVal _ = Nothing
  wasmValType _ = F64

instance WasmValType WasmVal where
  toWasmVal = id
  fromWasmVal = Just
  wasmValType _ = I32

-- | 'Int8' is coerced to an @i32@ (Wasm has no @i8@).
instance WasmValType Int8 where
  toWasmVal = WasmI32 . fromIntegral
  fromWasmVal (WasmI32 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I32

-- | 'Word8' is coerced to an @i32@ (Wasm has no @u8@).
instance WasmValType Word8 where
  toWasmVal = WasmI32 . fromIntegral
  fromWasmVal (WasmI32 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I32

-- | 'Int16' is coerced to an @i32@ (Wasm has no @i16@).
instance WasmValType Int16 where
  toWasmVal = WasmI32 . fromIntegral
  fromWasmVal (WasmI32 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I32

-- | 'Word16' is coerced to an @i32@ (Wasm has no @u16@).
instance WasmValType Word16 where
  toWasmVal = WasmI32 . fromIntegral
  fromWasmVal (WasmI32 v) = Just (fromIntegral v)
  fromWasmVal _ = Nothing
  wasmValType _ = I32

-- | 'Bool' is encoded as @i32@: 0 = 'False', 1 = 'True'.
instance WasmValType Bool where
  toWasmVal True = WasmI32 1
  toWasmVal False = WasmI32 0
  fromWasmVal (WasmI32 0) = Just False
  fromWasmVal (WasmI32 _) = Just True
  fromWasmVal _ = Nothing
  wasmValType _ = I32

-- | Typeclass for host function return values.
class WasmReturn a where
  toWasmReturn :: a -> [WasmVal]
  wasmReturnTypes :: Proxy a -> [ValType]

instance WasmReturn () where
  toWasmReturn () = []
  wasmReturnTypes _ = []

instance WasmReturn Int32 where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [I32]

instance WasmReturn Word32 where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [I32]

instance WasmReturn Int64 where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [I64]

instance WasmReturn Word64 where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [I64]

instance WasmReturn Float where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [F32]

instance WasmReturn Double where
  toWasmReturn v = [toWasmVal v]
  wasmReturnTypes _ = [F64]

instance (WasmValType a, WasmValType b) => WasmReturn (a, b) where
  toWasmReturn (a, b) = [toWasmVal a, toWasmVal b]
  wasmReturnTypes _ = [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b)]

instance (WasmValType a, WasmValType b, WasmValType c) => WasmReturn (a, b, c) where
  toWasmReturn (a, b, c) = [toWasmVal a, toWasmVal b, toWasmVal c]
  wasmReturnTypes _ = [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b), wasmValType (Proxy :: Proxy c)]

{- | Typeclass for functions that can be converted into a WasmEdge host function.

Automatically converts Haskell typed arguments and results, with optional 'CallingFrame'.
-}
class ToHostFunc f where
  toHostFunc :: f -> HostFunc
  hostFuncSignature :: Proxy f -> ([ValType], [ValType])

-- 0 args
instance {-# OVERLAPPABLE #-} (WasmReturn r) => ToHostFunc (IO r) where
  toHostFunc act _frame [] = Right . toWasmReturn <$> act
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ = ([], wasmReturnTypes (Proxy :: Proxy r))

instance {-# OVERLAPPING #-} (WasmReturn r) => ToHostFunc (CallingFrame -> IO r) where
  toHostFunc act frame [] = Right . toWasmReturn <$> act frame
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ = ([], wasmReturnTypes (Proxy :: Proxy r))

-- 1 arg
instance {-# OVERLAPPABLE #-} (WasmValType a, WasmReturn r) => ToHostFunc (a -> IO r) where
  toHostFunc act _frame [va] = case fromWasmVal va of
    Just a -> Right . toWasmReturn <$> act a
    Nothing -> pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch for argument 1"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ = ([wasmValType (Proxy :: Proxy a)], wasmReturnTypes (Proxy :: Proxy r))

instance {-# OVERLAPPING #-} (WasmValType a, WasmReturn r) => ToHostFunc (CallingFrame -> a -> IO r) where
  toHostFunc act frame [va] = case fromWasmVal va of
    Just a -> Right . toWasmReturn <$> act frame a
    Nothing -> pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch for argument 1"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ = ([wasmValType (Proxy :: Proxy a)], wasmReturnTypes (Proxy :: Proxy r))

-- 2 args
instance {-# OVERLAPPABLE #-} (WasmValType a, WasmValType b, WasmReturn r) => ToHostFunc (a -> b -> IO r) where
  toHostFunc act _frame [va, vb] = case (fromWasmVal va, fromWasmVal vb) of
    (Just a, Just b) -> Right . toWasmReturn <$> act a b
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    ( [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b)]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

instance {-# OVERLAPPING #-} (WasmValType a, WasmValType b, WasmReturn r) => ToHostFunc (CallingFrame -> a -> b -> IO r) where
  toHostFunc act frame [va, vb] = case (fromWasmVal va, fromWasmVal vb) of
    (Just a, Just b) -> Right . toWasmReturn <$> act frame a b
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    ( [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b)]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

-- 3 args
instance {-# OVERLAPPABLE #-} (WasmValType a, WasmValType b, WasmValType c, WasmReturn r) => ToHostFunc (a -> b -> c -> IO r) where
  toHostFunc act _frame [va, vb, vc] = case (fromWasmVal va, fromWasmVal vb, fromWasmVal vc) of
    (Just a, Just b, Just c) -> Right . toWasmReturn <$> act a b c
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    ( [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b), wasmValType (Proxy :: Proxy c)]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

instance
  {-# OVERLAPPING #-}
  (WasmValType a, WasmValType b, WasmValType c, WasmReturn r) =>
  ToHostFunc (CallingFrame -> a -> b -> c -> IO r)
  where
  toHostFunc act frame [va, vb, vc] = case (fromWasmVal va, fromWasmVal vb, fromWasmVal vc) of
    (Just a, Just b, Just c) -> Right . toWasmReturn <$> act frame a b c
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    ( [wasmValType (Proxy :: Proxy a), wasmValType (Proxy :: Proxy b), wasmValType (Proxy :: Proxy c)]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

-- 4 args
instance
  {-# OVERLAPPABLE #-}
  (WasmValType a, WasmValType b, WasmValType c, WasmValType d, WasmReturn r) =>
  ToHostFunc (a -> b -> c -> d -> IO r)
  where
  toHostFunc act _frame [va, vb, vc, vd] = case (fromWasmVal va, fromWasmVal vb, fromWasmVal vc, fromWasmVal vd) of
    (Just a, Just b, Just c, Just d) -> Right . toWasmReturn <$> act a b c d
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    (
      [ wasmValType (Proxy :: Proxy a)
      , wasmValType (Proxy :: Proxy b)
      , wasmValType (Proxy :: Proxy c)
      , wasmValType (Proxy :: Proxy d)
      ]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

instance
  {-# OVERLAPPING #-}
  (WasmValType a, WasmValType b, WasmValType c, WasmValType d, WasmReturn r) =>
  ToHostFunc (CallingFrame -> a -> b -> c -> d -> IO r)
  where
  toHostFunc act frame [va, vb, vc, vd] = case (fromWasmVal va, fromWasmVal vb, fromWasmVal vc, fromWasmVal vd) of
    (Just a, Just b, Just c, Just d) -> Right . toWasmReturn <$> act frame a b c d
    _ ->
      pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Type mismatch in arguments"))
  toHostFunc _ _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid argument count"))
  hostFuncSignature _ =
    (
      [ wasmValType (Proxy :: Proxy a)
      , wasmValType (Proxy :: Proxy b)
      , wasmValType (Proxy :: Proxy c)
      , wasmValType (Proxy :: Proxy d)
      ]
    , wasmReturnTypes (Proxy :: Proxy r)
    )

-- | Create a host 'FunctionInstance' automatically inferring its 'FunctionType' from Haskell types.
createHostFunction :: forall f. (ToHostFunc f) => f -> IO FunctionInstance
createHostFunction f = do
  let (params, rets) = hostFuncSignature (Proxy :: Proxy f)
  ft <- createFunctionType params rets
  createFunction ft (toHostFunc f)

-- | Scoped 'FunctionInstance' with automatic signature inference.
withHostFunction :: (ToHostFunc f) => f -> (FunctionInstance -> IO a) -> IO a
withHostFunction f = bracket (createHostFunction f) deleteFunction

-- ---------------------------------------------------------------------------
-- Unsafe / internal pointer helpers
-- ---------------------------------------------------------------------------

-- | Extract the raw pointer of a 'FunctionInstance'.
unsafeFunctionPtr :: FunctionInstance -> IO (Ptr FunctionInstanceTag)
unsafeFunctionPtr (FunctionInstance o) = withOwned o pure

-- | Create a borrowed 'FunctionInstance' from a raw pointer (does not finalize).
borrowFunction :: Ptr FunctionInstanceTag -> IO FunctionInstance
borrowFunction p = FunctionInstance <$> mkOwned (\_ -> pure ()) p

-- | Extract the raw pointer of a 'FunctionType'.
unsafeFunctionTypePtr :: FunctionType -> IO (Ptr FunctionTypeTag)
unsafeFunctionTypePtr (FunctionType o) = withOwned o pure

-- | Create a borrowed 'FunctionType' from a raw pointer (does not finalize).
borrowFunctionType :: Ptr FunctionTypeTag -> IO FunctionType
borrowFunctionType p = FunctionType <$> mkOwned (\_ -> pure ()) p

-- | Disarm finalizer when ownership is transferred to a module instance.
disarmFunction :: FunctionInstance -> IO ()
disarmFunction (FunctionInstance o) = disarmOwned o

-- ---------------------------------------------------------------------------
-- Internal helper
-- ---------------------------------------------------------------------------

appFunPtr :: FunPtr (Ptr a -> IO ()) -> Ptr a -> IO ()
appFunPtr = mkDeleter

foreign import ccall "dynamic"
  mkDeleter :: FunPtr (Ptr a -> IO ()) -> Ptr a -> IO ()