defmodule Wasmex.Pipe do
@moduledoc ~S"""
A Pipe is a memory buffer that can be used in exchange for a Wasm file.
Pipes have a read and write position which can be set using `seek/2`.
## Example
Pipes can be written to and read from:
iex> {:ok, pipe} = Wasmex.Pipe.new()
iex> Wasmex.Pipe.write(pipe, "hello")
{:ok, 5}
iex> Wasmex.Pipe.seek(pipe, 0)
iex> Wasmex.Pipe.read(pipe)
"hello"
They can be used to capture stdout/stdin/stderr of WASI programs:
iex> {:ok, stdin} = Wasmex.Pipe.new()
iex> {:ok, stdout} = Wasmex.Pipe.new()
iex> {:ok, stderr} = Wasmex.Pipe.new()
iex> Wasmex.Store.new_wasi(%Wasmex.Wasi.WasiOptions{
...> stdin: stdin,
...> stdout: stdout,
...> stderr: stderr,
...> })
"""
@type t :: %__MODULE__{
resource: binary(),
reference: reference()
}
defstruct resource: nil,
# The actual NIF pipe resource.
# Normally the compiler will happily do stuff like inlining the
# resource in attributes. This will convert the resource into an
# empty binary with no warning. This will make that harder to
# accidentally do.
reference: nil
def __wrap_resource__(resource) do
%__MODULE__{
resource: resource,
reference: make_ref()
}
end
@doc ~S"""
Creates and returns a new Pipe.
## Example
iex> {:ok, %Pipe{}} = Wasmex.Pipe.new()
"""
@spec new() :: {:error, reason :: binary()} | {:ok, __MODULE__.t()}
def new() do
case Wasmex.Native.pipe_new() do
{:error, err} -> {:error, err}
resource -> {:ok, __wrap_resource__(resource)}
end
end
@doc ~S"""
Returns the current size of the Pipe in bytes.
## Example
iex> {:ok, pipe} = Wasmex.Pipe.new()
iex> Wasmex.Pipe.size(pipe)
0
iex> Wasmex.Pipe.write(pipe, "hello")
iex> Wasmex.Pipe.size(pipe)
5
"""
@spec size(__MODULE__.t()) :: integer()
def size(%__MODULE__{resource: resource}) do
Wasmex.Native.pipe_size(resource)
end
@doc ~S"""
Sets the read/write position of the Pipe to the given position.
The position is given as a number of bytes from the start of the Pipe.
## Example
iex> {:ok, pipe} = Wasmex.Pipe.new()
iex> Wasmex.Pipe.write(pipe, "hello")
iex> Wasmex.Pipe.seek(pipe, 0)
:ok
iex> Wasmex.Pipe.read(pipe)
"hello"
"""
@spec seek(__MODULE__.t(), integer()) :: :ok | :error
def seek(%__MODULE__{resource: resource}, pos_from_start) do
Wasmex.Native.pipe_seek(resource, pos_from_start)
end
@doc ~S"""
Reads all available bytes from the Pipe and returns them as a binary.
This function does not block if there are no bytes available.
Reading starts at the current read position, see `seek/2`, and forwards the read position to the end of the Pipe.
The read bytes are not erased and can be read again after seeking back.
## Example
iex> {:ok, pipe} = Wasmex.Pipe.new()
iex> Wasmex.Pipe.write(pipe, "hello")
iex> Wasmex.Pipe.read(pipe) # current position is at EOL, nothing more to read
""
iex> Wasmex.Pipe.seek(pipe, 0)
iex> Wasmex.Pipe.read(pipe)
"hello"
iex> Wasmex.Pipe.seek(pipe, 3)
iex> Wasmex.Pipe.read(pipe)
"lo"
iex> Wasmex.Pipe.read(pipe)
""
"""
@spec read(__MODULE__.t()) :: binary()
def read(%__MODULE__{resource: resource}) do
Wasmex.Native.pipe_read_binary(resource)
end
@doc ~S"""
Writes the given binary into the pipe.
Writing starts at the current write position, see `seek/2`, and forwards it.
## Example
iex> {:ok, pipe} = Wasmex.Pipe.new()
iex> Wasmex.Pipe.write(pipe, "hello")
{:ok, 5}
iex> Wasmex.Pipe.seek(pipe, 0)
iex> Wasmex.Pipe.read(pipe)
"hello"
"""
@spec write(__MODULE__.t(), binary()) :: {:ok, integer()} | :error
def write(%__MODULE__{resource: resource}, binary) do
Wasmex.Native.pipe_write_binary(resource, binary)
end
end
defimpl Inspect, for: Wasmex.Pipe do
import Inspect.Algebra
def inspect(dict, opts) do
concat(["#Wasmex.Pipe<", to_doc(dict.reference, opts), ">"])
end
end