packages feed

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

{- |
Module      : WasmEdge.Caller
Description : Caller context and guest memory access for host functions
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : stable

Inspired by the @Caller@ abstraction in @wasmedge-sdk@ (Rust), this module
provides safe, idiomatic Haskell helpers for host functions to inspect and
manipulate the linear memory of the calling WebAssembly guest instance.

When a WebAssembly module invokes an imported host function, WasmEdge supplies
a 'CallingFrame'. Using 'getCallerMemory', 'readCallerMemory', or 'writeCallerMemory',
host functions can easily exchange structured data, buffers, and strings with the guest.

== Example

@
import WasmEdge.Caller
import qualified Data.ByteString as BS

-- Host function reading a string from guest memory and logging it
logMessageHostFunc :: CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal])
logMessageHostFunc frame [WasmI32 ptr, WasmI32 len] = do
  mStr <- readCallerMemory frame 0 (fromIntegral ptr) (fromIntegral len)
  case mStr of
    Nothing -> pure (Left (WasmEdgeError ErrCategory_UserDefined 1 "Invalid memory access"))
    Just bs -> do
      putStrLn $ "Guest message: " ++ show bs
      pure (Right [])
logMessageHostFunc _ _ = pure (Left (WasmEdgeError ErrCategory_UserDefined 2 "Invalid arguments"))
@
-}
module WasmEdge.Caller
  ( -- * Caller memory access
    getCallerMemory
  , readCallerMemory
  , writeCallerMemory
  , readCallerCString
  , readCallerCStringMax

    -- * Caller execution context
  , getCallerModuleInstance
  , getCallerExecutor
  ) where

import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Word (Word32, Word64)
import Foreign.Ptr (Ptr, castPtr, nullPtr)

import WasmEdge.Error (ErrCategory (..), WasmEdgeError (..), WasmResult)
import WasmEdge.Function
  ( CallingFrame (..)
  , callingFrameGetExecutor
  , callingFrameGetMemoryInstance
  , callingFrameGetModuleInstance
  )
import WasmEdge.Internal.Types (ExecutorTag, ModuleInstanceTag)
import WasmEdge.Memory
  ( MemoryInstance
  , borrowMemory
  , getMemoryData
  , getMemoryPageSize
  , getMemoryPointerConst
  , setMemoryData
  )

{- | Borrow the 'MemoryInstance' at the given index from the caller module.

In WebAssembly, index 0 is the default linear memory.
Returns 'Nothing' if the caller module has no memory at that index or if the
calling frame is invalid.
-}
getCallerMemory :: CallingFrame -> Word32 -> IO (Maybe MemoryInstance)
getCallerMemory frame idx = do
  p <- callingFrameGetMemoryInstance frame idx
  if p == nullPtr
    then pure Nothing
    else Just <$> borrowMemory p

{- | Read @length@ bytes from the caller's linear memory at index @memIdx@ starting at @offset@.
Returns 'Nothing' if the memory instance cannot be found or the range is out of bounds.
-}
readCallerMemory ::
  -- | Calling frame passed to host function
  CallingFrame ->
  -- | Memory index (typically 0)
  Word32 ->
  -- | Byte offset in memory
  Word32 ->
  -- | Number of bytes to read
  Word32 ->
  IO (Maybe ByteString)
readCallerMemory frame memIdx offset len = do
  mMem <- getCallerMemory frame memIdx
  case mMem of
    Nothing -> pure Nothing
    Just mem -> do
      res <- getMemoryData mem offset len
      case res of
        Left _ -> pure Nothing
        Right bs -> pure (Just bs)

-- | Write a 'ByteString' into the caller's linear memory at index @memIdx@ starting at @offset@.
writeCallerMemory ::
  -- | Calling frame passed to host function
  CallingFrame ->
  -- | Memory index (typically 0)
  Word32 ->
  -- | Byte offset in memory
  Word32 ->
  -- | Bytes to write
  ByteString ->
  IO (WasmResult ())
writeCallerMemory frame memIdx offset bs = do
  mMem <- getCallerMemory frame memIdx
  case mMem of
    Nothing -> pure (Left (WasmEdgeError ErrCategory_WASM 0x01 "Caller memory instance not found"))
    Just mem -> setMemoryData mem offset bs

{- | Read a null-terminated UTF-8 / ASCII string from caller memory starting at @offset@,
scanning up to 4096 bytes.
-}
readCallerCString ::
  -- | Calling frame passed to host function
  CallingFrame ->
  -- | Memory index (typically 0)
  Word32 ->
  -- | Byte offset in memory
  Word32 ->
  IO (Maybe ByteString)
readCallerCString frame memIdx offset = readCallerCStringMax frame memIdx offset 4096

-- | Read a null-terminated string from caller memory with an explicit upper bound on bytes scanned.
readCallerCStringMax ::
  -- | Calling frame
  CallingFrame ->
  -- | Memory index (typically 0)
  Word32 ->
  -- | Byte offset in memory
  Word32 ->
  -- | Maximum bytes to scan
  Word32 ->
  IO (Maybe ByteString)
readCallerCStringMax frame memIdx offset maxScan = do
  mMem <- getCallerMemory frame memIdx
  case mMem of
    Nothing -> pure Nothing
    Just mem -> do
      pageSize <- getMemoryPageSize mem
      -- 1 page = 65536 bytes. Use Word64 to avoid overflow at max page count (65536 pages = 4 GiB).
      let totalBytes = fromIntegral pageSize * 65536 :: Word64
          offset64 = fromIntegral offset :: Word64
      if offset64 >= totalBytes
        then pure Nothing
        else do
          let boundedLen = fromIntegral (min (fromIntegral maxScan :: Word64) (totalBytes - offset64)) :: Word32
          ptr <- getMemoryPointerConst mem offset boundedLen
          if ptr == nullPtr
            then pure Nothing
            else do
              bs <- BS.packCString (castPtr ptr)
              pure (Just bs)

-- | Retrieve the raw module instance pointer of the current calling frame.
getCallerModuleInstance :: CallingFrame -> IO (Ptr ModuleInstanceTag)
getCallerModuleInstance = callingFrameGetModuleInstance

-- | Retrieve the raw executor pointer from the current calling frame.
getCallerExecutor :: CallingFrame -> IO (Ptr ExecutorTag)
getCallerExecutor = callingFrameGetExecutor