packages feed

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

{-# LANGUAGE ScopedTypeVariables #-}

{- |
Module      : WasmEdge.Function.Memory
Description : Memory-aware host function lifting
Copyright   : (c) 2026 Tushar Adhatrao
License     : MIT
Stability   : experimental

Provides utilities for writing host functions that work with complex Haskell
types by automatically marshalling them through Wasm linear memory.

== The problem

WebAssembly functions can only pass numeric values (@i32@, @i64@, @f32@, @f64@)
across the FFI boundary. To call a host function with a @Text@ argument or
return a @[Double]@, the convention is:

  * The guest writes the data into its linear memory.
  * The guest passes the @(ptr: i32, len: i32)@ pair as function arguments.
  * The host reads the bytes from the calling frame's memory and deserialises.

'liftMemFunc' automates this entire read-deserialise-compute-serialise-write
pipeline for host functions.

== Usage

@
-- A host function that takes Text input and returns Text output:
myGreeter :: Text -> IO (Either WasmEdgeError Text)
myGreeter name = pure (Right (\"Hello, \" \<\> name \<\> \"!\"))

-- Register it as a host function with signature (i32, i32) -> i64:
fn \<- liftMemFunc myGreeter
withFunction someType fn $ \\hostFn -> ...
@

The Wasm guest calls it as:
@
let ptr = alloc(len);
store_str(ptr, input_str);
let result_packed = greet(ptr, len);
let ret_ptr = (result_packed >> 32) as i32;
let ret_len = (result_packed & 0xFFFFFFFF) as i32;
@

== Result encoding

Return values are packed as a single @i64@: upper 32 bits = output pointer,
lower 32 bits = output length. The output buffer is written into a scratch
region of Wasm linear memory.

For host functions that do not return data (only side effects), use
'liftMemFunc_'.
-}
module WasmEdge.Function.Memory
  ( -- * Lifting host functions
    liftMemFunc
  , liftMemFunc_
  , liftMemFuncWith

    -- * Helper: get memory from calling frame
  , callingFrameMemory
  ) where

import Data.Bits (shiftL, (.|.))
import Data.Text qualified as T
import Data.Word (Word32, Word64)
import Foreign.Ptr (nullPtr)

import WasmEdge.Error (ErrCategory (..), WasmEdgeError (..), WasmResult)
import WasmEdge.Function (CallingFrame, callingFrameGetMemoryInstance)
import WasmEdge.Memory (MemoryInstance, borrowMemory)
import WasmEdge.Memory.Alloc (AllocConvention (..), wasmAlloc)
import WasmEdge.Memory.Codec (WasmMemory (..))
import WasmEdge.Types (WasmVal (..))
import WasmEdge.VM (VM)

-- ---------------------------------------------------------------------------
-- Calling-frame memory helper
-- ---------------------------------------------------------------------------

{- | Get the linear memory instance from a 'CallingFrame'.

The calling frame always has memory index 0 available if the guest module
declares linear memory, which all practical Wasm modules do.

Returns 'Left' if no linear memory is available (null pointer from C API).
-}
callingFrameMemory :: CallingFrame -> IO (WasmResult MemoryInstance)
callingFrameMemory frame = do
  ptr <- callingFrameGetMemoryInstance frame 0
  if ptr == nullPtr
    then
      pure
        ( Left
            ( WasmEdgeError
                ErrCategory_UserDefined
                30
                "callingFrameMemory: no linear memory at index 0 in calling frame"
            )
        )
    else Right <$> borrowMemory ptr

-- ---------------------------------------------------------------------------
-- Core lifting
-- ---------------------------------------------------------------------------

{- | Lift a Haskell function @a -> IO (Either WasmEdgeError b)@ into a raw
'HostFunc' that marshals arguments through Wasm linear memory.

The generated host function:

* Expects two @i32@ arguments: @(ptr, len)@ — the pointer and length of the
  input buffer in Wasm linear memory.
* Reads and deserialises @a@ from that buffer using 'wasmPeek'.
* Runs the user function.
* Serialises @b@ into a scratch region of Wasm linear memory at 1 MiB offset.
* Returns a single @i64@ with @(retPtr \<\< 32 | retLen)@.

The function type to register with 'createFunction' is @[I32, I32] -> [I64]@.

__Note__: The scratch-region approach works for output up to ~63 MiB. For
larger outputs or when the Wasm module manages its own heap, use
'liftMemFuncWith' instead.
-}
liftMemFunc ::
  forall a b.
  (WasmMemory a, WasmMemory b) =>
  -- | The Haskell function to lift
  (a -> IO (Either WasmEdgeError b)) ->
  (CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal]))
liftMemFunc f frame args =
  case args of
    [WasmI32 inPtr, WasmI32 inLen] -> do
      memResult <- callingFrameMemory frame
      case memResult of
        Left err -> pure (Left err)
        Right mem -> do
          peekResult <- wasmPeek mem (fromIntegral inPtr) (fromIntegral inLen)
          case peekResult of
            Left err -> pure (Left err)
            Right a -> do
              result <- f a
              case result of
                Left err -> pure (Left err)
                Right b -> do
                  -- Write output to a well-known scratch region (1 MiB offset)
                  let scratchBase = 1048576 :: Word32 -- 1 MiB
                  pokeResult <- wasmPoke mem scratchBase b
                  case pokeResult of
                    Left err -> pure (Left err)
                    Right outLen ->
                      let packed :: Word64
                          packed =
                            (fromIntegral scratchBase `shiftL` 32)
                              .|. fromIntegral outLen
                       in pure (Right [WasmI64 (fromIntegral packed)])
    _ ->
      pure
        ( Left
            ( WasmEdgeError
                ErrCategory_UserDefined
                31
                "liftMemFunc: expected exactly two i32 arguments (ptr, len)"
            )
        )

{- | Like 'liftMemFunc' but for functions with no return value (pure side-effects).

The generated host function returns no values.
The function type for 'createFunction' is @[I32, I32] -> []@.
-}
liftMemFunc_ ::
  forall a.
  (WasmMemory a) =>
  (a -> IO (Either WasmEdgeError ())) ->
  (CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal]))
