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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`onRunTrackedJs`, `onGetTrackedObjects` and `onTrackedObjectDelete` hooks, which embedders may
override to customize what tracking means
- Added `string:to_integer/1`
- Added `gen:start/5,6`, used by Elixir's `GenServer`

### Changed
- `erlang:process_info/2` now accepts only pids of local processes, as Erlang/OTP does:
Expand Down Expand Up @@ -111,6 +112,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
builds, which made `erlang:crc32/2`, `erlang:crc32_combine/3` and `crypto:pbkdf2_hmac/5`
silently truncate huge integer arguments instead of raising `badarg`
- Fixed a bug where bigints were not normalized, yielding equality errors
- Fixed `gen_server` returning an error instead of `ignore` when `init/1` returns `ignore`
- 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

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

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

-export([
start/5,
start/6,
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) ->
case proc_lib:start_monitor(GenMod, init_it, InitArgs, Timeout, SpawnOpts) of
{{ok, Pid}, Mon} -> {ok, {Pid, Mon}};
{Error, _Mon} -> Error
end.

%%-----------------------------------------------------------------------------
%% @doc Perform a call on a gen server. This API not documented by OTP,
Expand Down
16 changes: 11 additions & 5 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,9 +139,11 @@
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.

Expand Down Expand Up @@ -169,6 +173,8 @@
}};
{stop, Reason} ->
{fail, {error, Reason}, {exit, Reason}};
ignore ->
{fail, ignore, {exit, normal}};
Reply ->
{fail, {error, {unexpected_reply_from_init, Reply}},
{exit, {bad_return_value, Reply}}}
Expand Down Expand Up @@ -603,7 +609,7 @@
) ->
case erlang:function_exported(Module, code_change, 3) of
true ->
case catch Module:code_change(OldVsn, ModState, Extra) of

Check warning on line 612 in libs/estdlib/src/gen_server.erl

View workflow job for this annotation

GitHub Actions / build-and-test (cc, 29.0, ubuntu-24.04, c++, strict, 1.19.5, 3.27.0, -DAVM_DISABLE_JIT=OFF, x86_64)

