Skip to content
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,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, instead of one direction silently overwriting the other's state

### Changed
- Updated network type db() to dbm() to reflect the actual representation of the type
Expand Down Expand Up @@ -66,6 +70,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