Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added support for configuring pins and width for sdmmc on ESP32
- Added support for map comprehensions
- Added USB CDC port drivers for ESP32, RP2, and STM32 platforms
- Implemented `enif_select_write` (previously declared but not implemented), and reworked
`enif_select`/`enif_select_read`/`enif_select_write` so a resource can hold independent
pending read and write selects (each with its own ref/message/pid) on the same event at
the same time
- Added a Linux `gpio` driver for the generic_unix port (in `avm_unix`) using sysfs

### Changed
Expand Down Expand Up @@ -69,6 +73,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed several underallocation issues that could trigger data corruption on `binary:replace`, `zlib:compress` and bsd socket recv code.
- Fixed a bug where `catch` would raise on regular atom results
- Fixed ESP32 socket driver holding the global socket-list lock across blocking TCP connects, leaking the port on connect failure, losing concurrent `accept` waiters, leaking `netbuf` on receive error paths, and a recycled-`netconn` race between socket close and the event handler
- `socket:send/2,3` now correctly distinguishes transient send backpressure (lwIP `ERR_MEM` / BSD
`EAGAIN`|`EWOULDBLOCK`) from a closed connection at the NIF level, and waits for the socket to
become writable again (using a new `nif_select_write/2`) and retries internally, so callers get
`ok | {error, Reason}` and no longer need to implement their own retry/backoff for partial sends
or backpressure; `ssl:send/2`, `ssl:recv/2` and the TLS handshake/close-notify loops likewise wait
for write-readiness instead of busy-looping on `want_write`
- Fixed Erlang distribution over sockets (`socket_dist_controller`) silently ignoring
`socket:send/2` errors on `tick` and outgoing distribution data; the connection is now
terminated instead of continuing to run against a broken socket

## [0.7.0-alpha.1] - 2026-04-06