'catch ...' is deprecated; please use 'try ... catch ... end' instead.
{ok, NewModState} ->
{ok, State#state{mod_state = NewModState}};
Other ->
Expand Down
48 changes: 18 additions & 30 deletions libs/estdlib/src/proc_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ start(Module, Function, Args, Timeout) ->
%%-----------------------------------------------------------------------------
-spec start(module(), atom(), [any()], timeout(), [start_spawn_option()]) -> any().
start(Module, Function, Args, Timeout, SpawnOpts) ->
start0(Module, Function, Args, Timeout, SpawnOpts, false, false).
start0(Module, Function, Args, Timeout, SpawnOpts, false).

%% @equiv start_link(Module, Function, Args, infinity)
-spec start_link(module(), atom(), [any()]) -> any().
Expand All @@ -164,7 +164,7 @@ start_link(Module, Function, Args, Timeout) ->
%%-----------------------------------------------------------------------------
-spec start_link(module(), atom(), [any()], timeout(), [start_spawn_option()]) -> any().
start_link(Module, Function, Args, Timeout, SpawnOpts) ->
start0(Module, Function, Args, Timeout, [link | SpawnOpts], true, false).
start0(Module, Function, Args, Timeout, [link | SpawnOpts], false).

%% @equiv start_monitor(Module, Function, Args, infinity)
-spec start_monitor(module(), atom(), [any()]) -> any().
Expand All @@ -188,11 +188,11 @@ start_monitor(Module, Function, Args, Timeout) ->
%%-----------------------------------------------------------------------------
-spec start_monitor(module(), atom(), [any()], timeout(), [start_spawn_option()]) -> any().
start_monitor(Module, Function, Args, Timeout, SpawnOpts) ->
start0(Module, Function, Args, Timeout, SpawnOpts, true, true).
start0(Module, Function, Args, Timeout, SpawnOpts, true).

%% @private
start0(Module, Function, Args, Timeout, SpawnOpts, Link, Monitor) ->
case lists:member(monitor, SpawnOpts) of
start0(Module, Function, Args, Timeout, SpawnOpts, Monitor) ->
case lists:member(monitor, SpawnOpts) orelse lists:keymember(monitor, 1, SpawnOpts) of
true -> error(badarg);
false -> ok
end,
Expand All @@ -211,42 +211,27 @@ start0(Module, Function, Args, Timeout, SpawnOpts, Link, Monitor) ->
%% so its registered name is freed and linked children have got their
%% EXIT signal.
{nack, Pid, Result} when Monitor ->
flush_exit(Pid),
receive
{'DOWN', MonitorRef, process, Pid, _} -> ok
end,
flush_exit(Pid, Link),
{Result, MonitorRef};
{nack, Pid, Result} ->
flush_exit(Pid),
receive
{'DOWN', MonitorRef, process, Pid, _} -> ok
end,
flush_exit(Pid, Link),
Result;
{'DOWN', MonitorRef, process, Pid, Reason} when Link ->
receive
{'EXIT', Pid, _} -> ok
after 0 -> ok
end,
receive
{'DOWN', MonitorRef, process, Pid, _} -> ok
end,
{error, Reason};
{'DOWN', MonitorRef, process, Pid, Reason} when Monitor ->
flush_exit(Pid),
{{error, Reason}, MonitorRef};
{'DOWN', MonitorRef, process, Pid, Reason} ->
flush_exit(Pid),
{error, Reason}
after Timeout ->
if
Link ->
unlink(Pid),
exit(Pid, kill),
receive
{'EXIT', Pid, _} -> ok
after 0 -> ok
end;
true ->
exit(Pid, kill)
end,
unlink(Pid),
exit(Pid, kill),
flush_exit_message(Pid),
receive
{'DOWN', MonitorRef, process, Pid, _} -> ok
end,
Expand All @@ -259,9 +244,12 @@ start0(Module, Function, Args, Timeout, SpawnOpts, Link, Monitor) ->
end.

%% @private
flush_exit(_Pid, false) ->
ok;
flush_exit(Pid, true) ->
%% unlink as spawn_opt may have linked.
flush_exit(Pid) ->
unlink(Pid),
flush_exit_message(Pid).

flush_exit_message(Pid) ->
receive
{'EXIT', Pid, _} -> ok
after 0 -> ok
Expand Down
113 changes: 113 additions & 0 deletions tests/libs/estdlib/test_gen_server.erl
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ test() ->
ok = test_start_link(),
ok = test_start_monitor(),
ok = test_start_name(),
ok = test_gen_start(),
ok = test_gen_start_named_occupied(),
ok = test_gen_start_ignore(),
ok = test_gen_start_timeout(),
ok = test_continue(),
ok = test_init_exception(),
ok = test_late_reply(),
Expand Down Expand Up @@ -133,6 +137,108 @@ test_start_name() ->
undefined = whereis(?MODULE),
ok.

%% gen:start/5,6 is the OTP-private entry point Elixir's GenServer.start*
%% functions call.
test_gen_start() ->
{ok, Pid1} = gen:start(gen_server, nolink, ?MODULE, [], []),
pong = gen_server:call(Pid1, ping),
ok = gen_server:stop(Pid1),

PreviousTrapExit = erlang:process_flag(trap_exit, true),
{ok, Pid2} = gen:start(gen_server, link, ?MODULE, [], []),
pong = gen_server:call(Pid2, ping),
ok = gen_server:stop(Pid2),
normal =
receive
{'EXIT', Pid2, Reason} -> Reason
after 5000 -> timeout
end,
true = erlang:process_flag(trap_exit, PreviousTrapExit),

{ok, {Pid3, Ref3}} = gen:start(gen_server, monitor, ?MODULE, [], []),
true = is_pid(Pid3),
true = is_reference(Ref3),
pong = gen_server:call(Pid3, ping),
ok = gen_server:cast(Pid3, crash),
ok =
receive
{'DOWN', Ref3, process, Pid3, _Reason} -> ok
after 30000 -> timeout
end,

undefined = whereis(?MODULE),
{ok, Pid4} = gen:start(gen_server, nolink, {local, ?MODULE}, ?MODULE, [], []),
Pid4 = whereis(?MODULE),
{error, {already_started, Pid4}} = gen:start(
gen_server, nolink, {local, ?MODULE}, ?MODULE, [], []
),
ok = gen_server:stop(Pid4),
undefined = whereis(?MODULE),
ok.

%% The caller's mailbox must be left empty: a failed start leaks no EXIT or DOWN.
test_gen_start_named_occupied() ->
{ok, Pid} = gen:start(gen_server, nolink, {local, ?MODULE}, ?MODULE, [], []),
PreviousTrapExit = erlang:process_flag(trap_exit, true),
{error, {already_started, Pid}} = gen:start(
gen_server, link, {local, ?MODULE}, ?MODULE, [], []
),
[] = drain_mailbox(),
{error, {already_started, Pid}} = gen:start(
gen_server, monitor, {local, ?MODULE}, ?MODULE, [], []
),
[] = drain_mailbox(),
ok = gen_server:stop(Pid),
_ = drain_mailbox(),
true = erlang:process_flag(trap_exit, PreviousTrapExit),
undefined = whereis(?MODULE),
ok.

test_gen_start_ignore() ->
ignore = gen:start(gen_server, nolink, ?MODULE, ignore_me, []),
[] = drain_mailbox(),
ignore = gen:start(gen_server, nolink, {local, ?MODULE}, ?MODULE, ignore_me, []),
undefined = whereis(?MODULE),
[] = drain_mailbox(),
ignore = gen:start(gen_server, monitor, ?MODULE, ignore_me, []),
[] = drain_mailbox(),
PreviousTrapExit = erlang:process_flag(trap_exit, true),
ignore = gen:start(gen_server, link, ?MODULE, ignore_me, []),
[] = drain_mailbox(),
true = erlang:process_flag(trap_exit, PreviousTrapExit),
ok.

%% Start isolated and non-trapping so a stray exit signal shows up as a kill.
test_gen_start_timeout() ->
{survived, {error, timeout}} = isolated_start(nolink, [{timeout, 200}]),
{survived, {error, timeout}} = isolated_start(link, [{timeout, 200}]),
{survived, {error, timeout}} = isolated_start(monitor, [{timeout, 200}]),
{survived, {error, timeout}} = isolated_start(nolink, [{timeout, 200}, {spawn_opt, [link]}]),
ok.

isolated_start(LinkP, Options) ->
Self = self(),
Pid = spawn(fun() ->
Result = gen:start(gen_server, LinkP, ?MODULE, hang, Options),
Self ! {done, self(), Result}
end),
Ref = monitor(process, Pid),
receive
{done, Pid, Result} ->
demonitor(Ref, [flush]),
{survived, Result};
{'DOWN', Ref, process, Pid, Reason} ->
{killed, Reason}
after 8000 ->
{no_reply, Pid}
end.

drain_mailbox() ->
receive
M -> [M | drain_mailbox()]
after 100 -> []
end.

test_continue() ->
{ok, Pid} = gen_server:start_link(?MODULE, {continue, self()}, []),
[{Pid, continue}, {Pid, after_continue}] = read_replies(Pid),
Expand Down Expand Up @@ -585,6 +691,13 @@ get_otp_version() ->
%% callbacks
%%

init(ignore_me) ->
ignore;
init(hang) ->
receive
after 5000 -> ok
end,
{ok, #state{}};
init(throwme) ->
throw(throwme);
init({continue, Pid}) ->
Expand Down
9 changes: 9 additions & 0 deletions tests/libs/estdlib/test_proc_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ test_start_monitor_badarg() ->
error:badarg ->
ok
end,
%% OTP rejects the tuple form too
ok =
try
proc_lib:start(?MODULE, init_ok, [Parent], infinity, [{monitor, []}]),
unexpected
catch
error:badarg ->
ok
end,
ok.

test_start_link_sync() ->
Expand Down
Loading