Skip to content
Merged
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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.7.0-beta.1] - Unreleased

### Added
- Added `gen:start/5,6`, used by Elixir's `GenServer`

### Fixed
- Fixed maps decoded by `binary_to_term/1,2` and received over distribution keeping the
encoder's key order, which broke `==` and made `=>` updates on an existing key produce
Expand All @@ -23,6 +26,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed the ESP32 UART driver blocking the scheduler, until the task watchdog reset the chip,
when a read had already taken the bytes of a pending data event, or when `uart:read/2` counted
bytes that the other scheduler then took
- Fixed `gen_server` returning an error instead of `ignore` when `init/1` returns `ignore`
- Fixed `gen_server` start when `init/1` stops, crashes or returns a bad value: like OTP, the
server now exits with that reason, which a linked caller receives, and the start returns
`{error, Reason}` with the exit reason instead of `{unexpected_reply_from_init, _}` or
`{bad_return_value, _}`
- Fixed a failed named `gen_server` start leaving a stray `EXIT` or `DOWN` in the caller's mailbox
- Fixed `proc_lib:start/5` killing a linked caller when a start passing `{spawn_opt, [link]}`
timed out, and `proc_lib:start_link/5,start_monitor/5` hanging when the child died before
acknowledging
- Fixed `proc_lib:start*/5` accepting the `{monitor, _}` spawn option, which is not allowed
- Fixed a failed `proc_lib:start_monitor/3,4,5` consuming the `DOWN` message of the monitor it
returns, which callers waiting on that monitor never received

## [0.7.0-beta.0] - 2026-09-28

Expand Down
69 changes: 69 additions & 0 deletions libs/estdlib/src/gen.erl
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,82 @@
-moduledoc false.

-export([
start/5,
start/6,
monitor_return/1,
call/4,
cast/2,
reply/2
]).

-type server_ref() :: atom() | pid().
-type from() :: {pid(), reference()}.
-type linkage() :: link | nolink | monitor.
-type emgr_name() :: {local, atom()}.

%%-----------------------------------------------------------------------------
%% @doc Start a `gen_*' process without a registered name. This is the
%% OTP-private entry point Elixir's `GenServer.start_link/start' calls
%% (`:gen.start(:gen_server, Link, Module, Args, Options)'). It bridges to the
%% AtomVM `gen_*' module's `init_it' protocol via {@link proc_lib}.
%% @end
%%-----------------------------------------------------------------------------
-spec start(
GenMod :: module(),
LinkP :: linkage(),
Module :: module(),
Args :: term(),
Options :: list()
) -> {ok, pid()} | {ok, {pid(), reference()}} | ignore | {error, term()}.
start(GenMod, LinkP, Module, Args, Options) ->
do_start(GenMod, LinkP, [self(), Module, Args, Options], Options).

%%-----------------------------------------------------------------------------
%% @doc Start a `gen_*' process with a registered name, as called by Elixir
%% (`:gen.start(:gen_server, Link, {local, Name}, Module, Args, Options)').
%%
%% Only `{local, Name}' is supported.
%% @end
%%-----------------------------------------------------------------------------
-spec start(
GenMod :: module(),
LinkP :: linkage(),
Name :: emgr_name(),
Module :: module(),
Args :: term(),
Options :: list()
) -> {ok, pid()} | {ok, {pid(), reference()}} | ignore | {error, term()}.
start(GenMod, LinkP, {local, Name}, Module, Args, Options) when is_atom(Name) ->
case whereis(Name) of
undefined ->
do_start(
GenMod, LinkP, [self(), Name, Module, Args, [{name, Name} | Options]], Options
);
Pid ->
{error, {already_started, Pid}}
end.

%% @private
do_start(GenMod, LinkP, InitArgs, Options) ->
Timeout = proplists:get_value(timeout, Options, infinity),
SpawnOpts = proplists:get_value(spawn_opt, Options, []),
do_start(GenMod, LinkP, InitArgs, Timeout, SpawnOpts).

do_start(GenMod, link, InitArgs, Timeout, SpawnOpts) ->
proc_lib:start_link(GenMod, init_it, InitArgs, Timeout, SpawnOpts);
do_start(GenMod, nolink, InitArgs, Timeout, SpawnOpts) ->
proc_lib:start(GenMod, init_it, InitArgs, Timeout, SpawnOpts);
do_start(GenMod, monitor, InitArgs, Timeout, SpawnOpts) ->
monitor_return(proc_lib:start_monitor(GenMod, init_it, InitArgs, Timeout, SpawnOpts)).

%% @private
%% On failure, the monitor is not returned, so consume its DOWN.
monitor_return({{ok, Pid}, Mon}) ->
{ok, {Pid, Mon}};
monitor_return({Error, Mon}) ->
receive
{'DOWN', Mon, process, _Pid, _Reason} -> Error
end.

%%-----------------------------------------------------------------------------
%% @doc Perform a call on a gen server. This API not documented by OTP,
Expand Down
119 changes: 52 additions & 67 deletions libs/estdlib/src/gen_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,17 @@
}).

-type options() :: list({atom(), term()}).
-type start_ret() :: {ok, pid()} | {error, Reason :: term()}.
-type start_mon_ret() :: {ok, {Pid :: pid(), MonRef :: reference()}} | {error, Reason :: term()}.
-type start_ret() :: {ok, pid()} | ignore | {error, Reason :: term()}.
-type start_mon_ret() ::
{ok, {Pid :: pid(), MonRef :: reference()}} | ignore | {error, Reason :: term()}.
-type server_ref() :: atom() | pid().
-type from() :: {pid(), reference()}.

