From 6ae89f66af3f896c2c3af1f6e6a57bfcfaa2f686 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 2 May 2026 19:00:02 +0200 Subject: [PATCH 1/3] Fix and change socket for a platform such as WASI - Handle spurious select wakeups - Allow linger and reuseaddr as no-ops if unsupported - Add nif_select_write/2 to strengthen send/sendto - Update tests to run when there is no inet driver - Fix socket operations to return {error, closed} when the fd is closed Signed-off-by: Paul Guyot --- CHANGELOG.md | 11 ++ libs/estdlib/src/gen_tcp_socket.erl | 67 ++++--- libs/estdlib/src/gen_udp_socket.erl | 79 +++++++-- libs/estdlib/src/socket.erl | 173 +++++++++++++++--- src/libAtomVM/otp_socket.c | 234 ++++++++++++++++++------- src/libAtomVM/resources.c | 10 ++ tests/libs/estdlib/test_gen_tcp.erl | 94 ++++++---- tests/libs/estdlib/test_gen_udp.erl | 12 +- tests/libs/estdlib/test_inet.erl | 14 +- tests/libs/estdlib/test_tcp_socket.erl | 18 ++ 10 files changed, 551 insertions(+), 161 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0b55f2c2c..0986b38ac6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 longer lines return `{error, {parser, {line_too_long, Prefix}}}` with the first 128 bytes of the offending line. Callers whose upstream servers emit unusually large headers must account for this limit +- `socket:setopt(Socket, {socket, reuseaddr}, _Value)` and + `socket:setopt(Socket, {socket, linger}, _Value)` are accepted on platforms + that do not implement the options ### Removed - Removed `ahttp_client` support for obsolete line folding (RFC 9112 ยง5.2); folded header and @@ -56,6 +59,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed a bug in `supervisor` handling of failing child - Fixed two bugs related to closing fds in `atomvm:subprocess/4` - Fixed `erlang:localtime/1` memory leak, use-after-free, and TZ restore bugs on newlib/picolibc +- `socket:recv/3` and `socket:recvfrom/3` now tolerate spurious select wakeups +- `gen_tcp_socket` and `gen_udp_socket` no longer leak `pending_selects` entries +- `socket:send/2` and `socket:sendto/3` now wait via select +- `socket:bind`, `socket:listen`, `socket:accept`, `socket:connect`, + `socket:shutdown`, `socket:sockname`, `socket:peername`, `socket:recv`, + `socket:recvfrom`, `socket:send` and `socket:sendto` operating on a fd + that was already closed by another process now return `{error, closed}` + rather than `{error, ebadf}` ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/libs/estdlib/src/gen_tcp_socket.erl b/libs/estdlib/src/gen_tcp_socket.erl index b17e5059d5..80c370f84b 100644 --- a/libs/estdlib/src/gen_tcp_socket.erl +++ b/libs/estdlib/src/gen_tcp_socket.erl @@ -350,12 +350,12 @@ handle_cast(_Request, State) -> {noreply, State}. %% @hidden -handle_info({'$socket', _Socket, select, Ref}, State) -> +handle_info({'$socket', Socket, select, Ref}, State) -> %% TODO cancel timer case maps:get(Ref, State#state.pending_selects, undefined) of undefined -> ?LOG_WARNING("Unable to find select ref ~p in pending selects", [Ref]), - %% select_stop? + socket:nif_select_stop(Socket), {noreply, State}; {accept, From, AcceptingProc, _Timeout} -> ?LOG_INFO("Select ready for read on accept"), @@ -365,14 +365,11 @@ handle_info({'$socket', _Socket, select, Ref}, State) -> }}; active -> ?LOG_INFO("Select ready for read on active recv"), - NewState = handle_active_recv(State), + NewState = handle_active_recv(State, Ref), {noreply, NewState}; {passive, From, Length, Timeout} -> ?LOG_INFO("Select ready for read on passive recv"), - NewState = handle_passive_recv(State, From, Length, Timeout), - {noreply, NewState#state{ - pending_selects = maps:remove(Ref, State#state.pending_selects) - }} + {noreply, handle_passive_recv(State, Ref, From, Length, Timeout)} end; handle_info({'$socket', Socket, abort, {Ref, closed}}, State) -> %% TODO cancel timer @@ -406,6 +403,7 @@ handle_info({timeout, Ref, From}, State) -> {noreply, State}; _ -> ?LOG_INFO("Select ref ~p in pending selects has timed out.", [Ref]), + socket:nif_select_stop(State#state.socket), gen_server:reply(From, {error, timeout}), {noreply, State#state{pending_selects = maps:remove(Ref, State#state.pending_selects)}} end; @@ -471,7 +469,7 @@ handle_accept(State, From, AcceptingProc) -> end. %% @private -handle_active_recv(State) -> +handle_active_recv(State, OldRef) -> Socket = State#state.socket, Length = maps:get(buffer, State#state.options, 0), WrappedSocket = {?GEN_TCP_MONIKER, self(), ?MODULE}, @@ -486,16 +484,17 @@ handle_active_recv(State) -> State#state.controlling_process ! {tcp, WrappedSocket, BinaryOrList}, %% start a new select - Ref = erlang:make_ref(), - case socket:nif_select_read(Socket, Ref) of + NewRef = erlang:make_ref(), + case socket:nif_select_read(Socket, NewRef) of ok -> - ?LOG_INFO("Started read select on ref=~p", [Ref]), - PendingSelects = State#state.pending_selects, - NewPendingSelects = maps:remove(Ref, PendingSelects), - State#state{pending_selects = NewPendingSelects#{Ref => active}}; + ?LOG_INFO("Started read select on ref=~p", [NewRef]), + PendingSelects = maps:remove(OldRef, State#state.pending_selects), + State#state{pending_selects = PendingSelects#{NewRef => active}}; Error -> - ?LOG_ERROR("Unable to start select on new ref=~p Error=~p", [Ref, Error]), - State + ?LOG_ERROR("Unable to start select on new ref=~p Error=~p", [NewRef, Error]), + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + } end; {closed, OtherRef} -> ?LOG_INFO("Socket was closed other ref=~p", [OtherRef]), @@ -515,6 +514,23 @@ handle_active_recv(State) -> WrappedSocket = {?GEN_TCP_MONIKER, self(), ?MODULE}, State#state.controlling_process ! {tcp_closed, WrappedSocket}, State; + {error, timeout} -> + %% Spurious select wakeup - re-arm and stay in active mode. + NewRef = erlang:make_ref(), + case socket:nif_select_read(Socket, NewRef) of + ok -> + PendingSelects = maps:remove(OldRef, State#state.pending_selects), + State#state{pending_selects = PendingSelects#{NewRef => active}}; + {error, _} = SelectError -> + socket:nif_select_stop(Socket), + ?LOG_ERROR("Unable to re-arm select after spurious wakeup Error=~p", [ + SelectError + ]), + State#state.controlling_process ! {tcp_error, WrappedSocket, SelectError}, + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + } + end; {error, _} = Error -> ?LOG_ERROR("Error on receive on pending select Error=~p", [Error]), %% CAUTION: internal API @@ -526,15 +542,16 @@ handle_active_recv(State) -> end. %% @private -handle_passive_recv(State, From, Length, _Timeout) -> +handle_passive_recv(State, Ref, From, Length, _Timeout) -> Socket = State#state.socket, + Pending = State#state.pending_selects, %% CAUTION: internal API case socket:nif_recv(Socket, Length) of {ok, Data} -> ?LOG_INFO("Received data len=~p", [erlang:byte_size(Data)]), BinaryOrList = maybe_encode_binary(State#state.options, Data), gen_server:reply(From, {ok, BinaryOrList}), - State; + State#state{pending_selects = maps:remove(Ref, Pending)}; {closed, OtherRef} -> ?LOG_INFO("Socket was closed other ref=~p", [OtherRef]), % socket was closed by another process @@ -544,13 +561,23 @@ handle_passive_recv(State, From, Length, _Timeout) -> % {closed, Ref} and {select, _, Ref, _} in the % queue gen_server:reply(From, {error, closed}), - State; + State#state{pending_selects = maps:remove(Ref, Pending)}; + {error, timeout} -> + %% Spurious select wakeup + case socket:nif_select_read(Socket, Ref) of + ok -> + State; + {error, _} = SelectError -> + socket:nif_select_stop(Socket), + gen_server:reply(From, SelectError), + State#state{pending_selects = maps:remove(Ref, Pending)} + end; {error, _} = Error -> ?LOG_ERROR("Error on receive from socket:nif_recv Error=~p", [Error]), socket:nif_select_stop(Socket), ?LOG_INFO("unable to receive on pending select~n"), gen_server:reply(From, Error), - State + State#state{pending_selects = maps:remove(Ref, Pending)} end. %% @private diff --git a/libs/estdlib/src/gen_udp_socket.erl b/libs/estdlib/src/gen_udp_socket.erl index c3a561e53e..71257758bd 100644 --- a/libs/estdlib/src/gen_udp_socket.erl +++ b/libs/estdlib/src/gen_udp_socket.erl @@ -242,19 +242,17 @@ handle_cast(_Request, State) -> {noreply, State}. %% @hidden -handle_info({'$socket', _Socket, select, Ref}, State) -> +handle_info({'$socket', Socket, select, Ref}, State) -> case maps:get(Ref, State#state.pending_selects, undefined) of undefined -> ?LOG_INFO("Unable to find select ref ~p in pending selects", [Ref]), + socket:nif_select_stop(Socket), {noreply, State}; active -> - NewState = handle_active_recvfrom(State), + NewState = handle_active_recvfrom(State, Ref), {noreply, NewState}; {passive, From, Length, Timeout} -> - NewState = handle_passive_recvfrom(State, From, Length, Timeout), - {noreply, NewState#state{ - pending_selects = maps:remove(Ref, State#state.pending_selects) - }} + {noreply, handle_passive_recvfrom(State, Ref, From, Length, Timeout)} end; handle_info({timeout, Ref, From}, State) -> case maps:get(Ref, State#state.pending_selects, undefined) of @@ -263,6 +261,7 @@ handle_info({timeout, Ref, From}, State) -> {noreply, State}; _ -> ?LOG_INFO("Select ref ~p in pending selects has timed out.", [Ref]), + socket:nif_select_stop(State#state.socket), gen_server:reply(From, {error, timeout}), {noreply, State#state{pending_selects = maps:remove(Ref, State#state.pending_selects)}} end; @@ -301,7 +300,7 @@ proplist_to_map([{K, V} | T], Accum) -> proplist_to_map(T, Accum#{K => V}). %% @private -handle_active_recvfrom(State) -> +handle_active_recvfrom(State, OldRef) -> Socket = State#state.socket, Length = maps:get(buffer, State#state.options, 0), ControllingProcess = State#state.controlling_process, @@ -315,14 +314,19 @@ handle_active_recvfrom(State) -> ControllingProcess ! {udp, WrappedSocket, Addr, Port, BinaryOrList}, %% start a new select - Ref = erlang:make_ref(), - case socket:nif_select_read(Socket, Ref) of + NewRef = erlang:make_ref(), + case socket:nif_select_read(Socket, NewRef) of ok -> - PendingSelects = State#state.pending_selects, - NewPendingSelects = maps:remove(Ref, PendingSelects), - State#state{pending_selects = NewPendingSelects#{Ref => active}}; - _ -> - State + PendingSelects = maps:remove(OldRef, State#state.pending_selects), + State#state{pending_selects = PendingSelects#{NewRef => active}}; + {error, _} = SelectError -> + ?LOG_ERROR("Unable to re-arm select on new ref=~p Error=~p", [ + NewRef, SelectError + ]), + State#state.controlling_process ! {udp_error, WrappedSocket, SelectError}, + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + } end; {closed, _Ref} -> % socket was closed by another process @@ -332,18 +336,41 @@ handle_active_recvfrom(State) -> % {closed, Ref} and {select, _, Ref, _} in the % queue State#state.controlling_process ! {udp_closed, WrappedSocket}, - State; + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + }; + {error, timeout} -> + %% Spurious select wakeup - re-arm and stay in active mode. + NewRef = erlang:make_ref(), + case socket:nif_select_read(Socket, NewRef) of + ok -> + PendingSelects = maps:remove(OldRef, State#state.pending_selects), + State#state{pending_selects = PendingSelects#{NewRef => active}}; + {error, _} = SelectError -> + %% CAUTION: internal API + socket:nif_select_stop(Socket), + ?LOG_ERROR("Unable to re-arm select after spurious wakeup Error=~p", [ + SelectError + ]), + State#state.controlling_process ! {udp_error, WrappedSocket, SelectError}, + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + } + end; {error, _} = E -> %% CAUTION: internal API socket:nif_select_stop(Socket), ?LOG_INFO("unable to receive on pending select~n"), State#state.controlling_process ! {udp_error, WrappedSocket, E}, - State + State#state{ + pending_selects = maps:remove(OldRef, State#state.pending_selects) + } end. %% @private -handle_passive_recvfrom(State, From, Length, _Timeout) -> +handle_passive_recvfrom(State, Ref, From, Length, _Timeout) -> Socket = State#state.socket, + Pending = State#state.pending_selects, %% CAUTION: internal API case socket:nif_recvfrom(Socket, Length) of {ok, {Address, Data}} -> @@ -351,12 +378,26 @@ handle_passive_recvfrom(State, From, Length, _Timeout) -> #{addr := Addr, port := Port} = Address, % WrappedSocket = {?GEN_UDP_MONIKER, self(), ?MODULE}, gen_server:reply(From, {ok, {Addr, Port, BinaryOrList}}), - State; + State#state{pending_selects = maps:remove(Ref, Pending)}; + {error, timeout} -> + %% Spurious select wakeup - re-arm with the same Ref and stay + %% pending. The user-supplied timeout timer keeps running. + case socket:nif_select_read(Socket, Ref) of + ok -> + State; + {error, _} = SelectError -> + %% CAUTION: internal API. Tear down the underlying + %% select before we drop the pending entry, mirroring + %% the {error, _} branch below. + socket:nif_select_stop(Socket), + gen_server:reply(From, SelectError), + State#state{pending_selects = maps:remove(Ref, Pending)} + end; {error, _} = E -> socket:nif_select_stop(Socket), ?LOG_INFO("unable to receive on pending select~n"), gen_server:reply(From, E), - State + State#state{pending_selects = maps:remove(Ref, Pending)} end. %% @private diff --git a/libs/estdlib/src/socket.erl b/libs/estdlib/src/socket.erl index 810cf563c2..82538cf44d 100644 --- a/libs/estdlib/src/socket.erl +++ b/libs/estdlib/src/socket.erl @@ -46,6 +46,7 @@ %% internal nifs -export([ nif_select_read/2, + nif_select_write/2, nif_accept/1, nif_recv/2, nif_recvfrom/2, @@ -267,21 +268,18 @@ accept0(Socket, Timeout) -> ok -> receive {'$socket', Socket, select, Ref} -> - case ?MODULE:nif_accept(Socket) of - {error, _} = E -> - ?MODULE:nif_select_stop(Socket), - E; - {ok, _Socket} = Reply -> - Reply - end; + ?MODULE:nif_select_stop(Socket), + ?MODULE:nif_accept(Socket); {'$socket', Socket, abort, {Ref, closed}} -> % socket was closed by another process % TODO: we need to handle: % (a) SELECT_STOP being scheduled % (b) flush of messages as we can have both in the % queue + ?MODULE:nif_select_stop(Socket), {error, closed} after Timeout -> + ?MODULE:nif_select_stop(Socket), {error, timeout} end; {error, _Reason} = Error -> @@ -375,29 +373,51 @@ recv0_noselect(Socket, Length) -> end. recv0(Socket, Length, Timeout) -> + Deadline = + case Timeout of + infinity -> infinity; + _ -> erlang:system_time(millisecond) + Timeout + end, + recv0_loop(Socket, Length, Deadline). + +recv0_loop(Socket, Length, Deadline) -> Ref = erlang:make_ref(), case ?MODULE:nif_select_read(Socket, Ref) of ok -> + Remaining = recv0_remaining(Deadline), receive {'$socket', Socket, select, Ref} -> + ?MODULE:nif_select_stop(Socket), case ?MODULE:nif_recv(Socket, Length) of - {error, _} = E -> - ?MODULE:nif_select_stop(Socket), - E; - {ok, Data} -> - {ok, Data} + {error, Reason} when Reason =:= timeout orelse Reason =:= eagain -> + %% Handle spurious select wakeup: some platforms + %% (wasi-libc) require that we tear down and + %% re-arm the select. + case recv0_remaining(Deadline) of + Done when Done =< 0, Deadline =/= infinity -> + {error, timeout}; + _ -> + recv0_loop(Socket, Length, Deadline) + end; + Result -> + Result end; {'$socket', Socket, abort, {Ref, closed}} -> % socket was closed by another process % TODO: see above in accept/2 + ?MODULE:nif_select_stop(Socket), {error, closed} - after Timeout -> + after Remaining -> + ?MODULE:nif_select_stop(Socket), {error, timeout} end; {error, _Reason} = Error -> Error end. +recv0_remaining(infinity) -> infinity; +recv0_remaining(Deadline) -> max(0, Deadline - erlang:system_time(millisecond)). + recv0_nowait(Socket, Length, Ref) -> case ?MODULE:nif_recv(Socket, Length) of {error, Reason} when Reason =:= timeout orelse Reason =:= eagain -> @@ -431,9 +451,20 @@ recv0_r(Socket, Length, Timeout, EndQuery, Acc) -> ok -> receive {'$socket', Socket, select, Ref} -> + ?MODULE:nif_select_stop(Socket), case ?MODULE:nif_recv(Socket, Length) of + {error, Reason} when Reason =:= timeout orelse Reason =:= eagain -> + %% Handle spurious select wakeup + case recv0_r_remaining(Timeout, EndQuery) of + Done when Done =< 0, Timeout =/= infinity -> + case Acc of + [] -> {error, timeout}; + _ -> {error, {timeout, list_to_binary(lists:reverse(Acc))}} + end; + NewTimeout -> + recv0_r(Socket, Length, NewTimeout, EndQuery, Acc) + end; {error, _} = E -> - ?MODULE:nif_select_stop(Socket), E; {ok, Data} -> NewAcc = [Data | Acc], @@ -442,19 +473,17 @@ recv0_r(Socket, Length, Timeout, EndQuery, Acc) -> 0 -> {ok, list_to_binary(lists:reverse(NewAcc))}; _ -> - NewTimeout = - case Timeout of - infinity -> infinity; - _ -> EndQuery - erlang:system_time(millisecond) - end, + NewTimeout = recv0_r_remaining(Timeout, EndQuery), recv0_r(Socket, Remaining, NewTimeout, EndQuery, NewAcc) end end; {'$socket', Socket, abort, {Ref, closed}} -> % socket was closed by another process % TODO: see above in accept/2 + ?MODULE:nif_select_stop(Socket), {error, closed} after Timeout -> + ?MODULE:nif_select_stop(Socket), case Acc of [] -> {error, timeout}; _ -> {error, {timeout, list_to_binary(lists:reverse(Acc))}} @@ -464,6 +493,9 @@ recv0_r(Socket, Length, Timeout, EndQuery, Acc) -> Error end. +recv0_r_remaining(infinity, _EndQuery) -> infinity; +recv0_r_remaining(_Timeout, EndQuery) -> max(0, EndQuery - erlang:system_time(millisecond)). + %%----------------------------------------------------------------------------- %% @equiv socket:recvfrom(Socket, 0) %% @end @@ -530,23 +562,40 @@ recvfrom0_noselect(Socket, Length) -> end. recvfrom0(Socket, Length, Timeout) -> + Deadline = + case Timeout of + infinity -> infinity; + _ -> erlang:system_time(millisecond) + Timeout + end, + recvfrom0_loop(Socket, Length, Deadline). + +recvfrom0_loop(Socket, Length, Deadline) -> Ref = erlang:make_ref(), case ?MODULE:nif_select_read(Socket, Ref) of ok -> + Remaining = recv0_remaining(Deadline), receive {'$socket', Socket, select, Ref} -> + ?MODULE:nif_select_stop(Socket), case ?MODULE:nif_recvfrom(Socket, Length) of - {error, _} = E -> - ?MODULE:nif_select_stop(Socket), - E; - {ok, {_Address, _Data}} = Reply -> - Reply + {error, Reason} when Reason =:= timeout orelse Reason =:= eagain -> + %% Handle spurious select wakeup + case recv0_remaining(Deadline) of + Done when Done =< 0, Deadline =/= infinity -> + {error, timeout}; + _ -> + recvfrom0_loop(Socket, Length, Deadline) + end; + Result -> + Result end; {'$socket', Socket, abort, {Ref, closed}} -> % socket was closed by another process % TODO: see above in accept/2 + ?MODULE:nif_select_stop(Socket), {error, closed} - after Timeout -> + after Remaining -> + ?MODULE:nif_select_stop(Socket), {error, timeout} end; {error, _Reason} = Error -> @@ -579,6 +628,10 @@ recvfrom0_nowait(Socket, Length, Ref) -> %% intended recipient, and the data may not even have been sent %% over the network. %% +%% If the underlying non-blocking send returns `eagain' (kernel +%% send buffer full), this function waits via select until the +%% socket is writable again, then retries. +%% %% Example: %% %% `ok = socket:send(ConnectedSocket, Data)' @@ -587,9 +640,35 @@ recvfrom0_nowait(Socket, Length, Ref) -> -spec send(Socket :: socket(), Data :: iodata()) -> ok | {ok, Rest :: binary()} | {error, Reason :: term()}. send(Socket, Data) when is_binary(Data) -> - ?MODULE:nif_send(Socket, Data); + send0(Socket, Data); send(Socket, Data) -> - ?MODULE:nif_send(Socket, erlang:iolist_to_binary(Data)). + send0(Socket, erlang:iolist_to_binary(Data)). + +send0(Socket, Data) -> + case ?MODULE:nif_send(Socket, Data) of + {error, eagain} -> + send_wait(Socket, Data); + Result -> + Result + end. + +%% Wait via select for the socket to become writable, then retry the send. +send_wait(Socket, Data) -> + Ref = erlang:make_ref(), + case ?MODULE:nif_select_write(Socket, Ref) of + ok -> + receive + {'$socket', Socket, select, Ref} -> + ?MODULE:nif_select_stop(Socket), + send0(Socket, Data); + {'$socket', Socket, abort, {_AnyRef, closed}} -> + %% Accept abort for any left over ref + ?MODULE:nif_select_stop(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + Error + end. %%----------------------------------------------------------------------------- %% @param Socket the socket @@ -603,6 +682,10 @@ send(Socket, Data) -> %% intended recipient, and the data may not even have been sent %% over the network. %% +%% As with {@link send/2}, the call waits via select for the +%% socket to become writable when the underlying NIF reports +%% `eagain'. +%% %% Example: %% %% `ok = socket:sendto(ConnectedSocket, Data, Dest)' @@ -611,9 +694,34 @@ send(Socket, Data) -> -spec sendto(Socket :: socket(), Data :: iodata(), Dest :: sockaddr()) -> ok | {ok, Rest :: binary()} | {error, Reason :: term()}. sendto(Socket, Data, Dest) when is_binary(Data) -> - ?MODULE:nif_sendto(Socket, Data, Dest); + sendto0(Socket, Data, Dest); sendto(Socket, Data, Dest) -> - ?MODULE:nif_sendto(Socket, erlang:iolist_to_binary(Data), Dest). + sendto0(Socket, erlang:iolist_to_binary(Data), Dest). + +sendto0(Socket, Data, Dest) -> + case ?MODULE:nif_sendto(Socket, Data, Dest) of + {error, eagain} -> + sendto_wait(Socket, Data, Dest); + Result -> + Result + end. + +sendto_wait(Socket, Data, Dest) -> + Ref = erlang:make_ref(), + case ?MODULE:nif_select_write(Socket, Ref) of + ok -> + receive + {'$socket', Socket, select, Ref} -> + ?MODULE:nif_select_stop(Socket), + sendto0(Socket, Data, Dest); + {'$socket', Socket, abort, {_AnyRef, closed}} -> + %% Accept abort for any left over ref + ?MODULE:nif_select_stop(Socket), + {error, closed} + end; + {error, _Reason} = Error -> + Error + end. %%----------------------------------------------------------------------------- %% @param Socket the socket @@ -653,6 +761,9 @@ getopt(_Socket, _SocketOption) -> %% `{ip, add_membership}'`ip_mreq()' %% %% +%% Some platforms (wasi) do not implement linger or reuseaddr. The +%% options are silently accepted for compatibility. +%% %% Example: %% %% `ok = socket:setopt(ListeningSocket, {socket, reuseaddr}, true)' @@ -708,6 +819,10 @@ shutdown(_Socket, _How) -> nif_select_read(_Socket, _Ref) -> erlang:nif_error(undefined). +%% @private +nif_select_write(_Socket, _Ref) -> + erlang:nif_error(undefined). + %% @private nif_accept(_Socket) -> erlang:nif_error(undefined). diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 46729dbb3d..605e72b044 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -67,6 +67,23 @@ #define TAG "otp_socket" +#if OTP_SOCKET_BSD +static inline bool errno_is_peer_close(int err) +{ + if (err == ECONNRESET) { + return true; + } +#ifdef __wasip2__ + // On wasm32-wasip2, wasi-libc maps wasi:sockets@0.2.x stream-end to + // EIO on read and EPIPE on write + if (err == EIO || err == EPIPE) { + return true; + } +#endif + return false; +} +#endif + // Check some LWIP options #if OTP_SOCKET_LWIP #if !TCP_LISTEN_BACKLOG @@ -201,7 +218,7 @@ static const char *const reuseaddr_atom = ATOM_STR("\x9", "reuseaddr"); static const char *const type_atom = ATOM_STR("\x4", "type"); static const char *const add_membership_atom = ATOM_STR("\xE", "add_membership"); -#define CLOSED_FD 0 +#define CLOSED_FD (-1) #define ADDR_ATOM globalcontext_make_atom(global, addr_atom) #define CLOSE_INTERNAL_ATOM globalcontext_make_atom(global, close_internal_atom) @@ -580,9 +597,8 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) #endif #if OTP_SOCKET_BSD rsrc_obj->fd = socket(domain, type, protocol); - if (UNLIKELY(rsrc_obj->fd == -1 || rsrc_obj->fd == CLOSED_FD)) { + if (UNLIKELY(rsrc_obj->fd == CLOSED_FD)) { AVM_LOGE(TAG, "Failed to initialize socket."); - rsrc_obj->fd = CLOSED_FD; enif_release_resource(rsrc_obj); return make_errno_tuple(ctx); } else { @@ -739,7 +755,7 @@ static term nif_socket_close(Context *ctx, int argc, term argv[]) SMP_RWLOCK_WRLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd) { + if (rsrc_obj->fd != CLOSED_FD) { // In POSIX with BSD sockets, if a file descriptor being monitored by // select() is closed in another thread, the result is unspecified. // select may continue. @@ -1141,6 +1157,76 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) return OK_ATOM; } +static term nif_socket_select_write(Context *ctx, int argc, term argv[]) +{ + TRACE("nif_socket_select_write\n"); + + UNUSED(argc); + + VALIDATE_VALUE(argv[0], term_is_otp_socket); + + term select_ref_term = argv[1]; + if (select_ref_term != UNDEFINED_ATOM) { + VALIDATE_VALUE(select_ref_term, term_is_local_reference); + } + struct SocketResource *rsrc_obj; + if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { + RAISE_ERROR(BADARG_ATOM); + } + + struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); + SMP_RWLOCK_WRLOCK(rsrc_obj->socket_lock); + + ErlNifEnv *env = erl_nif_env_from_context(ctx); + if (rsrc_obj->selecting_process_id != ctx->process_id && rsrc_obj->selecting_process_id != INVALID_PROCESS_ID) { + if (LIKELY(enif_demonitor_process(env, rsrc_obj, &rsrc_obj->selecting_process_monitor) == 0)) { + refc_binary_decrement_refcount(rsrc_refc, ctx->global); + } + rsrc_obj->selecting_process_id = INVALID_PROCESS_ID; + } + if (rsrc_obj->selecting_process_id != ctx->process_id) { + if (UNLIKELY(enif_monitor_process(env, rsrc_obj, &ctx->process_id, &rsrc_obj->selecting_process_monitor) != 0)) { + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + RAISE_ERROR(NOPROC_ATOM); + } + refc_binary_increment_refcount(rsrc_refc); + rsrc_obj->selecting_process_id = ctx->process_id; + } + + rsrc_obj->select_ref_ticks = (select_ref_term == UNDEFINED_ATOM) ? 0 : term_to_ref_ticks(select_ref_term); + +#if OTP_SOCKET_BSD + TRACE("rsrc_obj->fd=%i\n", (int) rsrc_obj->fd); + + if (rsrc_obj->fd == CLOSED_FD) { + send_closed_notification(ctx, argv[0], ctx->process_id, rsrc_obj); + } else { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, SOCKET_MAKE_SELECT_NOTIFICATION_SIZE, 2, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + term notification = socket_make_select_notification(rsrc_obj, &ctx->heap); + if (UNLIKELY(enif_select_write(erl_nif_env_from_context(ctx), rsrc_obj->fd, rsrc_obj, &ctx->process_id, notification, NULL) < 0)) { + if (LIKELY(enif_demonitor_process(env, rsrc_obj, &rsrc_obj->selecting_process_monitor) == 0)) { + refc_binary_decrement_refcount(rsrc_refc, ctx->global); + } + rsrc_obj->selecting_process_id = INVALID_PROCESS_ID; + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + RAISE_ERROR(BADARG_ATOM); + } + } +#elif OTP_SOCKET_LWIP + // Fire an immediate notification and let the caller retry. + LWIP_BEGIN(); + select_event_send_notification_from_nif(rsrc_obj, ctx); + LWIP_END(); +#endif + + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + return OK_ATOM; +} + static term nif_socket_select_stop(Context *ctx, int argc, term argv[]) { TRACE("nif_socket_stop\n"); @@ -1162,7 +1248,8 @@ static term nif_socket_select_stop(Context *ctx, int argc, term argv[]) rsrc_obj->selecting_process_id = INVALID_PROCESS_ID; } #if OTP_SOCKET_BSD - if (UNLIKELY(enif_select(erl_nif_env_from_context(ctx), rsrc_obj->fd, ERL_NIF_SELECT_STOP, rsrc_obj, NULL, term_nil()) < 0)) { + int stop_res = enif_select(erl_nif_env_from_context(ctx), rsrc_obj->fd, ERL_NIF_SELECT_STOP, rsrc_obj, NULL, term_nil()); + if (UNLIKELY(stop_res < 0 && stop_res != ERL_NIF_SELECT_INVALID_EVENT)) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); RAISE_ERROR(BADARG_ATOM); } @@ -1199,7 +1286,7 @@ static term nif_socket_getopt(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { #endif @@ -1288,7 +1375,7 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { #endif @@ -1311,6 +1398,10 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) int res = setsockopt(rsrc_obj->fd, SOL_SOCKET, SO_REUSEADDR, &option_value, sizeof(int)); SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); if (UNLIKELY(res != 0)) { + // SO_REUSEADDR may be unavailable (ENOPROTOOPT) + if (errno == ENOPROTOOPT) { + return OK_ATOM; + } return make_errno_tuple(ctx); } else { return OK_ATOM; @@ -1340,6 +1431,7 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) VALIDATE_VALUE(linger, term_is_integer); #if OTP_SOCKET_BSD +#ifdef SO_LINGER struct linger sl; sl.l_onoff = (onoff == TRUE_ATOM); sl.l_linger = term_to_int(linger); @@ -1350,6 +1442,12 @@ static term nif_socket_setopt(Context *ctx, int argc, term argv[]) } else { return OK_ATOM; } +#else + // Silently accept SO_LINGER if underlying doesn't know about it + UNUSED(onoff); + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + return OK_ATOM; +#endif #elif OTP_SOCKET_LWIP rsrc_obj->linger_on = (onoff == TRUE_ATOM); rsrc_obj->linger_sec = term_to_int(linger); @@ -1496,12 +1594,12 @@ static term nif_socket_sockname(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { #endif SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #if OTP_SOCKET_BSD @@ -1568,14 +1666,14 @@ static term nif_socket_peername(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (rsrc_obj->socket_state & SocketStateUDP) { // TODO: handle "connected" UDP sockets @@ -1646,14 +1744,14 @@ static term nif_socket_bind(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD TRACE("rsrc_obj->fd=%i\n", (int) rsrc_obj->fd); - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #endif @@ -1743,8 +1841,6 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) TRACE("nif_socket_listen\n"); UNUSED(argc); - GlobalContext *global = ctx->global; - VALIDATE_VALUE(argv[0], term_is_otp_socket); VALIDATE_VALUE(argv[1], term_is_integer); @@ -1755,18 +1851,18 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (rsrc_obj->socket_state & SocketStateUDP) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EPROTOTYPE, global), ctx); + return make_error_tuple(posix_errno_to_term(EPROTOTYPE, ctx->global), ctx); } #endif @@ -1846,14 +1942,14 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state & SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (rsrc_obj->socket_state & SocketStateUDP) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); @@ -1876,7 +1972,7 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) } int fd = accept(rsrc_obj->fd, (struct sockaddr *) &clientaddr, &clientlen); SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - if (UNLIKELY(fd == -1 || fd == CLOSED_FD)) { + if (UNLIKELY(fd == CLOSED_FD)) { int err = errno; if (err != EAGAIN) { AVM_LOGI(TAG, "Unable to accept on socket %i. errno=%i", rsrc_obj->fd, (int) err); @@ -2133,7 +2229,7 @@ static term nif_socket_recv_with_peek(Context *ctx, term resource_term, struct S if (res < 0) { if (errno == EAGAIN) { return make_error_tuple(TIMEOUT_ATOM, ctx); - } else if (errno == ECONNRESET) { + } else if (errno_is_peer_close(errno)) { TRACE("Peer closed connection.\n"); return make_error_tuple(CLOSED_ATOM, ctx); } @@ -2216,7 +2312,7 @@ static term nif_socket_recv_without_peek(Context *ctx, term resource_term, struc if (res < 0) { int err = errno; - if (err == ECONNRESET) { + if (errno_is_peer_close(err)) { TRACE("Peer closed connection.\n"); return make_error_tuple(CLOSED_ATOM, ctx); } else if (err == EAGAIN) { @@ -2364,14 +2460,14 @@ static term nif_socket_recv_internal(Context *ctx, term argv[], bool is_recvfrom SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, ctx->global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state & SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, ctx->global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (rsrc_obj->socket_state & SocketStateListening) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); @@ -2412,9 +2508,12 @@ static term nif_socket_recvfrom(Context *ctx, int argc, term argv[]) // // send/sendto // -static ssize_t do_socket_send(struct SocketResource *rsrc_obj, const uint8_t *buf, size_t len, term dest) +static ssize_t do_socket_send(struct SocketResource *rsrc_obj, const uint8_t *buf, size_t len, term dest, int *out_errno) { ssize_t sent_data = -1; + if (out_errno != NULL) { + *out_errno = 0; + } #if OTP_SOCKET_BSD if (!term_is_invalid_term(dest)) { struct RefcBinary *rsrc_refc = refc_binary_from_data(rsrc_obj); @@ -2439,10 +2538,14 @@ static ssize_t do_socket_send(struct SocketResource *rsrc_obj, const uint8_t *bu sent_data = send(rsrc_obj->fd, buf, len, 0); } if (sent_data < 0) { - if (errno == EAGAIN || errno == EWOULDBLOCK) { + int err = errno; + if (out_errno != NULL) { + *out_errno = err; + } + if (err == EAGAIN || err == EWOULDBLOCK) { return SocketWouldBlock; } - if (errno == EBADF || errno == ECONNRESET) { + if (errno_is_peer_close(err)) { return SocketClosed; } return SocketOtherError; @@ -2521,7 +2624,7 @@ static ssize_t do_socket_send(struct SocketResource *rsrc_obj, const uint8_t *bu ssize_t socket_send(struct SocketResource *rsrc_obj, const uint8_t *buf, size_t len, term dest) { SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); - ssize_t result = do_socket_send(rsrc_obj, buf, len, dest); + ssize_t result = do_socket_send(rsrc_obj, buf, len, dest, NULL); SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); return result; } @@ -2543,14 +2646,14 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i SMP_RWLOCK_RDLOCK(rsrc_obj->socket_lock); #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (rsrc_obj->socket_state & SocketStateListening) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); @@ -2567,35 +2670,38 @@ static term nif_socket_send_internal(Context *ctx, int argc, term argv[], bool i const uint8_t *buf = (const uint8_t *) term_binary_data(data); size_t len = term_binary_size(data); - ssize_t sent_data = do_socket_send(rsrc_obj, buf, len, dest); + if (len == 0) { + SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); + return OK_ATOM; + } + + int send_errno = 0; + ssize_t sent_data = do_socket_send(rsrc_obj, buf, len, dest, &send_errno); SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); // {ok, RestData} | {error, Reason} - size_t rest_len = len - sent_data; - if (rest_len == 0) { - return OK_ATOM; - } else if (sent_data > 0) { - + if (sent_data > 0) { + size_t rest_len = len - (size_t) sent_data; + if (rest_len == 0) { + return OK_ATOM; + } size_t requested_size = term_sub_binary_heap_size(data, rest_len); if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2) + requested_size, 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - term rest = term_maybe_create_sub_binary(data, sent_data, rest_len, &ctx->heap, global); return port_create_tuple2(ctx, OK_ATOM, rest); - - } else if (sent_data == 0) { - if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(2), 1, &data, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - - return port_create_tuple2(ctx, OK_ATOM, data); - } else { - TRACE("Unable to send data: res=%zi.\n", sent_data); - return make_error_tuple(CLOSED_ATOM, ctx); + } + TRACE("Unable to send data: res=%zi.\n", sent_data); + switch (sent_data) { + case SocketWouldBlock: + return make_error_tuple(posix_errno_to_term(EAGAIN, global), ctx); + case SocketClosed: + return make_error_tuple(CLOSED_ATOM, ctx); + default: + return make_error_tuple(posix_errno_to_term(send_errno != 0 ? send_errno : EINVAL, global), ctx); } } @@ -2720,14 +2826,14 @@ static term nif_socket_connect(Context *ctx, int argc, term argv[]) } #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } if (((rsrc_obj->socket_state & SocketStateTCPListening) == SocketStateTCPListening) || ((rsrc_obj->socket_state & SocketStateTCPConnected) == SocketStateTCPConnected)) { @@ -2850,12 +2956,12 @@ static term nif_socket_shutdown(Context *ctx, int argc, term argv[]) } #if OTP_SOCKET_BSD - if (rsrc_obj->fd == 0) { + if (rsrc_obj->fd == CLOSED_FD) { #elif OTP_SOCKET_LWIP if (rsrc_obj->socket_state == SocketStateClosed) { #endif SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); - return make_error_tuple(posix_errno_to_term(EBADF, global), ctx); + return make_error_tuple(CLOSED_ATOM, ctx); } term result = OK_ATOM; @@ -2929,6 +3035,10 @@ static const struct Nif socket_select_read_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_socket_select_read }; +static const struct Nif socket_select_write_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_socket_select_write +}; static const struct Nif socket_accept_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_socket_accept @@ -3003,6 +3113,10 @@ const struct Nif *otp_socket_nif_get_nif(const char *nifname) TRACE("Resolved platform nif %s ...\n", nifname); return &socket_select_read_nif; } + if (strcmp("nif_select_write/2", rest) == 0) { + TRACE("Resolved platform nif %s ...\n", nifname); + return &socket_select_write_nif; + } if (strcmp("nif_accept/1", rest) == 0) { TRACE("Resolved platform nif %s ...\n", nifname); return &socket_accept_nif; diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 8ebabb5166..9d3c040d28 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -251,6 +251,16 @@ int enif_select_read(ErlNifEnv *env, ErlNifEvent event, void *obj, const ErlNifP return enif_select_common(env, event, mode, obj, pid, term_nil(), message); } +int enif_select_write(ErlNifEnv *env, ErlNifEvent event, void *obj, const ErlNifPid *pid, ERL_NIF_TERM msg, ErlNifEnv *msg_env) +{ + if (UNLIKELY(msg_env != NULL)) { + return ERL_NIF_SELECT_BADARG; + } + Message *message = mailbox_message_create_normal_message_from_term(msg); + enum ErlNifSelectFlags mode = ERL_NIF_SELECT_WRITE; + return enif_select_common(env, event, mode, obj, pid, term_nil(), message); +} + term select_event_make_notification(void *rsrc_obj, uint64_t ref_ticks, bool is_write, Heap *heap) { term notification = term_alloc_tuple(4, heap); diff --git a/tests/libs/estdlib/test_gen_tcp.erl b/tests/libs/estdlib/test_gen_tcp.erl index 8ce59eeafa..d0d40b1897 100644 --- a/tests/libs/estdlib/test_gen_tcp.erl +++ b/tests/libs/estdlib/test_gen_tcp.erl @@ -25,19 +25,42 @@ -include("etest.hrl"). test() -> - ok = test_echo_server(), - ok = test_echo_server(true), - ok = test_listen_connect_parameters(), - ok = test_connect_parameters(), - ok = test_connect_bad_address(), - ok = test_tcp_double_close(), + HasInetDriver = test_inet:is_inet_driver_available(), + Backends = + case HasInetDriver of + true -> + [inet, socket]; + false -> + io:format( + "Warning: inet port driver not available, skipping inet backend cases in test_gen_tcp~n" + ), + [socket] + end, + lists:foreach( + fun(Backend) -> + ok = test_echo_server(Backend), + ok = test_echo_server(Backend, true) + end, + Backends + ), + %% test_tcp_double_close is only meaningful for the inet (port driver) + %% backend: closing a socket-backend gen_tcp socket stops the underlying + %% gen_server, so subsequent gen_tcp:close/1 or gen_tcp:recv/3 calls would + %% hit a dead process rather than returning {error, closed}. + case HasInetDriver of + true -> ok = test_tcp_double_close(); + false -> ok + end, + ok = test_listen_connect_parameters(HasInetDriver), + ok = test_connect_parameters(HasInetDriver), + ok = test_connect_bad_address(HasInetDriver), ok. -test_echo_server() -> - test_echo_server(false). +test_echo_server(Backend) -> + test_echo_server(Backend, false). -test_echo_server(SpawnControllingProcess) -> - {ok, ListenSocket} = gen_tcp:listen(0, []), +test_echo_server(Backend, SpawnControllingProcess) -> + {ok, ListenSocket} = gen_tcp:listen(0, [{inet_backend, Backend}]), {ok, {_Address, Port}} = inet:sockname(ListenSocket), Self = self(), @@ -53,7 +76,7 @@ test_echo_server(SpawnControllingProcess) -> after 5000 -> throw({timeout, test_echo_server, ?LINE}) end, - test_send_receive(Port, 10, SpawnControllingProcess), + test_send_receive(Backend, Port, 10, SpawnControllingProcess), %% TODO bug closing listening socket % gen_tcp:close(ListenSocket), @@ -92,8 +115,8 @@ echo(Pid, Socket) -> after 5000 -> throw({timeout, echo, ?LINE}) end. -test_send_receive(Port, N, SpawnControllingProcess) -> - {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, Port, [{active, true}]), +test_send_receive(Backend, Port, N, SpawnControllingProcess) -> + {ok, Socket} = gen_tcp:connect({127, 0, 0, 1}, Port, [{inet_backend, Backend}, {active, true}]), case SpawnControllingProcess of false -> loop(Socket, N); @@ -137,9 +160,12 @@ loop(Socket, I) -> after 5000 -> throw({timeout, loop, ?LINE}) end. -test_listen_connect_parameters() -> +test_listen_connect_parameters(HasInetDriver) -> ok = test_listen_connect_parameters(socket, socket), - ok = test_listen_connect_parameters(inet, inet), + case HasInetDriver of + true -> ok = test_listen_connect_parameters(inet, inet); + false -> ok + end, ok. test_listen_connect_parameters(InetClientBackend, InetServerBackend) -> @@ -352,7 +378,7 @@ test_listen_connect_parameters_server_loop(ListenMode, false = ListenActive, Soc {error, {unexpected_result, server, passive_receive, Other}} end. -test_connect_parameters() -> +test_connect_parameters(HasInetDriver) -> IP = case inet:getaddr("www.github.com", inet) of {ok, IPAddress} -> @@ -366,12 +392,17 @@ test_connect_parameters() -> end, Hostname = "www.atomvm.org", Port = 80, - OptTests = [ - [{active, true}], - [{active, false}], - [{inet_backend, socket}, {active, true}], - [{inet_backend, socket}, {active, false}] - ], + InetOptTests = + case HasInetDriver of + true -> [[{active, true}], [{active, false}]]; + false -> [] + end, + OptTests = + InetOptTests ++ + [ + [{inet_backend, socket}, {active, true}], + [{inet_backend, socket}, {active, false}] + ], test_connect_parameters(OptTests, IP, Hostname, Port, []). test_connect_parameters([], _IP, _Host, _Port, Results) -> @@ -404,15 +435,16 @@ test_connect_parameters([Test | TestOpts], IP, Host, Port, Results) -> end, test_connect_parameters(TestOpts, IP, Host, Port, [Results, IpResult, HostResult]). -test_connect_bad_address() -> - InetTest = test_connect_bad_address(inet_backend, []), +test_connect_bad_address(HasInetDriver) -> SocketTest = test_connect_bad_address(socket_backend, [{inet_backend, socket}]), - Result = [InetTest, SocketTest], - case Result of - [ok, ok] -> - ok; - _ -> - Result + Result = + case HasInetDriver of + true -> [test_connect_bad_address(inet_backend, []), SocketTest]; + false -> [SocketTest] + end, + case lists:all(fun(R) -> R =:= ok end, Result) of + true -> ok; + false -> Result end. test_connect_bad_address(Tag, Opts) -> @@ -500,7 +532,7 @@ test_connect_bad_address(Tag, Opts) -> end. test_tcp_double_close() -> - {ok, Socket} = gen_tcp:listen(10543, [{active, false}]), + {ok, Socket} = gen_tcp:listen(10543, [{inet_backend, inet}, {active, false}]), ok = gen_tcp:close(Socket), ok = gen_tcp:close(Socket), {error, closed} = gen_tcp:recv(Socket, 512, 5000), diff --git a/tests/libs/estdlib/test_gen_udp.erl b/tests/libs/estdlib/test_gen_udp.erl index 58d2aff0ba..60e6331ac6 100644 --- a/tests/libs/estdlib/test_gen_udp.erl +++ b/tests/libs/estdlib/test_gen_udp.erl @@ -25,12 +25,22 @@ -include("etest.hrl"). test() -> + Backends = + case test_inet:is_inet_driver_available() of + true -> + [[{inet_backend, inet}], [{inet_backend, socket}]]; + false -> + io:format( + "Warning: inet port driver not available, skipping inet backend in test_gen_udp~n" + ), + [[{inet_backend, socket}]] + end, [ ok = test_send_receive(SpawnControllingProcess, IsActive, Mode, BackendOption) || SpawnControllingProcess <- [false, true], IsActive <- [false, true], Mode <- [binary, list], - BackendOption <- [[{inet_backend, inet}], [{inet_backend, socket}]] + BackendOption <- Backends ], ok. diff --git a/tests/libs/estdlib/test_inet.erl b/tests/libs/estdlib/test_inet.erl index bb9d8e89fe..e38b44577b 100644 --- a/tests/libs/estdlib/test_inet.erl +++ b/tests/libs/estdlib/test_inet.erl @@ -20,7 +20,19 @@ -module(test_inet). --export([test/0]). +-export([test/0, is_inet_driver_available/0]). + +-spec is_inet_driver_available() -> boolean(). +is_inet_driver_available() -> + try gen_udp:open(0, [{inet_backend, inet}]) of + {ok, S} -> + catch gen_udp:close(S), + true; + _ -> + false + catch + _:_ -> false + end. test() -> ok = test_getaddr(), diff --git a/tests/libs/estdlib/test_tcp_socket.erl b/tests/libs/estdlib/test_tcp_socket.erl index 4ceed655b9..9fbf1e2dec 100644 --- a/tests/libs/estdlib/test_tcp_socket.erl +++ b/tests/libs/estdlib/test_tcp_socket.erl @@ -31,6 +31,7 @@ test() -> ok = test_recv_nowait(), ok = test_accept_nowait(), ok = test_setopt_getopt(), + ok = test_send_empty(), case erlang:system_info(machine) of "ATOM" -> ok = test_abandon_select(); @@ -529,6 +530,23 @@ test_setopt_getopt() -> {error, closed} = socket:setopt(Socket, {socket, reuseaddr}, true), ok. +test_send_empty() -> + etest:flush_msg_queue(), + + {ListenSocket, Port} = start_echo_server(0), + + {ok, Client} = socket:open(inet, stream, tcp), + ok = socket:connect(Client, #{family => inet, addr => loopback, port => Port}), + + ok = socket:send(Client, <<>>), + ok = socket:send(Client, []), + + ok = socket:send(Client, <<"echo:01">>), + {ok, <<"echo:01">>} = socket:recv(Client, ?PACKET_SIZE), + + ok = socket:close(Client), + ok = close_listen_socket(ListenSocket). + %% %% abandon_select test %% From 27c776fa287b0f2bb07747f88b9c377b9d506d39 Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sun, 3 May 2026 14:27:43 +0200 Subject: [PATCH 2/3] gen_tcp/gen_udp: fix dead reply semantics with socket backend Adopt OTP semantics for calls such as gen_tcp:close(Pid) when the gen_server is already closed, thus fixing a race condition in tests Signed-off-by: Paul Guyot --- CHANGELOG.md | 2 ++ libs/estdlib/src/gen_tcp_socket.erl | 17 ++++++++++++++++- libs/estdlib/src/gen_udp_socket.erl | 17 ++++++++++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0986b38ac6..ed0ec8a84f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,6 +67,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `socket:recvfrom`, `socket:send` and `socket:sendto` operating on a fd that was already closed by another process now return `{error, closed}` rather than `{error, ebadf}` +- `gen_tcp_socket` and `gen_udp_socket` now return proper result (`ok` or + `{error, closed}`) when the underlying process exited. ## [0.7.0-alpha.1] - 2026-04-06 diff --git a/libs/estdlib/src/gen_tcp_socket.erl b/libs/estdlib/src/gen_tcp_socket.erl index 80c370f84b..3637053278 100644 --- a/libs/estdlib/src/gen_tcp_socket.erl +++ b/libs/estdlib/src/gen_tcp_socket.erl @@ -582,7 +582,22 @@ handle_passive_recv(State, Ref, From, Length, _Timeout) -> %% @private call(Pid, Request) -> - gen_server:call(Pid, Request, infinity). + try + gen_server:call(Pid, Request, infinity) + catch + exit:{Reason, _} when + Reason =:= normal; + Reason =:= noproc; + Reason =:= shutdown + -> + dead_reply(Request); + exit:{{shutdown, _}, _} -> + dead_reply(Request) + end. + +%% @private +dead_reply(close) -> ok; +dead_reply(_) -> {error, closed}. %% @private maybe_encode_binary(Options, Data) -> diff --git a/libs/estdlib/src/gen_udp_socket.erl b/libs/estdlib/src/gen_udp_socket.erl index 71257758bd..f60b7a45d9 100644 --- a/libs/estdlib/src/gen_udp_socket.erl +++ b/libs/estdlib/src/gen_udp_socket.erl @@ -402,7 +402,22 @@ handle_passive_recvfrom(State, Ref, From, Length, _Timeout) -> %% @private call(Pid, Request) -> - gen_server:call(Pid, Request, infinity). + try + gen_server:call(Pid, Request, infinity) + catch + exit:{Reason, _} when + Reason =:= normal; + Reason =:= noproc; + Reason =:= shutdown + -> + dead_reply(Request); + exit:{{shutdown, _}, _} -> + dead_reply(Request) + end. + +%% @private +dead_reply(close) -> ok; +dead_reply(_) -> {error, closed}. %% @private maybe_encode_binary(Options, Data) -> From c4167cdbb479c1bb4c7cd5fa4e2f468497224f1c Mon Sep 17 00:00:00 2001 From: Paul Guyot Date: Sat, 2 May 2026 19:00:17 +0200 Subject: [PATCH 3/3] Add WASI platform Introduces the WASI (WebAssembly System Interface) platform allowing AtomVM to run as a pure WebAssembly module under runtimes such as wasmtime, WasmEdge, and wasmer. Three target triples are supported: - wasm32-wasip1: no SMP, no networking - wasm32-wasip1-threads: SMP via wasi-threads, no networking - wasm32-wasip2: networking via wasi:sockets@0.2.x, no SMP Signed-off-by: Paul Guyot --- .../workflows/build-and-test-on-freebsd.yaml | 2 +- .github/workflows/wasi-build.yaml | 183 ++++ CHANGELOG.md | 3 + README.Md | 6 +- doc/src/build-instructions.md | 103 ++- doc/src/welcome-to-atomvm.md | 2 +- src/platforms/wasi/.gitignore | 6 + src/platforms/wasi/CMakeLists.txt | 79 ++ src/platforms/wasi/README.md | 144 ++++ src/platforms/wasi/src/CMakeLists.txt | 68 ++ src/platforms/wasi/src/lib/CMakeLists.txt | 112 +++ .../wasi/src/lib/mbedtls_wasi_config.h | 33 + .../wasi/src/lib/otp_socket_platform.c | 27 + .../wasi/src/lib/otp_socket_platform.h | 48 ++ .../wasi/src/lib/platform_defaultatoms.c | 49 ++ .../wasi/src/lib/platform_defaultatoms.def | 21 + .../wasi/src/lib/platform_defaultatoms.h | 58 ++ src/platforms/wasi/src/lib/platform_nifs.c | 164 ++++ src/platforms/wasi/src/lib/smp.c | 243 ++++++ src/platforms/wasi/src/lib/sys.c | 790 ++++++++++++++++++ src/platforms/wasi/src/lib/wasi_compat.h | 27 + src/platforms/wasi/src/lib/wasi_sys.h | 58 ++ src/platforms/wasi/src/main.c | 147 ++++ src/platforms/wasi/wasi-sdk.cmake | 71 ++ tests/libs/eavmlib/test_dir.erl | 7 +- tests/libs/eavmlib/test_file.erl | 43 +- tests/libs/eavmlib/tests.erl | 15 +- tests/libs/estdlib/test_udp_socket.erl | 18 +- tests/libs/estdlib/tests.erl | 38 +- 29 files changed, 2519 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/wasi-build.yaml create mode 100644 src/platforms/wasi/.gitignore create mode 100644 src/platforms/wasi/CMakeLists.txt create mode 100644 src/platforms/wasi/README.md create mode 100644 src/platforms/wasi/src/CMakeLists.txt create mode 100644 src/platforms/wasi/src/lib/CMakeLists.txt create mode 100644 src/platforms/wasi/src/lib/mbedtls_wasi_config.h create mode 100644 src/platforms/wasi/src/lib/otp_socket_platform.c create mode 100644 src/platforms/wasi/src/lib/otp_socket_platform.h create mode 100644 src/platforms/wasi/src/lib/platform_defaultatoms.c create mode 100644 src/platforms/wasi/src/lib/platform_defaultatoms.def create mode 100644 src/platforms/wasi/src/lib/platform_defaultatoms.h create mode 100644 src/platforms/wasi/src/lib/platform_nifs.c create mode 100644 src/platforms/wasi/src/lib/smp.c create mode 100644 src/platforms/wasi/src/lib/sys.c create mode 100644 src/platforms/wasi/src/lib/wasi_compat.h create mode 100644 src/platforms/wasi/src/lib/wasi_sys.h create mode 100644 src/platforms/wasi/src/main.c create mode 100644 src/platforms/wasi/wasi-sdk.cmake diff --git a/.github/workflows/build-and-test-on-freebsd.yaml b/.github/workflows/build-and-test-on-freebsd.yaml index f22718ecc2..37258e9cdf 100644 --- a/.github/workflows/build-and-test-on-freebsd.yaml +++ b/.github/workflows/build-and-test-on-freebsd.yaml @@ -93,7 +93,7 @@ jobs: shell: freebsd {0} run: | cd $GITHUB_WORKSPACE; - sed -i '' 's/test_http_server/%test_http_server/g' tests/libs/eavmlib/tests.erl + sed -i '' 's/test_http_server, //g' tests/libs/eavmlib/tests.erl - name: "Build: create build dir" shell: freebsd {0} diff --git a/.github/workflows/wasi-build.yaml b/.github/workflows/wasi-build.yaml new file mode 100644 index 0000000000..b4bae08f37 --- /dev/null +++ b/.github/workflows/wasi-build.yaml @@ -0,0 +1,183 @@ +# +# Copyright 2026 Paul Guyot +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +name: WASI Build + +on: + push: + paths: + - '.github/workflows/wasi-build.yaml' + - 'CMakeLists.txt' + - 'CMakeModules/**' + - 'libs/**' + - 'src/platforms/wasi/**' + - 'src/libAtomVM/**' + pull_request: + paths: + - '.github/workflows/wasi-build.yaml' + - 'CMakeLists.txt' + - 'CMakeModules/**' + - 'libs/**' + - 'src/platforms/wasi/**' + - 'src/libAtomVM/**' + +permissions: + contents: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref != 'refs/heads/main' && github.ref || github.run_id }} + cancel-in-progress: true + +jobs: + + compile_tests: + + runs-on: ubuntu-24.04 + container: erlang:28 + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["c-cpp"] + + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install required packages + run: apt update && apt install -y gperf zlib1g-dev cmake ninja-build + + - name: "Git config safe.directory for codeql" + run: git config --global --add safe.directory /__w/AtomVM/AtomVM + + - name: "Initialize CodeQL" + uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + with: + languages: ${{matrix.language}} + build-mode: manual + queries: +./code-queries/term-to-non-term-func.ql,./code-queries/non-term-to-term-func.ql,./code-queries/mismatched-atom-string-length.ql,./code-queries/mismatched-free-type.ql,./code-queries/term-use-after-gc.ql,./code-queries/allocations-exceeding-ensure-free.ql,./code-queries/allocations-without-ensure-free.ql + + - name: Compile AtomVM and test modules + run: | + set -e + mkdir build + cd build + cmake .. -G Ninja -DAVM_WARNINGS_ARE_ERRORS=ON + ninja AtomVM atomvmlib erlang_test_modules test_etest test_alisp test_estdlib test_eavmlib + + - name: "Perform CodeQL Analysis" + uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4 + + - name: Upload AtomVM and test modules + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: atomvm-and-test-modules + path: | + build/**/*.avm + build/**/*.beam + build/src/AtomVM + retention-days: 1 + + wasi_build_and_test: + needs: compile_tests + runs-on: ubuntu-24.04 + + strategy: + fail-fast: false + matrix: + include: + # No SMP, no networking - minimal stable target. + - target: wasm32-wasip1 + disable_smp: ON + wasmtime_flags: "" + # SMP via wasi-threads, no networking. Needs shared-memory in wasmtime. + - target: wasm32-wasip1-threads + disable_smp: OFF + wasmtime_flags: "-W threads -W shared-memory -S threads" + # No SMP, networking via wasi:sockets@0.2.x - exercises BSD sockets and SSL. + - target: wasm32-wasip2 + disable_smp: ON + wasmtime_flags: "-S inherit-network -S allow-ip-name-lookup" + + steps: + - name: Checkout repo + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Install required packages + run: sudo apt-get update -y && sudo apt-get install -y cmake ninja-build gperf + + - name: Install WASI SDK + run: | + set -euo pipefail + WASI_SDK_VERSION=33 + wget -q "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" + sudo mkdir -p /opt/wasi-sdk + sudo tar xzf "wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux.tar.gz" --strip-components=1 -C /opt/wasi-sdk + + - name: Install wasmtime + run: | + set -euo pipefail + WASMTIME_VERSION=44.0.1 + WASMTIME_TARBALL="wasmtime-v${WASMTIME_VERSION}-x86_64-linux.tar.xz" + WASMTIME_SHA256="afd58715f105e3a7f454169daed22168c5736ec5f225fb04c4ac62c54c9508a3" + wget -q "https://github.com/bytecodealliance/wasmtime/releases/download/v${WASMTIME_VERSION}/${WASMTIME_TARBALL}" + echo "${WASMTIME_SHA256} ${WASMTIME_TARBALL}" | sha256sum -c - + sudo mkdir -p /opt/wasmtime + sudo tar xf "${WASMTIME_TARBALL}" --strip-components=1 -C /opt/wasmtime + echo "/opt/wasmtime" >> $GITHUB_PATH + + - name: Build (${{ matrix.target }}) + working-directory: ./src/platforms/wasi/ + run: | + set -euo pipefail + mkdir build + cd build + cmake -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=../wasi-sdk.cmake \ + -DAVM_DISABLE_JIT=ON \ + -DAVM_DISABLE_SMP=${{ matrix.disable_smp }} \ + -DWASI_TARGET=${{ matrix.target }} \ + .. + ninja + + - name: Download AtomVM and test modules + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: atomvm-and-test-modules + path: build + + - name: Test (${{ matrix.target }}) + working-directory: ./build + run: | + set -euxo pipefail + WASM=../src/platforms/wasi/build/src/AtomVM.wasm + FLAGS="${{ matrix.wasmtime_flags }}" + wasmtime run ${FLAGS} --dir=. ${WASM} tests/libs/alisp/test_alisp.avm + wasmtime run ${FLAGS} --dir=. ${WASM} tests/libs/estdlib/test_estdlib.avm + wasmtime run ${FLAGS} --dir=. ${WASM} tests/libs/etest/test_etest.avm + wasmtime run ${FLAGS} --dir=. ${WASM} tests/libs/eavmlib/test_eavmlib.avm + + - name: "Rename and write sha256sum" + if: startsWith(github.ref, 'refs/tags/') + working-directory: src/platforms/wasi/build/src + run: | + ATOMVM_WASM=AtomVM-${{ matrix.target }}-${{ github.ref_name }}.wasm + mv AtomVM.wasm "${ATOMVM_WASM}" + sha256sum "${ATOMVM_WASM}" > "${ATOMVM_WASM}.sha256" + + - name: "Release" + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 + if: startsWith(github.ref, 'refs/tags/') + with: + draft: true + fail_on_unmatched_files: true + files: | + src/platforms/wasi/build/src/AtomVM-${{ matrix.target }}-${{ github.ref_name }}.wasm + src/platforms/wasi/build/src/AtomVM-${{ matrix.target }}-${{ github.ref_name }}.wasm.sha256 diff --git a/CHANGELOG.md b/CHANGELOG.md index ed0ec8a84f..f57699626a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `"USB_SERIAL_JTAG"` peripheral to the ESP32 `uart` module on chips with a built-in USB-Serial-JTAG controller (C3/C5/C6/C61/H2/H21/H4/P4/S3) - Added support for the `safe` option in `erlang:binary_to_term/2` +- Added WASI platform, with `wasm32-wasip1` (no SMP, no networking), + `wasm32-wasip1-threads` (SMP, no networking) and `wasm32-wasip2` (no SMP, + networking via `wasi:sockets@0.2.x`) target triples ### Changed - Updated network type db() to dbm() to reflect the actual representation of the type diff --git a/README.Md b/README.Md index 52cb9d2aae..da837316ad 100644 --- a/README.Md +++ b/README.Md @@ -20,6 +20,7 @@ Supported Platforms * STM32 MCUs (with official ST HAL/LL SDK, see [stm32](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-stm32-platform)) * Raspberry Pi Pico and Pico 2 (see [rp2](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-on-the-raspberry-pi-pico-platform)) * Browsers and NodeJS with WebAssembly (see [emscripten](https://doc.atomvm.org/main/getting-started-guide.html#getting-started-with-atomvm-webassembly)) +* Standalone WebAssembly runtimes such as wasmtime (see [wasi](https://doc.atomvm.org/main/build-instructions.html#building-for-wasi)) AtomVM aims to be easily portable to new platforms with a minimum effort, so additional platforms might be supported in a near future. @@ -72,8 +73,9 @@ available in the documentation for [ESP32](https://doc.atomvm.org/main/build-instructions.html#building-for-esp32), [STM32](https://doc.atomvm.org/main/build-instructions.html#building-for-stm32), [Raspberry Pi Pico and Pico 2](https://doc.atomvm.org/main/build-instructions.html#building-for-raspberry-pi-pico) -(rp2), and -[WASM](https://doc.atomvm.org/main/build-instructions.html#building-for-nodejs-web) (NodeJS/Web). +(rp2), +[WASM](https://doc.atomvm.org/main/build-instructions.html#building-for-nodejs-web) (NodeJS/Web), and +[WASI](https://doc.atomvm.org/main/build-instructions.html#building-for-wasi) (standalone WebAssembly). Project Status ============== diff --git a/doc/src/build-instructions.md b/doc/src/build-instructions.md index a042d43f00..ecab966c92 100644 --- a/doc/src/build-instructions.md +++ b/doc/src/build-instructions.md @@ -20,7 +20,7 @@ The native C parts of AtomVM compile to machine code on MacOS, Linux, and FreeBS The Erlang and Elixir parts are compiled to BEAM byte-code using the Erlang (`erlc`) and Elixir compilers. For information about specific versions of required software, see the [Release Notes](./release-notes.md). -This guide provides information about how to build AtomVM for the various supported platforms (Generic UNIX, ESP32, and STM32). +This guide provides information about how to build AtomVM for the various supported platforms (Generic UNIX, ESP32, STM32, RP2, Emscripten WASM, and WASI). ```{attention} In order to build AtomVM AVM files for ESP32 and STM32 platforms, you will also need to build @@ -92,6 +92,7 @@ It is possible to use a local copy of `uf2tool` source code by setting `UF2TOOL_ * [STM32](#building-for-stm32) * [RP2](#building-for-rp2) (including Pico boards) * [WASM](#building-for-emscripten) (NodeJS or web) +* [WASI](#building-for-wasi) (WebAssembly System Interface, wasmtime/wasmer) ## Building for Generic UNIX @@ -1218,3 +1219,103 @@ $ cd src/platforms/emscripten/tests/ $ npm install cypress $ npx cypress open ``` + +## Building for WASI + +WASI (WebAssembly System Interface) allows AtomVM to run as a pure WebAssembly module without JavaScript dependencies. Unlike the Emscripten build, WASI targets standalone runtimes such as wasmtime, WasmEdge, and wasmer. + +### WASI Prerequisites + +* [WASI SDK](https://github.com/WebAssembly/wasi-sdk/releases) at version 33 or + newer recommended +* A WASI runtime, such as [wasmtime](https://wasmtime.dev) (recommended) +* `cmake` 3.13 or later +* Erlang/OTP + +### WASI Target Modes + +The build supports three target triples, picked automatically from `AVM_DISABLE_SMP`. SMP and networking sit on different targets and are currently mutually exclusive: + +| Target triple | SMP | Networking | Selection | +| ------------------------- | --- | ---------------------------------- | ------------------------------------------------------ | +| `wasm32-wasip1-threads` | yes | no | default (`-DAVM_DISABLE_SMP=OFF`) | +| `wasm32-wasip2` | no | yes (`wasi:sockets@0.2.x`) | `-DAVM_DISABLE_SMP=ON` | +| `wasm32-wasip1` | no | no | `-DAVM_DISABLE_SMP=ON -DWASI_TARGET=wasm32-wasip1` | + +`erlang:system_info(system_architecture)` reports the active triple (e.g. `wasm32-wasip2`). + +### WASI Build Instructions + +Default (SMP, no networking): + +```shell +$ cd src/platforms/wasi/ +$ mkdir build +$ cd build +$ cmake -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=../wasi-sdk.cmake \ + -DAVM_DISABLE_JIT=ON \ + .. +$ ninja +``` + +For networking (single-threaded `wasm32-wasip2`): + +```shell +$ cmake -G Ninja \ + -DCMAKE_TOOLCHAIN_FILE=../wasi-sdk.cmake \ + -DAVM_DISABLE_JIT=ON \ + -DAVM_DISABLE_SMP=ON \ + .. +``` + +The WASI SDK location is detected automatically from the `WASI_SDK_PATH` environment variable, `/opt/local/libexec/wasi-sdk` (MacPorts), or `/opt/wasi-sdk`. + +### Running AtomVM on WASI + +WASI uses capability-based security. Grant directory access with `--dir` when running: + +```shell +$ wasmtime run --dir=. build/src/AtomVM.wasm myapp.avm +``` + +For SMP (`wasm32-wasip1-threads`) wasmtime needs the threads proposal: + +```shell +$ wasmtime run -W threads -W shared-memory --dir=. build/src/AtomVM.wasm myapp.avm +``` + +For networking (`wasm32-wasip2`) grant socket and DNS capabilities: + +```shell +$ wasmtime run -S inherit-network -S allow-ip-name-lookup --dir=. \ + build/src/AtomVM.wasm myapp.avm +``` + +You can load multiple AVM or BEAM files by listing them as additional arguments: + +```shell +$ wasmtime run --dir=. build/src/AtomVM.wasm myapp.avm libs/atomvmlib.avm +``` + +### Running tests with WASI + +Build the test modules first using the Generic UNIX build (see above). Then run: + +```shell +$ cd build +$ wasmtime run --dir=. ../src/platforms/wasi/build/src/AtomVM.wasm \ + tests/libs/alisp/test_alisp.avm +$ wasmtime run --dir=. ../src/platforms/wasi/build/src/AtomVM.wasm \ + tests/libs/estdlib/test_estdlib.avm +$ wasmtime run --dir=. ../src/platforms/wasi/build/src/AtomVM.wasm \ + tests/libs/etest/test_etest.avm +``` + +### WASI Platform Limitations + +* JIT compilation is not supported; the WebAssembly sandbox prohibits executable memory. +* Networking requires the `wasm32-wasip2` target; the WASI Preview 1 targets (`wasm32-wasip1` and `wasm32-wasip1-threads`) have no sockets. +* SMP requires the `wasm32-wasip1-threads` target; `wasm32-wasip2` does not provide threads. +* Process creation (fork/exec) is not supported on any WASI target. +* Directory access requires explicit capability grants via `--dir`. diff --git a/doc/src/welcome-to-atomvm.md b/doc/src/welcome-to-atomvm.md index 4e218fbfc7..fb23b04329 100644 --- a/doc/src/welcome-to-atomvm.md +++ b/doc/src/welcome-to-atomvm.md @@ -55,7 +55,7 @@ AtomVM is licensed under the terms of the [Apache2](https://www.apache.org/licen ## Source Code -The [AtomVM Github Repository](https://github.com/atomvm/AtomVM) contains the AtomVM source code, including the AtomVM virtual machine and core libraries. The AtomVM [Build Instructions](./build-instructions.md) contains instructions for building AtomVM for Generic UNIX, ESP32, and STM32 platforms. +The [AtomVM Github Repository](https://github.com/atomvm/AtomVM) contains the AtomVM source code, including the AtomVM virtual machine and core libraries. The AtomVM [Build Instructions](./build-instructions.md) contains instructions for building AtomVM for Generic UNIX, ESP32, STM32, RP2, Emscripten, and WASI platforms. ## Contributing diff --git a/src/platforms/wasi/.gitignore b/src/platforms/wasi/.gitignore new file mode 100644 index 0000000000..906a32425b --- /dev/null +++ b/src/platforms/wasi/.gitignore @@ -0,0 +1,6 @@ +# Copyright 2026 Paul Guyot +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + +build/ +build.* diff --git a/src/platforms/wasi/CMakeLists.txt b/src/platforms/wasi/CMakeLists.txt new file mode 100644 index 0000000000..18109cb5bb --- /dev/null +++ b/src/platforms/wasi/CMakeLists.txt @@ -0,0 +1,79 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# +cmake_minimum_required(VERSION 3.13) +project(AtomVM) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../../../CMakeModules") + +# Require WASM toolchain +if (NOT CMAKE_SYSTEM_NAME STREQUAL "WASI") + message(FATAL_ERROR "This platform requires WASI toolchain. Use -DCMAKE_TOOLCHAIN_FILE=/path/to/wasi-sdk.cmake") +endif() + +# Options that make sense for this platform +option(AVM_DISABLE_SMP "Disable SMP." OFF) +option(AVM_USE_32BIT_FLOAT "Use 32 bit floats." OFF) +option(AVM_VERBOSE_ABORT "Print module and line number on VM abort" OFF) +option(AVM_CREATE_STACKTRACES "Create stacktraces" ON) + +# WASI target selection. SMP and networking are currently mutually exclusive: +# - wasm32-wasip1 : no SMP, no networking +# - wasm32-wasip1-threads : SMP, no networking +# - wasm32-wasip2 : networking via wasi:sockets@0.2.x, no SMP +# +# The default mirrors the AVM_DISABLE_SMP toggle: SMP-enabled builds (the +# project default) pick wasip1-threads (no networking), SMP-disabled builds +# pick wasip2 (networking). Override with -DWASI_TARGET=... to pick wasip1 +# explicitly or to pin a specific triple from a CI matrix. +if (NOT DEFINED WASI_TARGET) + if (AVM_DISABLE_SMP) + set(WASI_TARGET "wasm32-wasip2" CACHE STRING "WASI target triple") + else() + set(WASI_TARGET "wasm32-wasip1-threads" CACHE STRING "WASI target triple") + endif() +endif() + +# WASI 0.2 / Preview 2 detection - used only for diagnostics. The actual +# networking codepath is gated on the compiler-defined __wasip2__ macro. +if (WASI_TARGET MATCHES "wasip2") + set(WASI_PREVIEW 2) + message(STATUS "WASI Preview 2 selected (networking via wasi:sockets@0.2.x): ${WASI_TARGET}") + if (NOT AVM_DISABLE_SMP) + message(FATAL_ERROR + "wasm32-wasip2 does not support threads. Either build with " + "-DAVM_DISABLE_SMP=ON or pick wasm32-wasip1-threads (no networking).") + endif() +else() + set(WASI_PREVIEW 1) + message(STATUS "WASI Preview 1 selected (no networking): ${WASI_TARGET}") +endif() + +# Surface the actual WASI target through erlang:system_info(system_architecture) +# so tests can branch on it (e.g. suffix-match "wasip2" to gate networking). +# WASI has no vendor concept, so we keep the bare 2-part form. +set(AVM_SYSTEM_ARCHITECTURE_STRING "${WASI_TARGET}" CACHE STRING "" FORCE) + +# Ensure we're using the correct compiler target +set(CMAKE_C_COMPILER_TARGET ${WASI_TARGET}) +set(CMAKE_CXX_COMPILER_TARGET ${WASI_TARGET}) + +set(PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}) + +add_subdirectory(src) diff --git a/src/platforms/wasi/README.md b/src/platforms/wasi/README.md new file mode 100644 index 0000000000..c7b88b5cff --- /dev/null +++ b/src/platforms/wasi/README.md @@ -0,0 +1,144 @@ + + +# AtomVM WASI Platform + +This directory contains the AtomVM platform implementation for WASI (WebAssembly System Interface), allowing AtomVM to run as a pure WebAssembly module without JavaScript dependencies. + +## Overview + +WASI is a modular system interface for WebAssembly that enables portable, sandboxed execution of WebAssembly modules outside of web browsers. This platform targets: + +- **WASI Preview 1** (`wasm32-wasip1`): Stable, widely supported, no networking +- **WASI Preview 1 + threads** (`wasm32-wasip1-threads`): SMP support, no networking +- **WASI Preview 2** (`wasm32-wasip2`): BSD-style TCP/UDP networking via `wasi:sockets@0.2.x`, no SMP yet + +## Requirements + +1. **WASI SDK** version 33 or newer recommended. Download from https://github.com/WebAssembly/wasi-sdk/releases + ```bash + # macOS example + cd /opt + wget https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-33/wasi-sdk-33.0-arm64-macos.tar.gz + tar xzf wasi-sdk-33.0-arm64-macos.tar.gz + export WASI_SDK_PATH=/opt/wasi-sdk-33.0 + ``` + +2. **WASI Runtime** (one of): + - [wasmtime](https://wasmtime.dev) (recommended) + - [WasmEdge](https://wasmedge.org) + - [wasmer](https://wasmer.io) + +## Building + +```bash +cd src/platforms/wasi +mkdir build +cd build + +# Configure with WASI toolchain +cmake -DCMAKE_TOOLCHAIN_FILE=../wasi-sdk.cmake .. + +# Build +make -j +``` + +### Build Options + +| Option | Description | Default | +|--------|-------------|---------| +| `AVM_DISABLE_SMP` | Disable multi-threading support | OFF | +| `AVM_USE_32BIT_FLOAT` | Use 32-bit floats | OFF | +| `AVM_VERBOSE_ABORT` | Print abort info | OFF | +| `AVM_CREATE_STACKTRACES` | Create stack traces | ON | + +### Threading Support + +The default target is auto-selected based on `AVM_DISABLE_SMP`: + +- `AVM_DISABLE_SMP=OFF` (default) -> `wasm32-wasip1-threads` (SMP, no networking) +- `AVM_DISABLE_SMP=ON` -> `wasm32-wasip2` (networking, no SMP) + +Override explicitly: +```bash +cmake -DCMAKE_TOOLCHAIN_FILE=../wasi-sdk.cmake \ + -DWASI_TARGET=wasm32-wasip2 \ + -DAVM_DISABLE_SMP=ON \ + .. +``` + +Note: SMP and networking are currently mutually exclusive - see [WASI Versions](#wasi-versions) below. + +## Running + +WASI uses capability-based security. You must explicitly grant directory access, +and (when networking is built in) network access. + +### With wasmtime: +```bash +# No networking +wasmtime run --dir=. AtomVM.wasm myapp.avm + +# wasip2 build with networking (DNS lookup needed for ssl/getaddrinfo) +wasmtime run --dir=. -S inherit-network -S allow-ip-name-lookup AtomVM.wasm myapp.avm + +# wasip1-threads build with SMP (needs shared memory) +wasmtime run -W threads -W shared-memory -S threads --dir=. AtomVM.wasm myapp.avm +``` + +### With WasmEdge: +```bash +wasmedge --dir .:. AtomVM.wasm myapp.avm +``` + +### With wasmer: +```bash +wasmer run --dir . AtomVM.wasm -- myapp.avm +``` + +Note: as of May 2026, only wasmtime runs `wasm32-wasip2` builds with full +`wasi:sockets@0.2.x` networking out of the box. wasmer (tested with 7.1.0) +rejects the component module entirely ("component model feature is not +enabled"); WasmEdge's wasi-sockets plugin targets a Preview-1 socket +extension, not the Preview 2 component imports. Use wasmtime for the +networking-enabled build, or use the `wasm32-wasip1` / `wasm32-wasip1-threads` +target (no networking) on wasmer/WasmEdge. + +## WASI Versions + +### WASI Preview 1 (wasm32-wasip1) +- **Stable and widely supported** +- POSIX-like system calls via `wasi_snapshot_preview1` imports +- File access through preopened directories +- No networking + +### WASI Preview 1 + threads (wasm32-wasip1-threads) +- SMP multi-threading via the wasi-threads proposal (shared memory) +- No networking + +### WASI Preview 2 (wasm32-wasip2) +- Component-model based, runs on wasip2-aware runtimes (wasmtime >= 22, WasmEdge, + wasmer) +- BSD-style synchronous TCP/UDP, `getaddrinfo`, `inet_*` provided by wasi-libc on + top of `wasi:sockets@0.2.x` WIT interfaces +- No threading support yet (a `wasm32-wasip2-threads` target is being worked on + upstream but not in any released wasi-sdk) + +### Networking vs SMP + +These are currently mutually exclusive: `wasm32-wasip1-threads` provides SMP but +no networking, while `wasm32-wasip2` provides networking but no SMP. A combined +target is being worked on upstream and will land here once wasi-sdk ships it. + +### Detecting target at runtime + +`erlang:system_info(system_architecture)` returns the actual WASI triple: + +| Build | `system_architecture` | +|--------------------------|-------------------------------| +| `wasm32-wasip1` | `<<"wasm32-wasip1">>` | +| `wasm32-wasip1-threads` | `<<"wasm32-wasip1-threads">>` | +| `wasm32-wasip2` | `<<"wasm32-wasip2">>` | diff --git a/src/platforms/wasi/src/CMakeLists.txt b/src/platforms/wasi/src/CMakeLists.txt new file mode 100644 index 0000000000..aa1d412c09 --- /dev/null +++ b/src/platforms/wasi/src/CMakeLists.txt @@ -0,0 +1,68 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# +cmake_minimum_required (VERSION 3.13) + +add_executable(AtomVM main.c) +set_target_properties(AtomVM PROPERTIES SUFFIX ".wasm") + +target_compile_features(AtomVM PUBLIC c_std_11) + +add_subdirectory(../../../libAtomVM libAtomVM) +target_link_libraries(AtomVM PUBLIC libAtomVM) + +# Force-include the WASI compat shim (tzset prototype, missing sockopts) into +# every libAtomVM compile unit and into the WASI platform lib too. The platform +# lib pulls in libAtomVM/otp_socket.c, which expects SO_LINGER. +set(WASI_COMPAT_INCLUDE "-include${CMAKE_CURRENT_SOURCE_DIR}/lib/wasi_compat.h") +target_compile_options(libAtomVM PRIVATE ${WASI_COMPAT_INCLUDE}) + +if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(libAtomVM PUBLIC -O3) +endif() + +# Export malloc/free so the embedding host can pass binaries into the wasm +# instance without each host having to wrap them itself. +set(WASI_LINK_OPTIONS + -O3 + -Wl,--initial-memory=67108864 + -Wl,--max-memory=268435456 + -Wl,--export-dynamic + -Wl,--export=malloc + -Wl,--export=free +) + +if (NOT AVM_DISABLE_SMP) + list(APPEND WASI_LINK_OPTIONS -pthread) + target_compile_options(libAtomVM PUBLIC -pthread) +endif() + +target_link_options(AtomVM PRIVATE ${WASI_LINK_OPTIONS}) + +if (CMAKE_BUILD_TYPE STREQUAL "Debug") + target_compile_options(AtomVM PRIVATE -g) + target_link_options(AtomVM PRIVATE -g) + target_compile_options(libAtomVM PRIVATE -g) +endif() + +set(PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}) + +add_subdirectory(lib) +target_link_libraries(AtomVM PRIVATE libAtomVM${PLATFORM_LIB_SUFFIX}) +target_include_directories(AtomVM PRIVATE lib) diff --git a/src/platforms/wasi/src/lib/CMakeLists.txt b/src/platforms/wasi/src/lib/CMakeLists.txt new file mode 100644 index 0000000000..bc0c52dc13 --- /dev/null +++ b/src/platforms/wasi/src/lib/CMakeLists.txt @@ -0,0 +1,112 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# +cmake_minimum_required (VERSION 3.13) +project (libAtomVMPlatformWASI) + +# Disable mbedTLS unused targets that do not compile because wasi doesn't have +# the POSIX API they rely on +set(ENABLE_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ENABLE_TESTING OFF CACHE BOOL "" FORCE) +include(FetchMbedTLS) + +set(_mbedtls_wasi_cfg "MBEDTLS_USER_CONFIG_FILE=\"${CMAKE_CURRENT_SOURCE_DIR}/mbedtls_wasi_config.h\"") +foreach(_tgt mbedcrypto mbedtls mbedx509) + if(TARGET ${_tgt}) + target_compile_definitions(${_tgt} PRIVATE "${_mbedtls_wasi_cfg}") + endif() +endforeach() +unset(_mbedtls_wasi_cfg) +unset(_tgt) + +set(HEADER_FILES + wasi_sys.h + "../../../../libAtomVM/otp_crypto.h" +) + +set(SOURCE_FILES + platform_defaultatoms.c + platform_nifs.c + smp.c + sys.c + "../../../../libAtomVM/otp_crypto.c" +) + +if (WASI_TARGET MATCHES "wasip2") + list(APPEND HEADER_FILES otp_socket_platform.h) + list(APPEND SOURCE_FILES + otp_socket_platform.c + ../../../../libAtomVM/inet.c + ../../../../libAtomVM/otp_net.c + ../../../../libAtomVM/otp_socket.c + ) +endif() + +if (NOT AVM_DISABLE_JIT) + message(FATAL_ERROR "JIT is not supported on WASI: the WebAssembly sandbox prohibits executable memory allocation. Configure with -DAVM_DISABLE_JIT=ON.") +endif() + +set(PLATFORM_LIB_SUFFIX ${CMAKE_SYSTEM_NAME}-${CMAKE_SYSTEM_PROCESSOR}) + +add_library(libAtomVM${PLATFORM_LIB_SUFFIX} ${SOURCE_FILES} ${HEADER_FILES}) + +target_compile_features(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC c_std_11) + +target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC libAtomVM) + +# Pull in the WASI compat shim (defined by the parent src/CMakeLists.txt). +target_compile_options(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE ${WASI_COMPAT_INCLUDE}) + +target_include_directories(libAtomVM${PLATFORM_LIB_SUFFIX} + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} +) + +target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC ATOMVM_HAS_MBEDTLS) +target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC MbedTLS::mbedcrypto) + +# otp_ssl needs the full mbedTLS (not just mbedcrypto) and is only useful when +# we have BSD sockets to wrap. +if (WASI_TARGET MATCHES "wasip2") + target_link_libraries(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC MbedTLS::mbedtls) + target_sources(libAtomVM${PLATFORM_LIB_SUFFIX} PRIVATE + ../../../../libAtomVM/otp_ssl.c + ../../../../libAtomVM/otp_ssl.h + ) + include(DefineIfExists) + define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} gethostname "unistd.h" PRIVATE HAVE_GETHOSTNAME) + define_if_function_exists(libAtomVM${PLATFORM_LIB_SUFFIX} getservbyname "netdb.h" PRIVATE HAVE_SERVBYNAME) +endif() + +include(CheckCSourceCompiles) +get_target_property(_mbedcrypto_includes MbedTLS::mbedcrypto INTERFACE_INCLUDE_DIRECTORIES) +set(CMAKE_REQUIRED_INCLUDES ${_mbedcrypto_includes}) +check_c_source_compiles(" +#include +#ifndef MBEDTLS_PSA_CRYPTO_C +#error PSA Crypto not available +#endif +int main(void) { return 0; } +" HAVE_PSA_CRYPTO) +unset(CMAKE_REQUIRED_INCLUDES) +unset(_mbedcrypto_includes) + +if (HAVE_PSA_CRYPTO) + target_compile_definitions(libAtomVM${PLATFORM_LIB_SUFFIX} PUBLIC HAVE_PSA_CRYPTO) +endif() diff --git a/src/platforms/wasi/src/lib/mbedtls_wasi_config.h b/src/platforms/wasi/src/lib/mbedtls_wasi_config.h new file mode 100644 index 0000000000..bc33e229b1 --- /dev/null +++ b/src/platforms/wasi/src/lib/mbedtls_wasi_config.h @@ -0,0 +1,33 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +/* mbedTLS user config for WASI builds. + * Included via -DMBEDTLS_USER_CONFIG_FILE after the default config. + */ + +/* WASI has no /dev/random; provide entropy via mbedtls_hardware_poll + * (implemented in sys.c using getentropy(3)). */ +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +#define MBEDTLS_PLATFORM_MS_TIME_ALT + +#undef MBEDTLS_TIMING_C +#undef MBEDTLS_NET_C diff --git a/src/platforms/wasi/src/lib/otp_socket_platform.c b/src/platforms/wasi/src/lib/otp_socket_platform.c new file mode 100644 index 0000000000..8b786a2d5a --- /dev/null +++ b/src/platforms/wasi/src/lib/otp_socket_platform.c @@ -0,0 +1,27 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "otp_socket_platform.h" + +bool otp_socket_platform_supports_peek(void) +{ + /* wasi:sockets@0.2.x does not guarantee MSG_PEEK semantics. */ + return false; +} diff --git a/src/platforms/wasi/src/lib/otp_socket_platform.h b/src/platforms/wasi/src/lib/otp_socket_platform.h new file mode 100644 index 0000000000..7a6d3344be --- /dev/null +++ b/src/platforms/wasi/src/lib/otp_socket_platform.h @@ -0,0 +1,48 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef __OTP_SOCKET_PLATFORM_H__ +#define __OTP_SOCKET_PLATFORM_H__ + +#include +#include + +#define AVM_LOGD(tag, format, ...) \ + do { \ + } while (0) + +#define AVM_LOGI(tag, format, ...) \ + do { \ + fprintf(stderr, "I %s: " format " (%s:%i)\n", tag, ##__VA_ARGS__, __FILE__, __LINE__); \ + } while (0) + +#define AVM_LOGW(tag, format, ...) \ + do { \ + fprintf(stderr, "W %s: " format " (%s:%i)\n", tag, ##__VA_ARGS__, __FILE__, __LINE__); \ + } while (0) + +#define AVM_LOGE(tag, format, ...) \ + do { \ + fprintf(stderr, "E %s: " format " (%s:%i)\n", tag, ##__VA_ARGS__, __FILE__, __LINE__); \ + } while (0) + +bool otp_socket_platform_supports_peek(void); + +#endif diff --git a/src/platforms/wasi/src/lib/platform_defaultatoms.c b/src/platforms/wasi/src/lib/platform_defaultatoms.c new file mode 100644 index 0000000000..b321132256 --- /dev/null +++ b/src/platforms/wasi/src/lib/platform_defaultatoms.c @@ -0,0 +1,49 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "platform_defaultatoms.h" + +#include +#include + +#include +#include + +void platform_defaultatoms_init(GlobalContext *glb) +{ +#define X(name, lenstr, str) \ + lenstr str, + + static const char *const atoms[] = { +#include "platform_defaultatoms.def" + NULL + }; +#undef X + + for (size_t i = 0; i < ATOM_FIRST_AVAIL_INDEX - PLATFORM_ATOMS_BASE_INDEX; i++) { + if (UNLIKELY((size_t) atoms[i][0] != strlen(atoms[i] + 1))) { + AVM_ABORT(); + } + term atom_term = globalcontext_make_atom(glb, atoms[i]); + if (UNLIKELY(term_to_atom_index(atom_term) != i + PLATFORM_ATOMS_BASE_INDEX)) { + AVM_ABORT(); + } + } +} diff --git a/src/platforms/wasi/src/lib/platform_defaultatoms.def b/src/platforms/wasi/src/lib/platform_defaultatoms.def new file mode 100644 index 0000000000..f0cc4e6b74 --- /dev/null +++ b/src/platforms/wasi/src/lib/platform_defaultatoms.def @@ -0,0 +1,21 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +X(WASI_ATOM, "\x4", "wasi") diff --git a/src/platforms/wasi/src/lib/platform_defaultatoms.h b/src/platforms/wasi/src/lib/platform_defaultatoms.h new file mode 100644 index 0000000000..d28993c01e --- /dev/null +++ b/src/platforms/wasi/src/lib/platform_defaultatoms.h @@ -0,0 +1,58 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef PLATFORM_DEFAULTATOMS_H_ +#define PLATFORM_DEFAULTATOMS_H_ + +#include "defaultatoms.h" + +// About X macro: https://en.wikipedia.org/wiki/X_macro + +#define X(name, lenstr, str) \ + name##_INDEX, + +enum +{ + PLATFORM_ATOMS_BASE_INDEX_MINUS_ONE = PLATFORM_ATOMS_BASE_INDEX - 1, + +#include "platform_defaultatoms.def" + + ATOM_FIRST_AVAIL_INDEX +}; + +#undef X + +_Static_assert((int) ATOM_FIRST_AVAIL_INDEX > (int) PLATFORM_ATOMS_BASE_INDEX, + "default atoms and platform ones are overlapping"); + +#define X(name, lenstr, str) \ + name = TERM_FROM_ATOM_INDEX(name##_INDEX), + +enum +{ +#include "platform_defaultatoms.def" + + // dummy last item + ATOM_FIRST_AVAIL_DUMMY = TERM_FROM_ATOM_INDEX(ATOM_FIRST_AVAIL_INDEX) +}; + +#undef X + +#endif /* PLATFORM_DEFAULTATOMS_H_ */ diff --git a/src/platforms/wasi/src/lib/platform_nifs.c b/src/platforms/wasi/src/lib/platform_nifs.c new file mode 100644 index 0000000000..fe9dd7d191 --- /dev/null +++ b/src/platforms/wasi/src/lib/platform_nifs.c @@ -0,0 +1,164 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "platform_defaultatoms.h" +#include "wasi_sys.h" + +#ifdef __wasip2__ +#include +#include +#if ATOMVM_HAS_MBEDTLS +#include +#endif +#endif + +static const struct Nif *get_crypto_nif(const char *nifname, GlobalContext *global); + +static term nif_atomvm_platform(Context *ctx, int argc, term argv[]) +{ + UNUSED(ctx); + UNUSED(argc); + UNUSED(argv); + return WASI_ATOM; +} + +static term nif_atomvm_random_impl(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + uint32_t r; + if (UNLIKELY(wasi_get_random((uint8_t *) &r, sizeof(r)) != 0)) { + RAISE_ERROR(LOW_ENTROPY_ATOM); + } + + return term_from_int32((int32_t) r); +} + +static term nif_atomvm_random_bytes(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + VALIDATE_VALUE(argv[0], term_is_integer); + avm_int_t len = term_to_int(argv[0]); + if (len < 0 || len > INT_MAX) { + RAISE_ERROR(BADARG_ATOM); + } + + if (UNLIKELY(memory_ensure_free(ctx, term_binary_data_size_in_terms(len) + BINARY_HEADER_SIZE) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + term result = term_create_empty_binary(len, &ctx->heap, ctx->global); + uint8_t *data = (uint8_t *) term_binary_data(result); + + if (UNLIKELY(wasi_get_random(data, (size_t) len) != 0)) { + RAISE_ERROR(LOW_ENTROPY_ATOM); + } + + return result; +} + +static const struct Nif atomvm_platform_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_atomvm_platform +}; + +static const struct Nif atomvm_random_bytes_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_atomvm_random_bytes +}; + +static const struct Nif atomvm_random_impl_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_atomvm_random_impl +}; + +const struct Nif *platform_nifs_get_nif(const char *nifname) +{ + const struct Nif *nif = get_crypto_nif(nifname, NULL); + if (nif != NULL) { + return nif; + } + + if (strcmp("atomvm:platform/0", nifname) == 0) { + return &atomvm_platform_nif; + } + if (strcmp("atomvm:random/0", nifname) == 0) { + return &atomvm_random_impl_nif; + } + if (strcmp("atomvm:random_bytes/1", nifname) == 0) { + return &atomvm_random_bytes_nif; + } + +#ifdef __wasip2__ + nif = otp_net_nif_get_nif(nifname); + if (nif != NULL) { + return nif; + } + nif = otp_socket_nif_get_nif(nifname); + if (nif != NULL) { + return nif; + } +#if ATOMVM_HAS_MBEDTLS + nif = otp_ssl_nif_get_nif(nifname); + if (nif != NULL) { + return nif; + } +#endif +#endif + + return NULL; +} + +#if ATOMVM_HAS_MBEDTLS +#include + +static const struct Nif *get_crypto_nif(const char *nifname, GlobalContext *global) +{ + UNUSED(global); + return otp_crypto_nif_get_nif(nifname); +} +#else +static const struct Nif *get_crypto_nif(const char *nifname, GlobalContext *global) +{ + UNUSED(nifname); + UNUSED(global); + return NULL; +} +#endif diff --git a/src/platforms/wasi/src/lib/smp.c b/src/platforms/wasi/src/lib/smp.c new file mode 100644 index 0000000000..4b7c82168a --- /dev/null +++ b/src/platforms/wasi/src/lib/smp.c @@ -0,0 +1,243 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include "smp.h" + +#ifndef AVM_NO_SMP + +#include +#include +#include +#include + +#include +#include + +struct Mutex +{ + pthread_mutex_t mutex; +}; + +struct CondVar +{ + pthread_cond_t condvar; +}; + +struct RWLock +{ + pthread_rwlock_t lock; +}; + +/* Track scheduler thread handles so smp_scheduler_join_all can join them. + * Mirrors the generic_unix implementation: a sub-thread may still execute + * JIT epilogue code after decrementing running_schedulers; joining prevents + * use-after-free on resources the main thread is about to destroy. */ +struct SchedulerThreadList +{ + pthread_t thread; + struct SchedulerThreadList *next; +}; +static pthread_mutex_t scheduler_threads_lock = PTHREAD_MUTEX_INITIALIZER; +static struct SchedulerThreadList *scheduler_threads = NULL; + +static _Thread_local bool g_sub_main_thread = false; + +static void *scheduler_thread_entry_point(void *arg) +{ + g_sub_main_thread = true; + return (void *) (uintptr_t) scheduler_entry_point((GlobalContext *) arg); +} + +void smp_scheduler_start(GlobalContext *ctx) +{ + /* Allocate the bookkeeping node before spawning the thread so an OOM + * here cannot leak a running scheduler thread. */ + struct SchedulerThreadList *node = malloc(sizeof(*node)); + if (IS_NULL_PTR(node)) { + AVM_ABORT(); + } + if (UNLIKELY(pthread_create(&node->thread, NULL, scheduler_thread_entry_point, ctx))) { + free(node); + AVM_ABORT(); + } + pthread_mutex_lock(&scheduler_threads_lock); + node->next = scheduler_threads; + scheduler_threads = node; + pthread_mutex_unlock(&scheduler_threads_lock); +} + +void smp_scheduler_join_all(void) +{ + struct SchedulerThreadList *list; + pthread_mutex_lock(&scheduler_threads_lock); + list = scheduler_threads; + scheduler_threads = NULL; + pthread_mutex_unlock(&scheduler_threads_lock); + while (list) { + struct SchedulerThreadList *next = list->next; + (void) pthread_join(list->thread, NULL); + free(list); + list = next; + } +} + +bool smp_is_main_thread(GlobalContext *glb) +{ + UNUSED(glb); + return !g_sub_main_thread; +} + +Mutex *smp_mutex_create(void) +{ + Mutex *result = malloc(sizeof(Mutex)); + if (UNLIKELY(result == NULL)) { + AVM_ABORT(); + } + if (UNLIKELY(pthread_mutex_init(&result->mutex, NULL))) { + AVM_ABORT(); + } + return result; +} + +void smp_mutex_destroy(Mutex *mtx) +{ + if (UNLIKELY(pthread_mutex_destroy(&mtx->mutex))) { + AVM_ABORT(); + } + free(mtx); +} + +void smp_mutex_lock(Mutex *mtx) +{ + if (UNLIKELY(pthread_mutex_lock(&mtx->mutex))) { + AVM_ABORT(); + } +} + +bool smp_mutex_trylock(Mutex *mtx) +{ + int r = pthread_mutex_trylock(&mtx->mutex); + return r == 0; +} + +void smp_mutex_unlock(Mutex *mtx) +{ + if (UNLIKELY(pthread_mutex_unlock(&mtx->mutex))) { + AVM_ABORT(); + } +} + +CondVar *smp_condvar_create(void) +{ + CondVar *result = malloc(sizeof(CondVar)); + if (UNLIKELY(result == NULL && sizeof(CondVar) > 0)) { + AVM_ABORT(); + } + if (UNLIKELY(pthread_cond_init(&result->condvar, NULL))) { + AVM_ABORT(); + } + return result; +} + +void smp_condvar_destroy(CondVar *cv) +{ + if (UNLIKELY(pthread_cond_destroy(&cv->condvar))) { + AVM_ABORT(); + } + free(cv); +} + +void smp_condvar_wait(CondVar *cv, Mutex *mtx) +{ + if (UNLIKELY(pthread_cond_wait(&cv->condvar, &mtx->mutex))) { + AVM_ABORT(); + } +} + +void smp_condvar_signal(CondVar *cv) +{ + if (UNLIKELY(pthread_cond_signal(&cv->condvar))) { + AVM_ABORT(); + } +} + +RWLock *smp_rwlock_create(void) +{ + RWLock *result = malloc(sizeof(RWLock)); + if (UNLIKELY(result == NULL && sizeof(RWLock) > 0)) { + AVM_ABORT(); + } + if (UNLIKELY(pthread_rwlock_init(&result->lock, NULL))) { + AVM_ABORT(); + } + return result; +} + +void smp_rwlock_destroy(RWLock *lock) +{ + if (UNLIKELY(pthread_rwlock_destroy(&lock->lock))) { + AVM_ABORT(); + } + free(lock); +} + +void smp_rwlock_rdlock(RWLock *lock) +{ + if (UNLIKELY(pthread_rwlock_rdlock(&lock->lock))) { + AVM_ABORT(); + } +} + +bool smp_rwlock_tryrdlock(RWLock *lock) +{ + int r = pthread_rwlock_tryrdlock(&lock->lock); + if (r == EBUSY) { + return false; + } + if (UNLIKELY(r)) { + AVM_ABORT(); + } + return true; +} + +void smp_rwlock_wrlock(RWLock *lock) +{ + if (UNLIKELY(pthread_rwlock_wrlock(&lock->lock))) { + AVM_ABORT(); + } +} + +void smp_rwlock_unlock(RWLock *lock) +{ + if (UNLIKELY(pthread_rwlock_unlock(&lock->lock))) { + AVM_ABORT(); + } +} + +int smp_get_online_processors(void) +{ + long nprocs = sysconf(_SC_NPROCESSORS_ONLN); + if (nprocs < 1) { + return 1; + } + return (int) nprocs; +} + +#endif /* AVM_NO_SMP */ diff --git a/src/platforms/wasi/src/lib/sys.c b/src/platforms/wasi/src/lib/sys.c new file mode 100644 index 0000000000..f743830d23 --- /dev/null +++ b/src/platforms/wasi/src/lib/sys.c @@ -0,0 +1,790 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#ifdef __wasip2__ +#include +#include +#include + +#include +#include +#include +#include +#endif + +#if ATOMVM_HAS_MBEDTLS +#include +#include +#include +#if defined(MBEDTLS_VERSION_NUMBER) && (MBEDTLS_VERSION_NUMBER >= 0x03000000) +#include +#else +#include +#endif +#include "otp_ssl.h" +#include "sys_mbedtls.h" +#endif + +#include "platform_defaultatoms.h" +#include "wasi_sys.h" + +#define WASI_POLL_COUNT_DIRTY (-1) + +// Keep the access pattern explicit so the code is ready for a SMP + network +#ifndef AVM_NO_SMP +#include +#define WASI_POLL_COUNT_LOAD(p) atomic_load_explicit((p), memory_order_acquire) +#define WASI_POLL_COUNT_STORE(p, v) atomic_store_explicit((p), (v), memory_order_release) +#else +#define WASI_POLL_COUNT_LOAD(p) (*(p)) +#define WASI_POLL_COUNT_STORE(p, v) (*(p) = (v)) +#endif + +struct WASIPlatformData +{ +#ifndef AVM_NO_SMP + pthread_mutex_t poll_mutex; + pthread_cond_t poll_cond; + bool should_poll; +#if ATOMVM_HAS_MBEDTLS + Mutex *entropy_mutex; + Mutex *random_mutex; +#endif +#endif +#ifdef __wasip2__ + struct pollfd *fds; + int ATOMIC listeners_poll_count; + int ATOMIC select_events_poll_count; +#endif +#if ATOMVM_HAS_MBEDTLS + mbedtls_entropy_context entropy_ctx; + bool entropy_is_initialized; + mbedtls_ctr_drbg_context random_ctx; + bool random_is_initialized; +#endif +}; + +#if ATOMVM_HAS_MBEDTLS +static inline void wasi_entropy_lock(struct WASIPlatformData *platform) +{ +#ifndef AVM_NO_SMP + SMP_MUTEX_LOCK(platform->entropy_mutex); +#else + UNUSED(platform); +#endif +} + +static inline void wasi_entropy_unlock(struct WASIPlatformData *platform) +{ +#ifndef AVM_NO_SMP + SMP_MUTEX_UNLOCK(platform->entropy_mutex); +#else + UNUSED(platform); +#endif +} + +static inline void wasi_random_lock(struct WASIPlatformData *platform) +{ +#ifndef AVM_NO_SMP + SMP_MUTEX_LOCK(platform->random_mutex); +#else + UNUSED(platform); +#endif +} + +static inline void wasi_random_unlock(struct WASIPlatformData *platform) +{ +#ifndef AVM_NO_SMP + SMP_MUTEX_UNLOCK(platform->random_mutex); +#else + UNUSED(platform); +#endif +} +#endif + +#ifdef __wasip2__ +static void event_listener_add_to_polling_set(struct EventListener *listener, GlobalContext *global); +static void listener_event_remove_from_polling_set(listener_event_t listener_fd, GlobalContext *global); +static bool event_listener_is_event(EventListener *listener, listener_event_t event); + +#include +#endif + +void sys_init_platform(GlobalContext *glb) +{ + struct WASIPlatformData *platform = malloc(sizeof(struct WASIPlatformData)); + if (IS_NULL_PTR(platform)) { + fprintf(stderr, "Cannot allocate platform data\n"); + AVM_ABORT(); + } +#ifndef AVM_NO_SMP + if (UNLIKELY(pthread_mutex_init(&platform->poll_mutex, NULL))) { + fprintf(stderr, "Cannot initialize pthread_mutex\n"); + AVM_ABORT(); + } + pthread_condattr_t cond_attr; + if (UNLIKELY(pthread_condattr_init(&cond_attr))) { + fprintf(stderr, "Cannot initialize pthread_condattr\n"); + AVM_ABORT(); + } + if (UNLIKELY(pthread_condattr_setclock(&cond_attr, CLOCK_MONOTONIC))) { + fprintf(stderr, "Cannot set pthread_condattr clock\n"); + AVM_ABORT(); + } + if (UNLIKELY(pthread_cond_init(&platform->poll_cond, &cond_attr))) { + fprintf(stderr, "Cannot initialize pthread_cond\n"); + AVM_ABORT(); + } + pthread_condattr_destroy(&cond_attr); + platform->should_poll = false; +#if ATOMVM_HAS_MBEDTLS + platform->entropy_mutex = smp_mutex_create(); + if (IS_NULL_PTR(platform->entropy_mutex)) { + AVM_ABORT(); + } + platform->random_mutex = smp_mutex_create(); + if (IS_NULL_PTR(platform->random_mutex)) { + AVM_ABORT(); + } +#endif +#endif +#ifdef __wasip2__ + platform->fds = NULL; + WASI_POLL_COUNT_STORE(&platform->listeners_poll_count, WASI_POLL_COUNT_DIRTY); + WASI_POLL_COUNT_STORE(&platform->select_events_poll_count, WASI_POLL_COUNT_DIRTY); +#endif +#if ATOMVM_HAS_MBEDTLS + platform->entropy_is_initialized = false; + platform->random_is_initialized = false; +#endif + + glb->platform_data = platform; + +#ifdef __wasip2__ + otp_net_init(glb); + otp_socket_init(glb); +#if ATOMVM_HAS_MBEDTLS + otp_ssl_init(glb); +#endif +#endif +} + +void sys_free_platform(GlobalContext *glb) +{ + struct WASIPlatformData *platform = glb->platform_data; +#ifndef AVM_NO_SMP + pthread_cond_destroy(&platform->poll_cond); + pthread_mutex_destroy(&platform->poll_mutex); +#if ATOMVM_HAS_MBEDTLS + smp_mutex_destroy(platform->entropy_mutex); + smp_mutex_destroy(platform->random_mutex); +#endif +#endif +#ifdef __wasip2__ + free(platform->fds); +#endif +#if ATOMVM_HAS_MBEDTLS + if (platform->random_is_initialized) { + mbedtls_ctr_drbg_free(&platform->random_ctx); + } + if (platform->entropy_is_initialized) { + mbedtls_entropy_free(&platform->entropy_ctx); + } +#endif + free(platform); +} + +static void sleep_ms(int timeout_ms) +{ + if (timeout_ms <= 0) { + return; + } + struct timespec req; + req.tv_sec = timeout_ms / 1000; + req.tv_nsec = (long) (timeout_ms % 1000) * 1000000L; + nanosleep(&req, NULL); +} + +#ifdef __wasip2__ +static void sys_poll_events_with_poll(GlobalContext *glb, int timeout_ms) +{ + struct WASIPlatformData *platform = glb->platform_data; + struct pollfd *fds = platform->fds; + int listeners_poll_count = WASI_POLL_COUNT_LOAD(&platform->listeners_poll_count); + int select_events_poll_count = WASI_POLL_COUNT_LOAD(&platform->select_events_poll_count); + + if (listeners_poll_count < 0 || select_events_poll_count < 0) { + struct ListHead *select_events = synclist_wrlock(&glb->select_events); + size_t select_events_new_count; + if (select_events_poll_count < 0) { + select_event_count_and_destroy_closed(select_events, NULL, NULL, &select_events_new_count, glb); + } else { + select_events_new_count = select_events_poll_count; + } + + size_t listeners_new_count = 0; + struct ListHead *listeners = NULL; + struct ListHead *item; + if (listeners_poll_count < 0) { + listeners = synclist_rdlock(&glb->listeners); + LIST_FOR_EACH (item, listeners) { + EventListener *listener = GET_LIST_ENTRY(item, EventListener, listeners_list_head); + if (listener->fd >= 0) { + listeners_new_count++; + } + } + } else { + listeners_new_count = listeners_poll_count; + } + + size_t new_size = sizeof(struct pollfd) * (select_events_new_count + listeners_new_count); + struct pollfd *new_fds = realloc(fds, new_size); + if (UNLIKELY(new_fds == NULL && new_size > 0)) { + /* Original fds is still valid; leave platform->fds untouched. */ + fprintf(stderr, "Cannot allocate pollfd array\n"); + AVM_ABORT(); + } + fds = new_fds; + platform->fds = fds; + + int fd_index = 0; + if (listeners_poll_count < 0) { + LIST_FOR_EACH (item, listeners) { + EventListener *listener = GET_LIST_ENTRY(item, EventListener, listeners_list_head); + if (listener->fd >= 0) { + fds[fd_index].fd = listener->fd; + fds[fd_index].events = POLLIN; + fds[fd_index].revents = 0; + fd_index++; + } + } + WASI_POLL_COUNT_STORE(&platform->listeners_poll_count, (int) listeners_new_count); + synclist_unlock(&glb->listeners); + } else { + fd_index += listeners_new_count; + } + + LIST_FOR_EACH (item, select_events) { + struct SelectEvent *select_event = GET_LIST_ENTRY(item, struct SelectEvent, head); + if (select_event->read || select_event->write) { + fds[fd_index].fd = select_event->event; + fds[fd_index].events = (select_event->read ? POLLIN : 0) | (select_event->write ? POLLOUT : 0); + fds[fd_index].revents = 0; + fd_index++; + } + } + WASI_POLL_COUNT_STORE(&platform->select_events_poll_count, (int) select_events_new_count); + synclist_unlock(&glb->select_events); + + listeners_poll_count = listeners_new_count; + select_events_poll_count = select_events_new_count; + } + + int poll_count = listeners_poll_count + select_events_poll_count; + if (poll_count == 0) { + if (timeout_ms > 0) { + sleep_ms(timeout_ms); + } + return; + } + + int nb_descriptors = poll(fds, poll_count, timeout_ms); + if (UNLIKELY(nb_descriptors < 0)) { + if (errno == EINTR) { + return; + } + // EBADF / EINVAL / EFAULT here mean a listener or select-event fd has + // gone bad: the cached pollfd array is no longer valid. + fprintf(stderr, "poll() failed: errno=%i\n", errno); + WASI_POLL_COUNT_STORE(&platform->listeners_poll_count, WASI_POLL_COUNT_DIRTY); + WASI_POLL_COUNT_STORE(&platform->select_events_poll_count, WASI_POLL_COUNT_DIRTY); + return; + } + + // poll() also signals POLLERR / POLLHUP / POLLNVAL in revents even if + // we did not request them in events; treat those as wake-ups so the + // waiting handler can observe the error / peer-close + const short revents_mask = POLLIN | POLLOUT | POLLERR | POLLHUP | POLLNVAL; + + int fd_index = 0; + if (nb_descriptors > 0) { + struct ListHead *listeners = synclist_wrlock(&glb->listeners); + struct ListHead *item = listeners->next; + struct ListHead *previous = listeners; + for (int i = 0; i < listeners_poll_count && nb_descriptors > 0; i++, fd_index++) { + if (!(fds[fd_index].revents & revents_mask)) { + continue; + } + fds[fd_index].revents = 0; + nb_descriptors--; + process_listener_handler(glb, fds[fd_index].fd, listeners, &item, &previous); + } + synclist_unlock(&glb->listeners); + } else { + fd_index += listeners_poll_count; + } + + for (int i = 0; i < select_events_poll_count && nb_descriptors > 0; i++, fd_index++) { + if (!(fds[fd_index].revents & revents_mask)) { + continue; + } + short revents = fds[fd_index].revents; + bool error_event = revents & (POLLERR | POLLHUP | POLLNVAL); + bool is_read = (revents & POLLIN) || error_event; + bool is_write = (revents & POLLOUT) || error_event; + fds[fd_index].revents = 0; + nb_descriptors--; + select_event_notify(fds[fd_index].fd, is_read, is_write, glb); + } +} +#endif + +void sys_poll_events(GlobalContext *glb, int timeout_ms) +{ +#ifdef __wasip2__ + if (timeout_ms == 0 + && synclist_is_empty(&glb->listeners) + && synclist_is_empty(&glb->select_events)) { + return; + } + sys_poll_events_with_poll(glb, timeout_ms); +#elif !defined(AVM_NO_SMP) + struct WASIPlatformData *platform = glb->platform_data; + + if (timeout_ms > 0) { + struct timespec abstime; + sys_monotonic_time(&abstime); + int timeout_secs_part = timeout_ms / 1000; + int timeout_ms_part = timeout_ms - (timeout_secs_part * 1000); + abstime.tv_nsec += timeout_ms_part * 1000000; + if (abstime.tv_nsec >= 1000000000) { + timeout_secs_part += 1; + abstime.tv_nsec -= 1000000000; + } + abstime.tv_sec += timeout_secs_part; + pthread_mutex_lock(&platform->poll_mutex); + if (!platform->should_poll) { + (void) pthread_cond_timedwait(&platform->poll_cond, &platform->poll_mutex, &abstime); + } + } else if (timeout_ms < 0) { + pthread_mutex_lock(&platform->poll_mutex); + if (!platform->should_poll) { + (void) pthread_cond_wait(&platform->poll_cond, &platform->poll_mutex); + } + } else { + pthread_mutex_lock(&platform->poll_mutex); + } + platform->should_poll = false; + pthread_mutex_unlock(&platform->poll_mutex); +#else + sleep_ms(timeout_ms); + UNUSED(glb); +#endif +} + +#if !defined(AVM_NO_SMP) || defined(AVM_TASK_DRIVER_ENABLED) +void sys_signal(GlobalContext *glb) +{ +#ifndef AVM_NO_SMP + struct WASIPlatformData *platform = glb->platform_data; + pthread_mutex_lock(&platform->poll_mutex); + platform->should_poll = true; + pthread_cond_signal(&platform->poll_cond); + pthread_mutex_unlock(&platform->poll_mutex); +#else + UNUSED(glb); +#endif +} +#endif + +void sys_wakeup(GlobalContext *glb) +{ +#ifndef AVM_NO_SMP + struct WASIPlatformData *platform = glb->platform_data; + pthread_mutex_lock(&platform->poll_mutex); + platform->should_poll = true; + pthread_cond_signal(&platform->poll_cond); + pthread_mutex_unlock(&platform->poll_mutex); +#else + UNUSED(glb); +#endif +} + +// No-op tzset for nifs.c compatibility +void tzset(void) {} + +#ifdef __wasip2__ +static void event_listener_add_to_polling_set(struct EventListener *listener, GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + UNUSED(listener); + WASI_POLL_COUNT_STORE(&platform->listeners_poll_count, WASI_POLL_COUNT_DIRTY); +} + +static void listener_event_remove_from_polling_set(listener_event_t listener_fd, GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + UNUSED(listener_fd); + WASI_POLL_COUNT_STORE(&platform->listeners_poll_count, WASI_POLL_COUNT_DIRTY); +} + +bool event_listener_is_event(EventListener *listener, listener_event_t event) +{ + return listener->fd == event; +} + +void sys_register_listener(GlobalContext *global, struct EventListener *listener) +{ + struct ListHead *listeners = synclist_wrlock(&global->listeners); + event_listener_add_to_polling_set(listener, global); + list_append(listeners, &listener->listeners_list_head); + synclist_unlock(&global->listeners); +} + +void sys_unregister_listener(GlobalContext *global, struct EventListener *listener) +{ + listener_event_remove_from_polling_set(listener->fd, global); + synclist_remove(&global->listeners, &listener->listeners_list_head); +} + +void sys_register_select_event(GlobalContext *global, ErlNifEvent event, bool is_write) +{ + struct WASIPlatformData *platform = global->platform_data; + UNUSED(event); + UNUSED(is_write); + WASI_POLL_COUNT_STORE(&platform->select_events_poll_count, WASI_POLL_COUNT_DIRTY); +} + +void sys_unregister_select_event(GlobalContext *global, ErlNifEvent event, bool is_write) +{ + struct WASIPlatformData *platform = global->platform_data; + UNUSED(event); + UNUSED(is_write); + WASI_POLL_COUNT_STORE(&platform->select_events_poll_count, WASI_POLL_COUNT_DIRTY); +} +#else +void sys_listener_destroy(struct ListHead *item) +{ + UNUSED(item); +} + +void sys_register_select_event(GlobalContext *global, ErlNifEvent event, bool is_write) +{ + UNUSED(global); + UNUSED(event); + UNUSED(is_write); +} + +void sys_unregister_select_event(GlobalContext *global, ErlNifEvent event, bool is_write) +{ + UNUSED(global); + UNUSED(event); + UNUSED(is_write); +} + +void sys_register_listener(GlobalContext *global, EventListener *listener) +{ + UNUSED(global); + UNUSED(listener); +} + +void sys_unregister_listener(GlobalContext *global, EventListener *listener) +{ + UNUSED(global); + UNUSED(listener); +} +#endif + +void sys_time(struct timespec *t) +{ + if (UNLIKELY(clock_gettime(CLOCK_REALTIME, t))) { + fprintf(stderr, "Failed clock_gettime.\n"); + AVM_ABORT(); + } +} + +void sys_monotonic_time(struct timespec *t) +{ + if (UNLIKELY(clock_gettime(CLOCK_MONOTONIC, t))) { + fprintf(stderr, "Failed clock_gettime.\n"); + AVM_ABORT(); + } +} + +uint64_t sys_monotonic_time_u64(void) +{ + struct timespec ts; + if (UNLIKELY(clock_gettime(CLOCK_MONOTONIC, &ts))) { + fprintf(stderr, "Failed clock_gettime.\n"); + AVM_ABORT(); + } + return (uint64_t) ts.tv_sec * 1000000000ULL + (uint64_t) ts.tv_nsec; +} + +uint64_t sys_monotonic_time_ms_to_u64(uint64_t ms) +{ + return ms * 1000000; +} + +uint64_t sys_monotonic_time_u64_to_ms(uint64_t t) +{ + return t / 1000000; +} + +static void load_file_log_error(const char *path, const char *what, int err) +{ + if (err != 0) { + fprintf(stderr, "load_file: %s failed for %s: %s\n", what, path, strerror(err)); + } else { + fprintf(stderr, "load_file: %s failed for %s\n", what, path); + } +} + +static void *load_file(const char *path, size_t *out_size) +{ + int fd = open(path, O_RDONLY); + if (fd < 0) { + load_file_log_error(path, "open", errno); + return NULL; + } + off_t fsize = lseek(fd, 0, SEEK_END); + if (fsize < 0) { + load_file_log_error(path, "lseek", errno); + close(fd); + return NULL; + } + lseek(fd, 0, SEEK_SET); + void *data = malloc((size_t) fsize); + if (IS_NULL_PTR(data)) { + load_file_log_error(path, "malloc", 0); + close(fd); + return NULL; + } + size_t remaining = (size_t) fsize; + uint8_t *ptr = (uint8_t *) data; + while (remaining > 0) { + ssize_t r = read(fd, ptr, remaining); + if (UNLIKELY(r <= 0)) { + load_file_log_error(path, r < 0 ? "read" : "read (short)", r < 0 ? errno : 0); + close(fd); + free(data); + return NULL; + } + ptr += r; + remaining -= (size_t) r; + } + close(fd); + if (out_size) { + *out_size = (size_t) fsize; + } + return data; +} + +enum OpenAVMResult sys_open_avm_from_file( + GlobalContext *global, const char *path, struct AVMPackData **avm_data) +{ + TRACE("sys_open_avm_from_file: Going to open: %s\n", path); + UNUSED(global); + + size_t size; + void *data = load_file(path, &size); + if (IS_NULL_PTR(data)) { + return AVM_OPEN_CANNOT_OPEN; + } + if (UNLIKELY(!avmpack_is_valid(data, size))) { + free(data); + return AVM_OPEN_INVALID; + } + + struct ConstAVMPack *const_avm = malloc(sizeof(struct ConstAVMPack)); + if (IS_NULL_PTR(const_avm)) { + free(data); + return AVM_OPEN_FAILED_ALLOC; + } + avmpack_data_init(&const_avm->base, &const_avm_pack_info); + const_avm->base.data = (const uint8_t *) data; + + *avm_data = &const_avm->base; + return AVM_OPEN_OK; +} + +Module *sys_load_module_from_file(GlobalContext *global, const char *path) +{ + TRACE("sys_load_module_from_file: Going to load: %s\n", path); + + size_t size; + void *data = load_file(path, &size); + if (IS_NULL_PTR(data)) { + return NULL; + } + if (UNLIKELY(!iff_is_valid_beam(data))) { + fprintf(stderr, "%s is not a valid BEAM file.\n", path); + free(data); + return NULL; + } + Module *new_module = module_new_from_iff_binary(global, data, size); + if (IS_NULL_PTR(new_module)) { + free(data); + return NULL; + } + new_module->module_platform_data = data; + + return new_module; +} + +Context *sys_create_port(GlobalContext *glb, const char *driver_name, term opts) +{ + UNUSED(glb); + UNUSED(driver_name); + UNUSED(opts); + return NULL; +} + +term sys_get_info(Context *ctx, term key) +{ + UNUSED(ctx); + UNUSED(key); + return UNDEFINED_ATOM; +} + +#if ATOMVM_HAS_MBEDTLS + +mbedtls_ms_time_t mbedtls_ms_time(void) +{ + struct timespec ts; + if (UNLIKELY(clock_gettime(CLOCK_MONOTONIC, &ts) != 0)) { + AVM_ABORT(); + } + return (mbedtls_ms_time_t) ts.tv_sec * 1000 + (mbedtls_ms_time_t) ts.tv_nsec / 1000000; +} + +int sys_mbedtls_entropy_func(void *entropy, unsigned char *buf, size_t size) +{ +#ifndef MBEDTLS_THREADING_C + struct WASIPlatformData *platform + = CONTAINER_OF(entropy, struct WASIPlatformData, entropy_ctx); + wasi_entropy_lock(platform); + int result = mbedtls_entropy_func(entropy, buf, size); + wasi_entropy_unlock(platform); + return result; +#else + return mbedtls_entropy_func(entropy, buf, size); +#endif +} + +mbedtls_entropy_context *sys_mbedtls_get_entropy_context_lock(GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + wasi_entropy_lock(platform); + if (!platform->entropy_is_initialized) { + mbedtls_entropy_init(&platform->entropy_ctx); + platform->entropy_is_initialized = true; + } + return &platform->entropy_ctx; +} + +void sys_mbedtls_entropy_context_unlock(GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + wasi_entropy_unlock(platform); +} + +mbedtls_ctr_drbg_context *sys_mbedtls_get_ctr_drbg_context_lock(GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + wasi_random_lock(platform); + if (!platform->random_is_initialized) { + mbedtls_ctr_drbg_init(&platform->random_ctx); + mbedtls_entropy_context *entropy_ctx = sys_mbedtls_get_entropy_context_lock(global); + sys_mbedtls_entropy_context_unlock(global); + const char *seed = "AtomVM Mbed-TLS initial seed."; + int seed_len = strlen(seed); + int seed_err = mbedtls_ctr_drbg_seed(&platform->random_ctx, sys_mbedtls_entropy_func, + entropy_ctx, (const unsigned char *) seed, seed_len); + if (UNLIKELY(seed_err != 0)) { + wasi_random_unlock(platform); + AVM_ABORT(); + } + platform->random_is_initialized = true; + } + return &platform->random_ctx; +} + +void sys_mbedtls_ctr_drbg_context_unlock(GlobalContext *global) +{ + struct WASIPlatformData *platform = global->platform_data; + wasi_random_unlock(platform); +} + +int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen) +{ + UNUSED(data); + + if (UNLIKELY(wasi_get_random(output, len) != 0)) { + return -1; + } + *olen = len; + return 0; +} + +#endif /* ATOMVM_HAS_MBEDTLS */ + +int wasi_get_random(uint8_t *output, size_t len) +{ + size_t total = 0; + while (total < len) { + size_t chunk = len - total; + if (chunk > 256) { + chunk = 256; + } + if (UNLIKELY(getentropy(output + total, chunk) != 0)) { + return -1; + } + total += chunk; + } + return 0; +} diff --git a/src/platforms/wasi/src/lib/wasi_compat.h b/src/platforms/wasi/src/lib/wasi_compat.h new file mode 100644 index 0000000000..f4525927aa --- /dev/null +++ b/src/platforms/wasi/src/lib/wasi_compat.h @@ -0,0 +1,27 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef WASI_COMPAT_H_ +#define WASI_COMPAT_H_ + +// Define no-op tzset for compatibility +extern void tzset(void); + +#endif /* WASI_COMPAT_H_ */ diff --git a/src/platforms/wasi/src/lib/wasi_sys.h b/src/platforms/wasi/src/lib/wasi_sys.h new file mode 100644 index 0000000000..34ab1442a4 --- /dev/null +++ b/src/platforms/wasi/src/lib/wasi_sys.h @@ -0,0 +1,58 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#ifndef WASI_SYS_H_ +#define WASI_SYS_H_ + +#include +#include +#include +#include +#include + +#ifdef __wasip2__ +typedef int listener_event_t; + +struct EventListener +{ + struct ListHead listeners_list_head; + event_handler_t handler; + listener_event_t fd; +}; +#endif + +/** + * @brief Signal the event poll to wake up. + * @details Only used internally by the WASI platform; not part of the sys.h API. + * @param glb the global context + */ +void sys_wakeup(GlobalContext *glb); + +/** + * @brief Fill an output buffer using getentropy(3) in 256-byte chunks. + * @details getentropy(3) (per POSIX/WASI) refuses requests larger than 256 + * bytes; this helper hides the chunking from callers. + * @param output destination buffer + * @param len number of bytes to fill + * @return 0 on success, -1 on failure (errno set by getentropy) + */ +int wasi_get_random(uint8_t *output, size_t len); + +#endif /* WASI_SYS_H_ */ diff --git a/src/platforms/wasi/src/main.c b/src/platforms/wasi/src/main.c new file mode 100644 index 0000000000..9116c4d517 --- /dev/null +++ b/src/platforms/wasi/src/main.c @@ -0,0 +1,147 @@ +/* + * This file is part of AtomVM. + * + * Copyright 2026 Paul Guyot + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "wasi_sys.h" + +static GlobalContext *global = NULL; +static Module *main_module = NULL; + +/** + * @brief Load a module in .avm or .beam format. + * @param path Path to the module or avm package to load. + * @return EXIT_SUCCESS or EXIT_FAILURE which is then returned by main + */ +static int load_module(const char *path) +{ + const char *ext = strrchr(path, '.'); + if (ext && strcmp(ext, ".avm") == 0) { + struct AVMPackData *avmpack_data; + if (sys_open_avm_from_file(global, path, &avmpack_data) != AVM_OPEN_OK) { + fprintf(stderr, "Failed opening %s.\n", path); + return EXIT_FAILURE; + } + synclist_append(&global->avmpack_data, &avmpack_data->avmpack_head); + if (IS_NULL_PTR(main_module)) { + const void *startup_beam = NULL; + uint32_t startup_beam_size; + const char *startup_module_name; + avmpack_find_section_by_flag(avmpack_data->data, BEAM_START_FLAG, BEAM_START_FLAG, &startup_beam, &startup_beam_size, &startup_module_name); + if (startup_beam) { + avmpack_data->in_use = true; + main_module = module_new_from_iff_binary(global, startup_beam, startup_beam_size); + if (IS_NULL_PTR(main_module)) { + fprintf(stderr, "Cannot load startup module: %s\n", startup_module_name); + return EXIT_FAILURE; + } + globalcontext_insert_module(global, main_module); + main_module->module_platform_data = NULL; + } + } + } else if (ext && strcmp(ext, ".beam") == 0) { + Module *module = sys_load_module_from_file(global, path); + if (IS_NULL_PTR(module)) { + fprintf(stderr, "Failed loading %s.\n", path); + return EXIT_FAILURE; + } + globalcontext_insert_module(global, module); + if (IS_NULL_PTR(main_module) && module_search_exported_function(module, START_ATOM_INDEX, 0) != 0) { + main_module = module; + } + } else { + fprintf(stderr, "%s is not an AVM or BEAM file.\n", path); + return EXIT_FAILURE; + } + return EXIT_SUCCESS; +} + +/** + * @brief Create first context and run main module's start function + * @return EXIT_SUCCESS or EXIT_FAILURE which is then returned by main + */ +static int start(void) +{ + if (IS_NULL_PTR(main_module)) { + fprintf(stderr, "main module not loaded\n"); + return EXIT_FAILURE; + } + run_result_t ret_value = globalcontext_run(global, main_module, stdout, 0, NULL); + int status; + if (ret_value == RUN_SUCCESS) { + status = EXIT_SUCCESS; + } else { + status = EXIT_FAILURE; + } + return status; +} + +/** + * @brief WASI entry point + * @details Runs the AtomVM interpreter with files passed as arguments. + * WASI runtimes provide preopened directories for file access. + * + * @param argc number of arguments + * @param argv arguments + * @return EXIT_SUCCESS or EXIT_FAILURE + */ +int main(int argc, char **argv) +{ + if (argc < 2) { + fprintf(stderr, "Usage: %s [additional_modules...]\n", argv[0]); + fprintf(stderr, "\nWASI-specific:\n"); + fprintf(stderr, " Make sure to grant directory access with --dir=.\n"); + fprintf(stderr, " Example: wasmtime run --dir=. AtomVM.wasm myapp.avm\n"); + return EXIT_FAILURE; + } + + int result = EXIT_SUCCESS; + global = globalcontext_new(); + if (IS_NULL_PTR(global)) { + fprintf(stderr, "Cannot allocate global context\n"); + return EXIT_FAILURE; + } + + for (int i = 1; i < argc; ++i) { + result = load_module(argv[i]); + if (UNLIKELY(result != EXIT_SUCCESS)) { + break; + } + } + + if (result == EXIT_SUCCESS) { + result = start(); + } + + globalcontext_destroy(global); + global = NULL; + main_module = NULL; + + return result; +} diff --git a/src/platforms/wasi/wasi-sdk.cmake b/src/platforms/wasi/wasi-sdk.cmake new file mode 100644 index 0000000000..ce493a8b3e --- /dev/null +++ b/src/platforms/wasi/wasi-sdk.cmake @@ -0,0 +1,71 @@ +# +# This file is part of AtomVM. +# +# Copyright 2026 Paul Guyot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later +# + +# CMake toolchain file for WASI SDK +# Usage: cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/wasi-sdk.cmake .. + +if(DEFINED ENV{WASI_SDK_PATH}) + set(WASI_SDK_PATH $ENV{WASI_SDK_PATH}) +elseif(EXISTS /opt/local/libexec/wasi-sdk) + set(WASI_SDK_PATH /opt/local/libexec/wasi-sdk) +elseif(EXISTS /opt/wasi-sdk) + set(WASI_SDK_PATH /opt/wasi-sdk) +else() + message(FATAL_ERROR "WASI SDK not found. Install via MacPorts: sudo port install wasi-sdk, or set WASI_SDK_PATH.") +endif() + +set(CMAKE_SYSTEM_NAME WASI) +set(CMAKE_SYSTEM_PROCESSOR wasm32) + +set(CMAKE_C_COMPILER ${WASI_SDK_PATH}/bin/clang) +set(CMAKE_CXX_COMPILER ${WASI_SDK_PATH}/bin/clang++) +set(CMAKE_AR ${WASI_SDK_PATH}/bin/llvm-ar) +set(CMAKE_RANLIB ${WASI_SDK_PATH}/bin/llvm-ranlib) +set(CMAKE_STRIP ${WASI_SDK_PATH}/bin/llvm-strip) + +# Pick the WASI target triple. The toolchain file is re-executed inside +# try_compile() projects, where our parent project's cache (AVM_DISABLE_SMP, +# WASI_TARGET) is NOT inherited - but CMAKE_C_COMPILER_TARGET IS, via +# CMakeCCompiler.cmake. Honour that first so try_compile() invocations don't +# silently drift to a different target. (The matrix of WASI targets vs SMP / +# networking is documented in src/platforms/wasi/CMakeLists.txt.) +if(CMAKE_C_COMPILER_TARGET) + set(WASI_TARGET "${CMAKE_C_COMPILER_TARGET}") +elseif(NOT DEFINED WASI_TARGET) + if(AVM_DISABLE_SMP) + set(WASI_TARGET "wasm32-wasip2" CACHE STRING "WASI target triple") + else() + set(WASI_TARGET "wasm32-wasip1-threads" CACHE STRING "WASI target triple") + endif() +endif() + +set(CMAKE_C_COMPILER_TARGET ${WASI_TARGET}) +set(CMAKE_CXX_COMPILER_TARGET ${WASI_TARGET}) + +set(CMAKE_SYSROOT ${WASI_SDK_PATH}/share/wasi-sysroot) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +set(CMAKE_EXECUTABLE_SUFFIX ".wasm") + +message(STATUS "WASI SDK: ${WASI_SDK_PATH}, target: ${WASI_TARGET}") diff --git a/tests/libs/eavmlib/test_dir.erl b/tests/libs/eavmlib/test_dir.erl index 40e66126eb..da6bf09f17 100644 --- a/tests/libs/eavmlib/test_dir.erl +++ b/tests/libs/eavmlib/test_dir.erl @@ -33,6 +33,11 @@ all_dir_entries(Dir, Acc) -> case atomvm:posix_readdir(Dir) of eof -> [eof | Acc]; - {ok, {dirent, Inode, Name} = Dirent} when is_integer(Inode) and is_binary(Name) -> + {ok, {dirent, Inode, Name} = Dirent} when + (is_integer(Inode) orelse Inode =:= undefined) andalso is_binary(Name) + -> + %% Inode may be `undefined' on platforms whose readdir does not + %% populate `d_ino' (e.g. wasi-libc on wasip1 / wasip2 leaves it + %% at the sentinel value, which posix_readdir maps to undefined). all_dir_entries(Dir, [Dirent | Acc]) end. diff --git a/tests/libs/eavmlib/test_file.erl b/tests/libs/eavmlib/test_file.erl index c69a3a9f24..f1b2907fee 100644 --- a/tests/libs/eavmlib/test_file.erl +++ b/tests/libs/eavmlib/test_file.erl @@ -25,8 +25,9 @@ -include("etest.hrl"). test() -> - HasSelect = atomvm:platform() =/= emscripten, - HasExecve = atomvm:platform() =/= emscripten, + Platform = atomvm:platform(), + HasSelect = Platform =/= emscripten andalso Platform =/= wasi, + HasExecve = Platform =/= emscripten andalso Platform =/= wasi, ok = test_basic_file(), ok = test_fifo_select(HasSelect), ok = test_gc(HasSelect), @@ -45,8 +46,16 @@ test() -> ok = test_subprocess(HasExecve), ok. +%% Build a unique temp file path. +temp_path() -> + Suffix = integer_to_list(erlang:system_time(millisecond)), + case atomvm:platform() of + wasi -> "atomvm.tmp." ++ Suffix; + _ -> "/tmp/atomvm.tmp." ++ Suffix + end. + test_basic_file() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_wronly, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), ok = atomvm:posix_close(Fd), @@ -62,7 +71,7 @@ test_basic_file() -> test_fifo_select(false) -> ok; test_fifo_select(_HasSelect) -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), ok = atomvm:posix_mkfifo(Path, 8#644), {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), @@ -128,7 +137,7 @@ test_fifo_select(_HasSelect) -> % Test is based on the fact that `erlang:memory(binary)` count resources. test_gc(HasSelect) -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), GCSubPid = spawn(fun() -> gc_loop(Path, undefined) end), MemorySize0 = erlang:memory(binary), call_gc_loop(GCSubPid, open), @@ -218,7 +227,7 @@ test_crash_no_leak(false) -> ok; test_crash_no_leak(true) -> Before = erlang:memory(binary), - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), Pid = spawn(fun() -> crash_leak(Path) end), Ref = monitor(process, Pid), ok = @@ -252,7 +261,7 @@ test_select_with_gone_process(false) -> ok; test_select_with_gone_process(true) -> Before = erlang:memory(binary), - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), test_select_with_gone_process0(Path), % GC so Fd's ref count is decremented erlang:garbage_collect(), @@ -303,7 +312,7 @@ test_select_with_listeners(_HasSelect) -> out_of_memory -> {error, enomem}; Result -> Result end, - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), ok = atomvm:posix_mkfifo(Path, 8#644), {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), @@ -324,7 +333,7 @@ test_select_with_listeners(_HasSelect) -> ok. test_seek() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), {ok, 0} = atomvm:posix_seek(Fd, 0, seek_set), @@ -342,7 +351,7 @@ test_seek() -> ok = atomvm:posix_unlink(Path). test_pread_pwrite() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), {ok, <<"He">>} = atomvm:posix_pread(Fd, 2, 0), @@ -356,7 +365,7 @@ test_pread_pwrite() -> ok = atomvm:posix_unlink(Path). test_fsync() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), ok = atomvm:posix_fsync(Fd), @@ -364,7 +373,7 @@ test_fsync() -> ok = atomvm:posix_unlink(Path). test_ftruncate() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 11} = atomvm:posix_write(Fd, <<"Hello World">>), %% Truncate to shorter length @@ -384,7 +393,7 @@ test_ftruncate() -> ok = atomvm:posix_unlink(Path). test_rename() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), NewPath = Path ++ ".renamed", {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), @@ -402,7 +411,7 @@ test_rename() -> ok = atomvm:posix_unlink(NewPath). test_stat() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 11} = atomvm:posix_write(Fd, <<"Hello World">>), ok = atomvm:posix_close(Fd), @@ -431,7 +440,7 @@ test_stat() -> ok = atomvm:posix_unlink(Path). test_fstat() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 11} = atomvm:posix_write(Fd, <<"Hello World">>), {ok, #{ @@ -452,7 +461,7 @@ test_fstat() -> ok = atomvm:posix_unlink(Path). test_mkdir_rmdir() -> - Base = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Base = temp_path(), Dir = Base ++ ".dir", ok = atomvm:posix_mkdir(Dir, 8#755), {ok, #{st_mode := _}} = atomvm:posix_stat(Dir), @@ -470,7 +479,7 @@ test_mkdir_rmdir() -> ok. test_validation() -> - Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + Path = temp_path(), {ok, Fd} = atomvm:posix_open(Path, [o_rdwr, o_creat], 8#644), {ok, 5} = atomvm:posix_write(Fd, <<"Hello">>), ok = expect_badarg(fun() -> atomvm:posix_read(Fd, -1) end), diff --git a/tests/libs/eavmlib/tests.erl b/tests/libs/eavmlib/tests.erl index 1eb41026ba..ded50da85e 100644 --- a/tests/libs/eavmlib/tests.erl +++ b/tests/libs/eavmlib/tests.erl @@ -23,12 +23,15 @@ -export([start/0]). start() -> - etest:test([ + BaseTests = [ test_dir, test_file, - test_http_server, - test_mdns, test_port, - test_timer_manager, - test_ahttp_client - ]). + test_timer_manager + ], + NetworkingTests = + case atomvm:platform() of + wasi -> []; + _ -> [test_http_server, test_mdns, test_ahttp_client] + end, + etest:test(BaseTests ++ NetworkingTests). diff --git a/tests/libs/estdlib/test_udp_socket.erl b/tests/libs/estdlib/test_udp_socket.erl index 359b001bee..852731e147 100644 --- a/tests/libs/estdlib/test_udp_socket.erl +++ b/tests/libs/estdlib/test_udp_socket.erl @@ -31,14 +31,18 @@ test() -> % Workaround for image limitation on CI % https://github.com/actions/runner-images/issues/10924 Platform = erlang:system_info(machine), - System = execute_command(Platform, "uname -s"), TestMulticast = - case System of - "Darwin\n" -> - Version = execute_command(Platform, "uname -r"), - Version < "24"; - _ -> - true + case Platform =:= "ATOM" andalso atomvm:platform() =:= wasi of + true -> + %% wasi:sockets@0.2.x has no multicast API + false; + false -> + case execute_command(Platform, "uname -s") of + "Darwin\n" -> + execute_command(Platform, "uname -r") < "24"; + _ -> + true + end end, if TestMulticast -> diff --git a/tests/libs/estdlib/tests.erl b/tests/libs/estdlib/tests.erl index 922dbe183a..b32fea6406 100644 --- a/tests/libs/estdlib/tests.erl +++ b/tests/libs/estdlib/tests.erl @@ -25,24 +25,26 @@ start() -> OTPVersion = get_otp_version(), NonNetworkingTests = get_non_networking_tests(OTPVersion), - Networking = + NetworkingTests = case OTPVersion of atomvm -> case atomvm:platform() of emscripten -> - false; + []; stm32 -> - false; + []; + wasi -> + %% Only wasm32-wasip2 ships BSD sockets via + %% wasi:sockets@0.2.x; wasip1/wasip1-threads do not. + case erlang:system_info(system_architecture) of + <<"wasm32-wasip2">> -> get_wasip2_networking_tests(); + _ -> [] + end; _ -> - true + get_networking_tests() end; _ -> - true - end, - NetworkingTests = - if - Networking -> get_networking_tests(); - true -> [] + get_networking_tests() end, ok = etest:test(NonNetworkingTests ++ NetworkingTests). @@ -97,3 +99,19 @@ get_networking_tests() -> test_inet, test_net_kernel ]. + +%% Subset of get_networking_tests/0 that runs on wasm32-wasip2. Excluded: +%% - test_epmd, test_net_kernel: need fork/exec (atomvm:subprocess), unavailable on WASI +%% +%% test_gen_tcp / test_gen_udp gate their inet-port-driver subtests on +%% test_inet:is_inet_driver_available/0 and run only the socket backend on WASI. +get_wasip2_networking_tests() -> + [ + test_tcp_socket, + test_udp_socket, + test_net, + test_inet, + test_ssl, + test_gen_tcp, + test_gen_udp + ].