liftMemFunc_ f frame args =
  case args of
    [WasmI32 inPtr, WasmI32 inLen] -> do
      memResult <- callingFrameMemory frame
      case memResult of
        Left err -> pure (Left err)
        Right mem -> do
          peekResult <- wasmPeek mem (fromIntegral inPtr) (fromIntegral inLen)
          case peekResult of
            Left err -> pure (Left err)
            Right a -> do
              result <- f a
              case result of
                Left err -> pure (Left err)
                Right () -> pure (Right [])
    _ ->
      pure
        ( Left
            ( WasmEdgeError
                ErrCategory_UserDefined
                31
                "liftMemFunc_: expected exactly two i32 arguments (ptr, len)"
            )
        )

{- | Advanced variant of 'liftMemFunc' that uses the 'AllocConvention' to
call the guest's @alloc@ export for output buffer management.

This is the production-grade approach: the guest controls the heap, and the
host allocates an output buffer via @alloc@ before writing the result. The
'VM' reference is needed to call back into the guest's @alloc@ export.

The generated host function signature is @[I32, I32] -> [I64]@ (same as
'liftMemFunc').
-}
liftMemFuncWith ::
  forall a b.
  (WasmMemory a, WasmMemory b) =>
  -- | The VM (used to call the guest's @alloc@ export)
  VM ->
  -- | Module name (identifies which module's @alloc@ to call)
  String ->
  -- | Allocation convention (names the @alloc@ / @dealloc@ exports)
  AllocConvention ->
  (a -> IO (Either WasmEdgeError b)) ->
  (CallingFrame -> [WasmVal] -> IO (WasmResult [WasmVal]))
liftMemFuncWith vm modName conv f frame args =
  case args of
    [WasmI32 inPtr, WasmI32 inLen] -> do
      memResult <- callingFrameMemory frame
      case memResult of
        Left err -> pure (Left err)
        Right mem -> do
          peekResult <- wasmPeek mem (fromIntegral inPtr) (fromIntegral inLen)
          case peekResult of
            Left err -> pure (Left err)
            Right a -> do
              result <- f a
              case result of
                Left err -> pure (Left err)
                Right b -> do
                  -- Serialise b to find the required output size
                  pokeResult <- wasmPoke mem 0 b
                  case pokeResult of
                    Left _ ->
                      -- Fallback: poke at scratch region if size-probe fails
                      liftMemFunc f frame args
                    Right outputLen -> do
                      -- Allocate a proper guest buffer for the output
                      let modT = T.pack modName
                      allocResult <- wasmAlloc vm modT conv outputLen
                      case allocResult of
                        Left _ ->
                          -- Fallback to scratch region if alloc fails
                          liftMemFunc f frame args
                        Right outPtr -> do
                          pokeResult2 <- wasmPoke mem outPtr b
                          case pokeResult2 of
                            Left err -> pure (Left err)
                            Right outLen ->
                              let packed :: Word64
                                  packed =
                                    (fromIntegral outPtr `shiftL` 32)
                                      .|. fromIntegral outLen
                               in pure (Right [WasmI64 (fromIntegral packed)])
    _ ->
      pure
        ( Left
            ( WasmEdgeError
                ErrCategory_UserDefined
                31
                "liftMemFuncWith: expected exactly two i32 arguments (ptr, len)"
            )
        )