-type init_result(StateType) ::
{ok, State :: StateType}
| {ok, State :: StateType, timeout() | {timeout, timeout(), Msg :: any()} | {continue, term()}}
| {stop, Reason :: any()}.
| {stop, Reason :: any()}
| ignore.

-type handle_continue_result(StateType) ::
{noreply, NewState :: StateType}
Expand Down Expand Up @@ -137,61 +139,54 @@ init_it(Starter, Name, Module, Args, Options) ->
badarg,
S
),
proc_lib:init_ack(Starter, {error, badarg});
proc_lib:init_fail(Starter, {error, badarg}, {exit, normal});
Pid when is_pid(Pid) ->
proc_lib:init_ack(Starter, {error, {already_started, Pid}})
proc_lib:init_fail(
Starter, {error, {already_started, Pid}}, {exit, normal}
)
end
end.

%% Like OTP, only ignore is reported with init_fail. Any other failure makes the
%% server exit before acknowledging, so the starter gets {error, Reason} from
%% the DOWN message and a linked starter gets the exit signal.
init_it(Starter, Module, Args, Options) ->
Outcome =
try Module:init(Args) of
{ok, ModState} ->
{ok, #state{
name = proplists:get_value(name, Options),
mod = Module,
mod_state = ModState,
timeout = infinity
}};
{ok, ModState, {continue, NewContinue}} ->
{ok, #state{
name = proplists:get_value(name, Options),
mod = Module,
mod_state = ModState,
timeout = {continue, NewContinue}
}};
{ok, ModState, InitTimeout} ->
{ok, #state{
name = proplists:get_value(name, Options),
mod = Module,
mod_state = ModState,
timeout = InitTimeout
}};
{stop, Reason} ->
{fail, {error, Reason}, {exit, Reason}};
Reply ->
{fail, {error, {unexpected_reply_from_init, Reply}},
{exit, {bad_return_value, Reply}}}
catch
Class:E:S ->
crash_report(
io_lib:format(
"gen_server:init_it/4: Error initializing module ~p", [Module]
),
Starter,
E,
S
),
{fail, {error, {bad_return_value, E}}, {Class, E, S}}
end,
case Outcome of
{ok, State} ->
proc_lib:init_ack(Starter, {ok, self()}),
system_continue(Starter, [], State);
{fail, Return, Exception} ->
proc_lib:init_fail(Starter, Return, Exception)
case init_it(Module, Args) of
{ok, {ok, ModState}} ->
ack_and_loop(Starter, Module, ModState, infinity, Options);
{ok, {ok, ModState, {continue, NewContinue}}} ->
ack_and_loop(Starter, Module, ModState, {continue, NewContinue}, Options);
{ok, {ok, ModState, InitTimeout}} ->
ack_and_loop(Starter, Module, ModState, InitTimeout, Options);
{ok, {stop, Reason}} ->
exit(Reason);
{ok, ignore} ->
proc_lib:init_fail(Starter, ignore, {exit, normal});
{ok, Reply} ->
exit({bad_return_value, Reply});
{'EXIT', Class, Reason, Stacktrace} ->
erlang:raise(Class, Reason, Stacktrace)
end.

%% A value thrown by init/1 is its return value, as in OTP.
init_it(Module, Args) ->
try
{ok, Module:init(Args)}
catch
throw:Reply -> {ok, Reply};
Class:Reason:Stacktrace -> {'EXIT', Class, Reason, Stacktrace}
end.

ack_and_loop(Starter, Module, ModState, Timeout, Options) ->
State = #state{
name = proplists:get_value(name, Options),
mod = Module,
mod_state = ModState,
timeout = Timeout
},
proc_lib:init_ack(Starter, {ok, self()}),
system_continue(Starter, [], State).

crash_report(ErrStr, Parent, E, S) ->
io:format("=============~n"),
io:format("CRASH REPORT~n"),
Expand Down Expand Up @@ -312,13 +307,7 @@ start_link(Module, Args, Options) ->
-spec start_monitor(Module :: module(), Args :: term(), Options :: options()) ->
start_mon_ret().
start_monitor(Module, Args, Options) ->
{Result, Monitor} = proc_lib:start_monitor(?MODULE, init_it, [self(), Module, Args, Options]),
case Result of
{ok, Pid} ->
{ok, {Pid, Monitor}};
_ ->
Result
end.
gen:monitor_return(proc_lib:start_monitor(?MODULE, init_it, [self(), Module, Args, Options])).

%%-----------------------------------------------------------------------------
%% @param ServerName the name with which to register the gen_server
Expand All @@ -345,15 +334,11 @@ start_monitor(Module, Args, Options) ->
start_monitor({local, Name}, Module, Args, Options) when is_atom(Name) ->
case erlang:whereis(Name) of
undefined ->
{Result, Monitor} = proc_lib:start_monitor(?MODULE, init_it, [
self(), Name, Module, Args, [{name, Name} | Options]
]),
case Result of
{ok, Pid} ->
{ok, {Pid, Monitor}};
_ ->
Result
end;
gen:monitor_return(
proc_lib:start_monitor(?MODULE, init_it, [
self(), Name, Module, Args, [{name, Name} | Options]
])
);
Pid ->
{error, {already_started, Pid}}
end.
Expand Down
Loading
Loading