Expand Down
2 changes: 0 additions & 2 deletions examples/erlang/rp2/picow_tcp_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,6 @@ echo(ConnectedSocket) ->
case socket:send(ConnectedSocket, Data) of
ok ->
io:format("All data was sent~n");
{ok, Rest} ->
io:format("Some data was sent. Remaining: ~p~n", [Rest]);
{error, Reason} ->
io:format("An error occurred sending data: ~p~n", [Reason])
end,
Expand Down
2 changes: 0 additions & 2 deletions examples/erlang/tcp_socket_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ echo(ConnectedSocket) ->
case socket:send(ConnectedSocket, Data) of
ok ->
io:format("All data was sent~n");
{ok, Rest} ->
io:format("Some data was sent. Remaining: ~p~n", [Rest]);
{error, Reason} ->
io:format("An error occurred sending data: ~p~n", [Reason])
end,
Expand Down
4 changes: 0 additions & 4 deletions examples/erlang/udp_socket_client.erl
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ loop(Socket) ->
io:format("Send packet ~p to ~p.~n", [
Data, Dest
]);
{ok, Rest} ->
io:format("Send packet ~p to ~p. Remaining: ~p~n", [
Data, Dest, Rest
]);
{error, Reason} ->
io:format("An error occurred sending a packet: ~p~n", [Reason])
end,
Expand Down
2 changes: 1 addition & 1 deletion libs/avm_network/src/epmd.erl
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ process_req(
) ->
case socket:recv(Socket, 1, nowait) of
{select, {select_info, recv, Ref}} ->
socket:send(Socket, <<?ALIVE2_X_RESP, 0, Creation:32>>),
ok = socket:send(Socket, <<?ALIVE2_X_RESP, 0, Creation:32>>),
State#state{
recv_selects = [{Ref, client} | RecvSelects],
clients = [
Expand Down
89 changes: 81 additions & 8 deletions libs/estdlib/src/socket.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
%% internal nifs
-export([
nif_select_read/2,
nif_select_write/2,
nif_accept/1,
nif_recv/2,
nif_recvfrom/2,
Expand Down Expand Up @@ -571,49 +572,58 @@ recvfrom0_nowait(Socket, Length, Ref) ->
%%-----------------------------------------------------------------------------
%% @param Socket the socket
%% @param Data the data to send
%% @returns `{ok, Rest}' if successful; `{error, Reason}', otherwise.
%% @returns `ok' if successful; `{error, Reason}', otherwise.
%% @doc Send data on the specified socket.
%%
%% Note that this function will block until data is sent
%% on the socket. The data may not have been received by the
%% intended recipient, and the data may not even have been sent
%% over the network.
%%
%% Partial sends and transient send backpressure (e.g. a full TCP
%% send buffer) are retried internally, waiting for the socket to
%% become writable again, until all data has been accepted by the
%% stack or an error occurs. Callers therefore do not need to
%% implement their own retry logic for `eagain'-like conditions.
%%
%% Example:
%%
%% `ok = socket:send(ConnectedSocket, Data)'
%% @end
%%-----------------------------------------------------------------------------
-spec send(Socket :: socket(), Data :: iodata()) ->
ok | {ok, Rest :: binary()} | {error, Reason :: term()}.
ok | {error, Reason :: term()}.
send(Socket, Data) when is_binary(Data) ->
?MODULE:nif_send(Socket, Data);
send_all(Socket, Data);
send(Socket, Data) ->
?MODULE:nif_send(Socket, erlang:iolist_to_binary(Data)).
send_all(Socket, erlang:iolist_to_binary(Data)).

%%-----------------------------------------------------------------------------
%% @param Socket the socket
%% @param Data the data to send
%% @param Dest the destination to which to send the data
%% @returns `{ok, Rest}' if successful; `{error, Reason}', otherwise.
%% @returns `ok' if successful; `{error, Reason}', otherwise.
%% @doc Send data on the specified socket to the specified destination.
%%
%% Note that this function will block until data is sent
%% on the socket. The data may not have been received by the
%% intended recipient, and the data may not even have been sent
%% over the network.
%%
%% Transient send backpressure is retried internally, waiting for
%% the socket to become writable again, as with `send/2'.
%%
%% Example:
%%
%% `ok = socket:sendto(ConnectedSocket, Data, Dest)'
%% @end
%%-----------------------------------------------------------------------------
-spec sendto(Socket :: socket(), Data :: iodata(), Dest :: sockaddr()) ->
ok | {ok, Rest :: binary()} | {error, Reason :: term()}.
ok | {error, Reason :: term()}.
sendto(Socket, Data, Dest) when is_binary(Data) ->
?MODULE:nif_sendto(Socket, Data, Dest);
sendto_all(Socket, Data, Dest);
sendto(Socket, Data, Dest) ->
?MODULE:nif_sendto(Socket, erlang:iolist_to_binary(Data), Dest).
sendto_all(Socket, erlang:iolist_to_binary(Data), Dest).

%%-----------------------------------------------------------------------------
%% @param Socket the socket
Expand Down Expand Up @@ -708,6 +718,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).
Expand All @@ -731,3 +745,62 @@ nif_send(_Socket, _Data) ->
%% @private
nif_sendto(_Socket, _Data, _Dest) ->
erlang:nif_error(undefined).

%% @private
%% Send all of Data on Socket, retrying partial sends and transient send
%% backpressure ({error, eagain}) by waiting for the socket to become
%% writable again, until all data has been accepted or a real error occurs.
send_all(_Socket, <<>>) ->
ok;
send_all(Socket, Data) ->
case ?MODULE:nif_send(Socket, Data) of
ok ->
ok;
{ok, Rest} ->
send_all(Socket, Rest);
{error, eagain} ->
case wait_write(Socket) of
ok ->
send_all(Socket, Data);
{error, _Reason} = Error ->
Error
end;
{error, _Reason} = Error ->
Error
end.

%% @private
sendto_all(_Socket, <<>>, _Dest) ->
ok;
sendto_all(Socket, Data, Dest) ->
case ?MODULE:nif_sendto(Socket, Data, Dest) of
ok ->
ok;
{ok, Rest} ->
sendto_all(Socket, Rest, Dest);
{error, eagain} ->
case wait_write(Socket) of
ok ->
sendto_all(Socket, Data, Dest);
{error, _Reason} = Error ->
Error
end;
{error, _Reason} = Error ->
Error
end.

%% @private
%% Block the calling process until Socket is writable (or closed).
wait_write(Socket) ->
Ref = erlang:make_ref(),
case ?MODULE:nif_select_write(Socket, Ref) of
ok ->
receive
{'$socket', Socket, select, Ref} ->
ok;
{'$socket', Socket, abort, {Ref, Reason}} ->
{error, Reason}
end;
{error, _Reason} = Error ->
Error
end.
24 changes: 17 additions & 7 deletions libs/estdlib/src/socket_dist_controller.erl
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,12 @@ handle_call(getstat, _From, #state{received = Received, sent = Sent} = State) ->
{reply, {ok, Received, Sent, 0}, State}.

handle_cast(tick, #state{socket = Socket, sent = Sent} = State) ->
socket:send(Socket, <<0:32>>),
{noreply, State#state{sent = Sent + 1}};
case socket:send(Socket, <<0:32>>) of
ok ->
{noreply, State#state{sent = Sent + 1}};
{error, Reason} ->
{stop, {send_error, Reason}, State}
end;
handle_cast({handshake_complete, DHandle}, State0) ->
ok = erlang:dist_ctrl_get_data_notification(DHandle),
% We now need to get messages when data is coming.
Expand All @@ -154,8 +158,10 @@ handle_cast({handshake_complete, DHandle}, State0) ->
end.

handle_info(dist_data, State0) ->
State1 = send_data_loop(State0),
{noreply, State1};
case send_data_loop(State0) of
{ok, State1} -> {noreply, State1};
{stop, Reason, State1} -> {stop, Reason, State1}
end;
handle_info(
{'$socket', Socket, select, SelectHandle},
#state{socket = Socket, select_handle = SelectHandle} = State0
Expand Down Expand Up @@ -209,10 +215,14 @@ send_data_loop(#state{dhandle = DHandle, socket = Socket, sent = Sent} = State)
case erlang:dist_ctrl_get_data(DHandle) of
none ->
ok = erlang:dist_ctrl_get_data_notification(DHandle),
State;
{ok, State};
Data ->
DataBin = ?PRE_PROCESS(Data),
DataSize = byte_size(DataBin),
socket:send(Socket, <<DataSize:32, DataBin/binary>>),
send_data_loop(State#state{sent = Sent + 1})
case socket:send(Socket, <<DataSize:32, DataBin/binary>>) of
ok ->
send_data_loop(State#state{sent = Sent + 1});
{error, Reason} ->
{stop, {send_error, Reason}, State}
end
end.
49 changes: 41 additions & 8 deletions libs/estdlib/src/ssl.erl
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,13 @@ handshake_loop(SSLContext, Socket) ->
Error
end;
want_write ->
% We're currrently missing non-blocking writes
handshake_loop(SSLContext, Socket);
case wait_write(Socket) of
ok ->
handshake_loop(SSLContext, Socket);
{error, _Reason} = Error ->
socket:close(Socket),
Error
end;
{error, _Reason} = Error ->
socket:close(Socket),
Error
Expand Down Expand Up @@ -253,8 +258,13 @@ close_notify_loop(SSLContext, Socket) ->
Error
end;
want_write ->
% We're currrently missing non-blocking writes
close_notify_loop(SSLContext, Socket);
case wait_write(Socket) of
ok ->
close_notify_loop(SSLContext, Socket);
{error, _Reason} = Error ->
socket:close(Socket),
Error
end;
{error, _Reason} = Error ->
socket:close(Socket),
Error
Expand Down Expand Up @@ -283,8 +293,12 @@ send({SSLContext, Socket} = SSLSocket, Binary) ->
Error
end;
want_write ->
% We're currrently missing non-blocking writes
send(SSLSocket, Binary);
case wait_write(Socket) of
ok ->
send(SSLSocket, Binary);
{error, _Reason} = Error ->
Error
end;
{error, _Reason} = Error ->
Error
end.
Expand Down Expand Up @@ -318,8 +332,27 @@ recv0({SSLContext, Socket} = SSLSocket, Length, Remaining, Acc) ->
Error
end;
want_write ->
% We're currrently missing non-blocking writes
recv0(SSLSocket, Length, Remaining, Acc);
case wait_write(Socket) of
ok ->
recv0(SSLSocket, Length, Remaining, Acc);
{error, _Reason} = Error ->
Error
end;
{error, _Reason} = Error ->
Error
end.

%% @private
wait_write(Socket) ->
Ref = erlang:make_ref(),
case socket:nif_select_write(Socket, Ref) of
ok ->
receive
{'$socket', Socket, select, Ref} ->
ok;
{'$socket', Socket, abort, {Ref, Reason}} ->
{error, Reason}
end;
{error, _Reason} = Error ->
Error
end.
Expand Down
Loading