From 90c7e98ead07271874e78b6075975e540cf4bc64 Mon Sep 17 00:00:00 2001 From: Mateusz Front Date: Tue, 2 Jun 2026 11:58:32 +0000 Subject: [PATCH 1/6] Add support for process aliases Implement Erlang process aliases together with the reference-representation rework they require, rebased onto release-0.7 and squashed into one commit. Public API (libs/estdlib/src/erlang.erl): - alias/0, unalias/1 - monitor/3 with {alias, explicit_unalias | demonitor | reply_demonitor} - spawn_opt {monitor, [monitor_option()]} Runtime: - New process-reference term carrying ref_ticks + owner process id (RefData), alongside short/local references; BEAM-compliant reference ordering and binary_to_term/term_to_binary round-tripping (term.{c,h}, external_term.c). - Monitors store RefData; alias monitors (CONTEXT_MONITOR_ALIAS) with the three alias modes and their lifecycle (context.{c,h}). - send/2, the send opcode and the JIT send path resolve an alias to its target process and drop reply_demonitor aliases after first use (nifs.c, opcodesswitch.h, jit.c). - Reference size macros: REF_SIZE -> TERM_BOXED_REFERENCE_SHORT_SIZE, plus TERM_BOXED_REFERENCE_PROCESS_SIZE and TERM_BOXED_REFERENCE_RESOURCE_SIZE. Tests: alias coverage added to test_monitor, test_refs_ordering and test_binary_to_term. Signed-off-by: Davide Bettio --- libs/estdlib/src/erlang.erl | 54 ++++- src/libAtomVM/context.c | 104 +++++++-- src/libAtomVM/context.h | 45 +++- src/libAtomVM/defaultatoms.def | 6 + src/libAtomVM/ets.c | 2 +- src/libAtomVM/external_term.c | 19 +- src/libAtomVM/globalcontext.h | 2 - src/libAtomVM/jit.c | 36 +++- src/libAtomVM/nifs.c | 199 +++++++++++++++--- src/libAtomVM/nifs.gperf | 3 + src/libAtomVM/opcodesswitch.h | 24 ++- src/libAtomVM/otp_socket.c | 14 +- src/libAtomVM/resources.h | 2 +- src/libAtomVM/term.c | 19 ++ src/libAtomVM/term.h | 126 ++++++++++- .../emscripten/src/lib/websocket_nifs.c | 2 +- .../components/avm_builtins/adc_driver.c | 4 +- .../components/avm_builtins/dac_driver.c | 2 +- .../components/avm_builtins/i2c_resource.c | 2 +- .../components/avm_builtins/network_driver.c | 8 +- .../components/avm_builtins/socket_driver.c | 2 +- .../components/avm_builtins/uart_driver.c | 4 +- src/platforms/rp2/src/lib/networkdriver.c | 6 +- tests/erlang_tests/test_binary_to_term.erl | 17 ++ tests/erlang_tests/test_monitor.erl | 151 +++++++++++++ tests/erlang_tests/test_refs_ordering.erl | 35 ++- 26 files changed, 781 insertions(+), 107 deletions(-) diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 760063cec6..4fdb14225f 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -104,6 +104,7 @@ make_ref/0, send/2, monitor/2, + monitor/3, demonitor/1, demonitor/2, exit/1, @@ -174,7 +175,9 @@ tl/1, trunc/1, tuple_size/1, - tuple_to_list/1 + tuple_to_list/1, + alias/0, + unalias/1 ]). -export_type([ @@ -212,7 +215,8 @@ | {max_heap_size, pos_integer()} | {atomvm_heap_growth, atomvm_heap_growth_strategy()} | link - | monitor. + | monitor + | {monitor, [monitor_option()]}. -type send_destination() :: pid() @@ -238,6 +242,8 @@ -type raise_stacktrace() :: [{module(), atom(), arity() | [term()]} | {function(), arity() | [term()]}] | stacktrace(). +-type monitor_option() :: {alias, explicit_unalias | demonitor | reply_demonitor}. + %%----------------------------------------------------------------------------- %% @param Time time in milliseconds after which to send the timeout message. %% @param Dest Pid or server name to which to send the timeout message. @@ -1306,6 +1312,28 @@ send(_Target, _Message) -> monitor(_Type, _PidOrPort) -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Type type of monitor to create +%% @param PidOrPort pid or port of the object to monitor +%% @param Options monitor options +%% @returns a monitor reference +%% @doc Creates a monitor and allows passing additional options. +%% Currently, only the `{alias, AliasMode}' option is supported. Passing it +%% makes the monitor also an alias on the calling process (see `alias/0'). +%% `AliasMode' defines the behaviour of the alias: +%% - explicit_unalias - the alias can be only removed with `unalias/1', +%% - demonitor - the alias is also removed when `demonitor/1' is called +%% on the monitor, +%% - reply_demonitor - the alias is also removed after a first message +%% is sent via it. +%% @end +%%----------------------------------------------------------------------------- +-spec monitor + (Type :: process, Pid :: pid() | atom(), [monitor_option()]) -> reference(); + (Type :: port, Port :: port() | atom(), [monitor_option()]) -> reference(). +monitor(_Type, _PidOrPort, _Options) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Monitor reference of monitor to remove %% @returns `true' @@ -2147,3 +2175,25 @@ tuple_size(_Tuple) -> -spec tuple_to_list(Tuple :: tuple()) -> [term()]. tuple_to_list(_Tuple) -> erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @returns A reference aliasing the calling process. +%% @doc Creates an alias for the calling process. The alias can be used +%% to send messages to the process like the PID. The alias can also be +%% created along with a monitor - see `monitor/3'. The alias can be +%% removed by calling `unalias/1'. +%% @end +%%----------------------------------------------------------------------------- +-spec alias() -> Alias when Alias :: reference(). +alias() -> + erlang:nif_error(undefined). + +%%----------------------------------------------------------------------------- +%% @param Alias the alias to be removed. +%% @returns `true' if alias was removed, `false' if it was not found +%% @doc Removes process alias. See `alias/0' for more information. +%% @end +%%----------------------------------------------------------------------------- +-spec unalias(Alias) -> boolean() when Alias :: reference(). +unalias(_Alias) -> + erlang:nif_error(undefined). diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index b8b29273d5..14da1c327b 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -267,6 +267,7 @@ void context_destroy(Context *ctx) case CONTEXT_MONITOR_MONITORED_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: + case CONTEXT_MONITOR_ALIAS: UNREACHABLE(); } } @@ -430,7 +431,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL) { struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); @@ -441,7 +442,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal } else if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME) { int32_t monitor_process_id = term_to_local_process_id(monitor_obj); struct MonitorLocalRegisteredNameMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); @@ -726,7 +727,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); } free(monitoring_monitor); break; @@ -738,7 +739,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); } free(monitoring_monitor); break; @@ -786,7 +787,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - int required_terms = REF_SIZE + TUPLE_SIZE(5); + int required_terms = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5); if (UNLIKELY(memory_ensure_free(ctx, required_terms) != MEMORY_GC_OK)) { // TODO: handle out of memory here fprintf(stderr, "Cannot handle out of memory.\n"); @@ -794,7 +795,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) AVM_ABORT(); } // Prepare the message on ctx's heap which will be freed afterwards. - term ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap); + term ref = term_from_ref_data(&monitored_monitor->ref_data, &ctx->heap); term port_or_process = term_pid_or_port_from_context(ctx); term port_or_process_atom @@ -811,6 +812,11 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) free(monitored_monitor); break; } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + free(alias); + break; + } } } return result; @@ -850,7 +856,7 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring) { struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { @@ -862,12 +868,12 @@ struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monito monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORED_LOCAL; } monitor->monitor_obj = monitor_pid; - monitor->ref_ticks = ref_ticks; + monitor->ref_data = *ref_data; return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -876,7 +882,20 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME; monitor->monitor_process_id = monitor_process_id; monitor->monitor_name = monitor_name; - monitor->ref_ticks = ref_ticks; + monitor->ref_data = *ref_data; + + return &monitor->monitor; +} + +struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type) +{ + struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); + if (IS_NULL_PTR(monitor)) { + return NULL; + } + monitor->monitor.monitor_type = CONTEXT_MONITOR_ALIAS; + monitor->ref_data = *ref_data; + monitor->alias_type = alias_type; return &monitor->monitor; } @@ -915,7 +934,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *new_local_monitor = CONTAINER_OF(new_monitor, struct MonitorLocalMonitor, monitor); struct MonitorLocalMonitor *existing_local_monitor = CONTAINER_OF(existing, struct MonitorLocalMonitor, monitor); - if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_ticks == new_local_monitor->ref_ticks)) { + if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_data.ref_ticks == new_local_monitor->ref_data.ref_ticks)) { free(new_local_monitor); return false; } @@ -926,12 +945,22 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorLocalRegisteredNameMonitor *existing_local_registeredname_monitor = CONTAINER_OF(existing, struct MonitorLocalRegisteredNameMonitor, monitor); if (UNLIKELY(existing_local_registeredname_monitor->monitor_process_id == new_local_registeredname_monitor->monitor_process_id && existing_local_registeredname_monitor->monitor_name == new_local_registeredname_monitor->monitor_name - && existing_local_registeredname_monitor->ref_ticks == new_local_registeredname_monitor->ref_ticks)) { + && existing_local_registeredname_monitor->ref_data.ref_ticks == new_local_registeredname_monitor->ref_data.ref_ticks)) { free(new_local_registeredname_monitor); return false; } break; } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *new_alias_monitor = CONTAINER_OF(new_monitor, struct MonitorAlias, monitor); + struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); + + if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { + free(new_monitor); + return false; + } + break; + } case CONTEXT_MONITOR_RESOURCE: { struct ResourceContextMonitor *new_resource_monitor = CONTAINER_OF(new_monitor, struct ResourceContextMonitor, monitor); struct ResourceContextMonitor *existing_resource_monitor = CONTAINER_OF(existing, struct ResourceContextMonitor, monitor); @@ -1065,6 +1094,11 @@ void context_unlink_ack(Context *ctx, term link_pid, uint64_t unlink_id) void context_demonitor(Context *ctx, uint64_t ref_ticks) { + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(alias); + } + struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); @@ -1072,7 +1106,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_ticks == ref_ticks) { + if (local_monitor->ref_data.ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_monitor); return; @@ -1081,7 +1115,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_registeredname_monitor); return; @@ -1098,11 +1132,36 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: + case CONTEXT_MONITOR_ALIAS: break; } } } +struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) +{ + struct ListHead *item; + LIST_FOR_EACH (item, &ctx->monitors_head) { + struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); + if (monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { + struct MonitorAlias *alias_monitor = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + if (alias_monitor->ref_data.ref_ticks == ref_ticks) { + return alias_monitor; + } + } + } + + return NULL; +} + +void context_unalias(struct MonitorAlias *alias) +{ + TERM_DEBUG_ASSERT(alias != NULL); + struct Monitor *monitor = &alias->monitor; + list_remove(&monitor->monitor_list_head); + free(alias); +} + term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitoring) { struct ListHead *item; @@ -1112,7 +1171,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_ticks == ref_ticks) { + if (local_monitor->ref_data.ref_ticks == ref_ticks) { *is_monitoring = monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL; return local_monitor->monitor_obj; } @@ -1120,7 +1179,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { *is_monitoring = true; return term_from_local_process_id(local_registeredname_monitor->monitor_process_id); } @@ -1129,6 +1188,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: case CONTEXT_MONITOR_RESOURCE: + case CONTEXT_MONITOR_ALIAS: break; } } @@ -1255,7 +1315,13 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitor to "); term_display(stderr, monitoring_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_data.ref_ticks); + fprintf(stderr, "\n"); + break; + } + case CONTEXT_MONITOR_ALIAS: { + struct MonitorAlias *monitor_alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } @@ -1263,7 +1329,7 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitored by "); term_display(stderr, monitored_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } @@ -1273,7 +1339,7 @@ COLD_FUNC void context_dump(Context *ctx) term_display(stderr, local_registeredname_monitor->monitor_name, ctx); fprintf(stderr, " ("); term_display(stderr, term_from_local_process_id(local_registeredname_monitor->monitor_process_id), ctx); - fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_ticks); + fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_data.ref_ticks); fprintf(stderr, "\n"); break; } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index e43f360620..cb9f7e0b1a 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -177,6 +177,14 @@ enum ContextMonitorType CONTEXT_MONITOR_RESOURCE, CONTEXT_MONITOR_LINK_REMOTE, CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME, + CONTEXT_MONITOR_ALIAS, +}; + +enum ContextMonitorAliasType +{ + ContextMonitorAliasExplicitUnalias, + ContextMonitorAliasDemonitor, + ContextMonitorAliasReplyDemonitor, }; #define UNLINK_ID_LINK_ACTIVE 0x0 @@ -200,18 +208,25 @@ struct LinkLocalMonitor struct MonitorLocalMonitor { struct Monitor monitor; - uint64_t ref_ticks; + RefData ref_data; term monitor_obj; }; struct MonitorLocalRegisteredNameMonitor { struct Monitor monitor; - uint64_t ref_ticks; + RefData ref_data; int32_t monitor_process_id; term monitor_name; }; +struct MonitorAlias +{ + struct Monitor monitor; + RefData ref_data; + enum ContextMonitorAliasType alias_type; +}; + // The other half is called ResourceMonitor and is a linked list of resources struct ResourceContextMonitor { @@ -511,21 +526,23 @@ struct Monitor *monitor_link_new(term link_pid); * @brief Create a monitor on a process. * * @param monitor_pid monitored process - * @param ref_ticks reference of the monitor + * @param ref_data reference of the monitor * @param is_monitoring if ctx is the monitoring process * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring); + +struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type); /** * @brief Create a monitor on a process by registered name. * * @param monitor_process_id monitored process id * @param monitor_name name of the monitor (atom) - * @param ref_ticks reference of the monitor + * @param ref_data reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data); /** * @brief Create a resource monitor. @@ -582,6 +599,22 @@ void context_unlink_ack(Context *ctx, term link_pid, uint64_t unlink_id); */ void context_demonitor(Context *ctx, uint64_t ref_ticks); +/** + * @brief Find a process alias + * + * @param ctx the context being executed + * @param ref_ticks reference of the alias to find + * @return found alias or NULL + */ +struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks); + +/** + * @brief Remove an alias of a process + * + * @param alias The alias to remove, can be obtained using context_find_alias + */ +void context_unalias(struct MonitorAlias *alias); + /** * @brief Get target of a monitor. * diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 6dc95175d9..229271306f 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -222,3 +222,9 @@ X(JIT_RISCV32_ATOM, "\xB", "jit_riscv32") X(JIT_RISCV64_ATOM, "\xB", "jit_riscv64") X(JIT_WASM32_ATOM, "\xA", "jit_wasm32") X(JIT_XTENSA_ATOM, "\xA", "jit_xtensa") + +X(ALIAS_ATOM, "\x5", "alias") +X(DEMONITOR_ATOM, "\x9", "demonitor") +X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias") +X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor") +X(TAG_ATOM, "\x3", "tag") diff --git a/src/libAtomVM/ets.c b/src/libAtomVM/ets.c index b33715b686..f02576a6a1 100644 --- a/src/libAtomVM/ets.c +++ b/src/libAtomVM/ets.c @@ -180,7 +180,7 @@ ets_result_t ets_create_table_maybe_gc( if (named) { *ret = name; } else { - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { ets_multimap_delete(multimap, ctx->global); #ifndef AVM_NO_SMP smp_rwlock_destroy(table->lock); diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index e0da90a042..448a60d06b 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -521,6 +521,8 @@ static int serialize_term(uint8_t *buf, term t, GlobalContext *glb) uint32_t len; if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } @@ -542,6 +544,15 @@ static int serialize_term(uint8_t *buf, term t, GlobalContext *glb) WRITE_64_UNALIGNED(buf + k + 12, ((uintptr_t) serialize_ref)); } return k + 20; + } else if (term_is_process_reference(t)) { + if (!IS_NULL_PTR(buf)) { + uint64_t ticks = term_to_ref_ticks(t); + uint32_t process_id = term_process_ref_to_process_id(t); + WRITE_32_UNALIGNED(buf + k, creation); + WRITE_64_UNALIGNED(buf + k + 4, ticks); + WRITE_32_UNALIGNED(buf + k + 12, process_id); + } + return k + 16; } else { if (!IS_NULL_PTR(buf)) { uint64_t ticks = term_to_ref_ticks(t); @@ -999,6 +1010,10 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm if (len == 2 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; return term_from_ref_ticks(ticks, heap); + } else if (len == 3 && node == this_node && creation == this_creation) { + uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; + uint32_t process_id = data[2]; + return term_make_process_reference(process_id, ticks, heap); } else if (len == 4 && node == this_node && creation == this_creation) { // This is a resource uint64_t resource_type_ptr = ((uint64_t) data[0]) << 32 | data[1]; @@ -1442,7 +1457,9 @@ static int calculate_heap_usage(const uint8_t *external_term_buf, size_t remaini // Check if it's non-distributed node, in which case it's always a local ref if (external_term_buf[4] == strlen("nonode@nohost") && memcmp(external_term_buf + 5, "nonode@nohost", strlen("nonode@nohost")) == 0) { if (len == 2) { - heap_size = REF_SIZE; + heap_size = TERM_BOXED_REFERENCE_SHORT_SIZE; + } else if (len == 3) { + heap_size = TERM_BOXED_REFERENCE_PROCESS_SIZE; } else if (len == 4) { heap_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE; } diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index eb7779e9d1..2876aaab80 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -46,8 +46,6 @@ extern "C" { #endif -#define INVALID_PROCESS_ID 0 - struct Context; #ifndef TYPEDEF_CONTEXT diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index 8d8bd5b4f8..f25e6aeef7 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -840,15 +840,16 @@ static bool jit_send(Context *ctx, JITState *jit_state) return false; } ctx->x[0] = return_value; - } else { - if (term_is_atom(recipient_term)) { - recipient_term = globalcontext_get_registered_process(ctx->global, term_to_atom_index(recipient_term)); - if (UNLIKELY(recipient_term == UNDEFINED_ATOM)) { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; - } + } else if (term_is_local_pid_or_port(recipient_term)) { + int local_process_id = term_to_local_process_id(recipient_term); + globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); + ctx->x[0] = ctx->x[1]; + } else if (term_is_atom(recipient_term)) { + recipient_term = globalcontext_get_registered_process(ctx->global, term_to_atom_index(recipient_term)); + if (UNLIKELY(recipient_term == UNDEFINED_ATOM)) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } - int local_process_id; if (term_is_local_pid_or_port(recipient_term)) { local_process_id = term_to_local_process_id(recipient_term); @@ -858,7 +859,26 @@ static bool jit_send(Context *ctx, JITState *jit_state) } globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; + } else if (term_is_process_reference(recipient_term)) { + int32_t process_id = term_process_ref_to_process_id(recipient_term); + int64_t ref_ticks = term_to_ref_ticks(recipient_term); + Context *p = globalcontext_get_process_lock(ctx->global, process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (!IS_NULL_PTR(alias)) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, ctx->x[1]); + } + globalcontext_get_process_unlock(ctx->global, p); + } + ctx->x[0] = ctx->x[1]; + } else if (!term_is_reference(recipient_term)) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } + return true; } diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index e1926134c1..6e6962cf48 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -296,6 +296,8 @@ static term nif_unicode_characters_to_binary(Context *ctx, int argc, term argv[] static term nif_erlang_lists_subtract(Context *ctx, int argc, term argv[]); static term nif_erlang_crc32(Context *ctx, int argc, term argv[]); static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]); +static term nif_erlang_alias(Context *ctx, int argc, term argv[]); +static term nif_erlang_unalias(Context *ctx, int argc, term argv[]); static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]); #define DECLARE_MATH_NIF_FUN(moniker) \ @@ -987,6 +989,14 @@ static const struct Nif crc32_combine_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_erlang_crc32_combine_3 }; +static const struct Nif erlang_alias_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_alias +}; +static const struct Nif erlang_unalias_nif = { + .base.type = NIFFunctionType, + .nif_ptr = nif_erlang_unalias +}; static const struct Nif zlib_compress_nif = { .base.type = NIFFunctionType, .nif_ptr = nif_zlib_compress_1 @@ -1330,7 +1340,7 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - if (UNLIKELY(memory_ensure_free_opt(ctx, 12, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); } @@ -1422,6 +1432,42 @@ static NativeHandlerResult process_console_mailbox(Context *ctx) return result; } +static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, enum ContextMonitorAliasType *alias_type) +{ + *is_alias = false; + while (term_is_nonempty_list(monitor_opts)) { + term option = term_get_list_head(monitor_opts); + if (term_is_tuple(option) && term_get_tuple_arity(option) == 2 && term_get_tuple_element(option, 0) == ALIAS_ATOM) { + *is_alias = true; + switch (term_get_tuple_element(option, 1)) { + case EXPLICIT_UNALIAS_ATOM: + *alias_type = ContextMonitorAliasExplicitUnalias; + break; + case DEMONITOR_ATOM: + *alias_type = ContextMonitorAliasDemonitor; + break; + case REPLY_DEMONITOR_ATOM: + *alias_type = ContextMonitorAliasReplyDemonitor; + break; + default: + RAISE_ERROR(BADARG_ATOM); + } + } else if (term_is_tuple(option) && term_get_tuple_arity(option) == 2 && term_get_tuple_element(option, 0) == TAG_ATOM) { + RAISE_ERROR(UNSUPPORTED_ATOM); + } else { + RAISE_ERROR(BADARG_ATOM); + } + + monitor_opts = term_get_list_tail(monitor_opts); + } + + if (UNLIKELY(!term_is_nil(monitor_opts))) { + RAISE_ERROR(BADARG_ATOM); + } + + return OK_ATOM; +} + // Common handling of spawn/1, spawn/3, spawn_opt/2, spawn_opt/4 // opts_term is [] for spawn/1,3 static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_freeze, term opts_term) @@ -1429,7 +1475,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free term min_heap_size_term = interop_proplist_get_value(opts_term, MIN_HEAP_SIZE_ATOM); term max_heap_size_term = interop_proplist_get_value(opts_term, MAX_HEAP_SIZE_ATOM); term link_term = interop_proplist_get_value(opts_term, LINK_ATOM); - term monitor_term = interop_proplist_get_value(opts_term, MONITOR_ATOM); + term monitor_term = interop_proplist_get_value_default(opts_term, MONITOR_ATOM, term_invalid_term()); term heap_growth_strategy = interop_proplist_get_value_default(opts_term, ATOMVM_HEAP_GROWTH_ATOM, BOUNDED_FREE_ATOM); term request_term = interop_proplist_get_value_default(opts_term, REQUEST_ATOM, UNDEFINED_ATOM); term group_leader; @@ -1499,7 +1545,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_destroy(new_ctx); RAISE_ERROR(BADARG_ATOM); } - uint64_t ref_ticks = 0; + RefData ref_data; + bool is_spawn_monitor = false; term new_pid = term_from_local_process_id(new_ctx->process_id); if (link_term == TRUE_ATOM) { @@ -1519,25 +1566,53 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_add_monitor(ctx, self_link); } if (monitor_term == TRUE_ATOM) { - // We can call context_add_monitor directly on new process because it's not started yet - ref_ticks = globalcontext_get_ref_ticks(ctx->global); - struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), ref_ticks, false); + monitor_term = term_nil(); + } + if (term_is_list(monitor_term)) { + is_spawn_monitor = true; + bool is_alias; + enum ContextMonitorAliasType alias_type; + + if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, monitor_term, &is_alias, &alias_type)))) { + context_destroy(new_ctx); + return term_invalid_term(); + } + struct Monitor *alias_monitor = NULL; + if (is_alias) { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + context_destroy(new_ctx); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } else { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; + } + + struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); if (IS_NULL_PTR(new_monitor)) { context_destroy(new_ctx); + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_monitor = monitor_new(new_pid, ref_ticks, true); + struct Monitor *self_monitor = monitor_new(new_pid, &ref_data, true); if (IS_NULL_PTR(self_monitor)) { + free(alias_monitor); free(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + + // We can call context_add_monitor directly on new process because it's not started yet context_add_monitor(new_ctx, new_monitor); context_add_monitor(ctx, self_monitor); + if (is_alias) { + context_add_monitor(ctx, alias_monitor); + } } - if (ref_ticks) { - int res_size = REF_SIZE + TUPLE_SIZE(2); + if (is_spawn_monitor) { + int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1545,13 +1620,13 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free scheduler_init_ready(new_ctx); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + term ref = term_from_ref_data(&ref_data, &ctx->heap); - term pid_ref_tuple = term_alloc_tuple(2, &ctx->heap); - term_put_tuple_element(pid_ref_tuple, 0, new_pid); - term_put_tuple_element(pid_ref_tuple, 1, ref); + term process_ref_tuple = term_alloc_tuple(2, &ctx->heap); + term_put_tuple_element(process_ref_tuple, 0, new_pid); + term_put_tuple_element(process_ref_tuple, 1, ref); - return pid_ref_tuple; + return process_ref_tuple; } else if (UNLIKELY(valid_request)) { // Handling of spawn_request // spawn_request requires that the reply is enqueued before @@ -1744,6 +1819,20 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message(glb, local_process_id, argv[1]); + } else if (term_is_process_reference(target)) { + int32_t process_id = term_process_ref_to_process_id(target); + int64_t ref_ticks = term_to_ref_ticks(target); + Context *p = globalcontext_get_process_lock(glb, process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (alias != NULL) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, argv[1]); + } + globalcontext_get_process_unlock(glb, p); + } } else if (term_is_atom(target)) { // We need to hold a lock on the processes_table until the message is sent to avoid a race condition, // otherwise the receiving process could be killed at any point between checking it is registered, @@ -1768,7 +1857,7 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message_nolock(glb, local_process_id, argv[1]); synclist_unlock(&glb->processes_table); - } else { + } else if (!term_is_reference(target)) { RAISE_ERROR(BADARG_ATOM); } @@ -1847,7 +1936,7 @@ term nif_erlang_make_ref_0(Context *ctx, int argc, term argv[]) UNUSED(argv); // a ref is 64 bits, hence 8 bytes - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -4925,17 +5014,22 @@ static term nif_erlang_memory(Context *ctx, int argc, term argv[]) static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) { - UNUSED(argc); term object_type = argv[0]; term target_proc = argv[1]; + term options = argc == 3 ? argv[2] : term_nil(); term target_pid; size_t target_proc_size = 0; + bool is_alias; + enum ContextMonitorAliasType alias_type; if (object_type != PROCESS_ATOM && object_type != PORT_ATOM) { RAISE_ERROR(BADARG_ATOM); } + if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, options, &is_alias, &alias_type)))) { + return term_invalid_term(); + } if (term_is_atom(target_proc)) { target_pid = globalcontext_get_registered_process(ctx->global, term_to_atom_index(target_proc)); target_proc_size = TUPLE_SIZE(2); @@ -4956,7 +5050,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) local_process_id = term_to_local_process_id(target_pid); // Monitoring self is possible but no monitor is actually created if (UNLIKELY(local_process_id == ctx->process_id)) { - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); @@ -4968,7 +5062,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if (IS_NULL_PTR(target)) { - int res_size = REF_SIZE + TUPLE_SIZE(5) + target_proc_size; + int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5) + target_proc_size; if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -4994,20 +5088,34 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) if ((object_type == PROCESS_ATOM && target->native_handler != NULL) || (object_type == PORT_ATOM && target->native_handler == NULL)) { RAISE_ERROR(BADARG_ATOM); } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term monitoring_pid = term_from_local_process_id(ctx->process_id); + + RefData ref_data; + struct Monitor *alias_monitor = NULL; + if (is_alias) { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + globalcontext_get_process_unlock(ctx->global, target); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } else { + ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; + } struct Monitor *self_monitor; if (term_is_atom(target_proc)) { - self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, ref_ticks); + self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, &ref_data); } else { - self_monitor = monitor_new(target_pid, ref_ticks, true); + self_monitor = monitor_new(target_pid, &ref_data, true); } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); + free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *other_monitor = monitor_new(monitoring_pid, ref_ticks, false); + term monitoring_pid = term_from_local_process_id(ctx->process_id); + struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); if (IS_NULL_PTR(other_monitor)) { + free(alias_monitor); free(self_monitor); globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -5016,12 +5124,15 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) globalcontext_get_process_unlock(ctx->global, target); context_add_monitor(ctx, self_monitor); + if (is_alias) { + context_add_monitor(ctx, alias_monitor); + } - if (UNLIKELY(memory_ensure_free_opt(ctx, REF_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - return term_from_ref_ticks(ref_ticks, &ctx->heap); + return term_from_ref_data(&ref_data, &ctx->heap); } static term nif_erlang_demonitor(Context *ctx, int argc, term argv[]) @@ -7453,6 +7564,42 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) return make_maybe_boxed_int64(ctx, crc); } +static term nif_erlang_alias(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + UNUSED(argv); + + if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; + term process_ref = term_from_ref_data(&ref_data, &ctx->heap); + struct Monitor *monitor = monitor_alias_new(&ref_data, ContextMonitorAliasExplicitUnalias); + if (IS_NULL_PTR(monitor)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + context_add_monitor(ctx, monitor); + return process_ref; +} + +static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) +{ + UNUSED(argc); + + term process_ref = argv[0]; + VALIDATE_VALUE(process_ref, term_is_local_reference); + uint64_t ref_ticks = term_to_ref_ticks(process_ref); + + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (IS_NULL_PTR(alias)) { + return FALSE_ATOM; + } else { + context_unalias(alias); + return TRUE_ATOM; + } +} + #ifdef WITH_ZLIB static term nif_zlib_compress_1(Context *ctx, int argc, term argv[]) { diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index a7281dbc7e..6a4c4f6665 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -101,6 +101,7 @@ erlang:make_ref/0, &make_ref_nif erlang:make_tuple/2, &make_tuple_nif erlang:memory/1, &memory_nif erlang:monitor/2, &monitor_nif +erlang:monitor/3, &monitor_nif erlang:demonitor/1, &demonitor_nif erlang:demonitor/2, &demonitor_nif erlang:is_process_alive/1, &is_process_alive_nif @@ -158,6 +159,8 @@ erlang:loaded/0, &erlang_loaded_nif erlang:module_loaded/1,&module_loaded_nif erlang:nif_error/1,&nif_error_nif erlang:list_to_bitstring/1, &list_to_bitstring_nif +erlang:alias/0, &erlang_alias_nif +erlang:unalias/1, &erlang_unalias_nif erts_debug:flat_size/1, &flat_size_nif erts_internal:cmp_term/2, &erts_internal_cmp_term_nif file:get_cwd/0, IF_HAVE_GETCWD_PATHMAX(&file_get_cwd_nif) diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 889ff319a8..49d3b11342 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -2374,12 +2374,28 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) int local_process_id; if (term_is_local_pid_or_port(recipient_term)) { local_process_id = term_to_local_process_id(recipient_term); - } else { + TRACE("send/0 target_pid=%i\n", local_process_id); + TRACE_SEND(ctx, x_regs[0], x_regs[1]); + globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); + } else if (term_is_process_reference(recipient_term)) { + int32_t local_process_id = term_process_ref_to_process_id(recipient_term); + TRACE("send/0 target_pid=%i\n", local_process_id); + TRACE_SEND(ctx, x_regs[0], x_regs[1]); + int64_t ref_ticks = term_to_ref_ticks(recipient_term); + Context *p = globalcontext_get_process_lock(ctx->global, local_process_id); + if (p) { + struct MonitorAlias *alias = context_find_alias(p, ref_ticks); + if (alias != NULL) { + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + context_unalias(alias); + } + mailbox_send(p, x_regs[1]); + } + globalcontext_get_process_unlock(ctx->global, p); + } + } else if (!term_is_reference(recipient_term)) { RAISE_ERROR(BADARG_ATOM); } - TRACE("send/0 target_pid=%i\n", local_process_id); - TRACE_SEND(ctx, x_regs[0], x_regs[1]); - globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); x_regs[0] = x_regs[1]; } break; diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 4195a96241..37381fe344 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -246,7 +246,7 @@ static const AtomStringIntPair otp_socket_setopt_level_table[] = { static ErlNifResourceType *socket_resource_type; -#define SOCKET_MAKE_SELECT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + REF_SIZE + TUPLE_SIZE(2) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) +#define SOCKET_MAKE_SELECT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) static term socket_make_select_notification(struct SocketResource *rsrc_obj, Heap *heap); // @@ -644,7 +644,7 @@ static term nif_socket_open(Context *ctx, int argc, term argv[]) term obj = term_from_resource(rsrc_obj, &ctx->heap); enif_release_resource(rsrc_obj); // decrement refcount after enif_alloc_resource - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -695,7 +695,7 @@ bool term_is_otp_socket(term socket_term) static int send_closed_notification(Context *ctx, term socket_term, int32_t selecting_process_id, struct SocketResource *rsrc_obj) { // send a {'$socket', Socket, abort, {Ref | undefined, closed}} message to the pid - if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(4) + TUPLE_SIZE(2) + REF_SIZE, 1, &socket_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(4) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE, 1, &socket_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); return -1; } @@ -1814,7 +1814,7 @@ static term nif_socket_listen(Context *ctx, int argc, term argv[]) #if OTP_SOCKET_LWIP static term make_accepted_socket_term(Context *ctx, struct SocketResource *conn_rsrc_obj) { - if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term obj = term_from_resource(conn_rsrc_obj, &ctx->heap); @@ -1909,7 +1909,7 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) term new_resource = term_from_resource(conn_rsrc_obj, &ctx->heap); enif_release_resource(conn_rsrc_obj); // decrement refcount after enif_alloc_resource - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &new_resource, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -1940,7 +1940,7 @@ static term nif_socket_accept(Context *ctx, int argc, term argv[]) SMP_RWLOCK_UNLOCK(rsrc_obj->socket_lock); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - size_t requested_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2) + REF_SIZE; + size_t requested_size = TERM_BOXED_REFERENCE_RESOURCE_SIZE + TUPLE_SIZE(2) + TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, argv, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); LWIP_END(); @@ -2319,7 +2319,7 @@ static term nif_socket_recv_lwip(Context *ctx, term resource_term, struct Socket } size_t ensure_packet_avail = term_binary_heap_size(buffer_size); - size_t requested_size = REF_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); + size_t requested_size = TERM_BOXED_REFERENCE_SHORT_SIZE + 2 * TUPLE_SIZE(2) + ensure_packet_avail + (is_recvfrom ? (TUPLE_SIZE(2) + INET_ADDR4_TUPLE_SIZE + TERM_MAP_SIZE(2)) : 0); // Because resource is locked, we must ensure it's not garbage collected if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &resource_term, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { AVM_LOGW(TAG, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); diff --git a/src/libAtomVM/resources.h b/src/libAtomVM/resources.h index 5c95a5c4ed..58c97cf9b6 100644 --- a/src/libAtomVM/resources.h +++ b/src/libAtomVM/resources.h @@ -164,7 +164,7 @@ void select_event_count_and_destroy_closed(struct ListHead *select_events, size_ */ void destroy_resource_monitors(struct RefcBinary *resource, GlobalContext *global); -#define SELECT_EVENT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) +#define SELECT_EVENT_NOTIFICATION_SIZE (TUPLE_SIZE(4) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE) /** * @brief Build a select event notification. diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index f8b1a0cce3..7fdfde55d3 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -405,6 +405,12 @@ int term_funprint(PrinterFun *fun, term t, const GlobalContext *global) uint64_t resource_ptr = (uintptr_t) refc_binary->data; return fun->print(fun, "#Ref<0.%" PRIu32 ".%" PRIu32 ".%" PRIu32 ".%" PRIu32 ">", (uint32_t) (resource_type_ptr >> 32), (uint32_t) resource_type_ptr, (uint32_t) (resource_ptr >> 32), (uint32_t) resource_ptr); + } else if (term_is_process_reference(t)) { + int32_t process_id = term_process_ref_to_process_id(t); + uint64_t ref_ticks = term_to_ref_ticks(t); + + // Update also REF_AS_CSTRING_LEN when changing this format string + return fun->print(fun, "#Ref<%" PRId32 ".%" PRIu32 ".%" PRIu32 ">", process_id, (uint32_t) (ref_ticks >> 32), (uint32_t) ref_ticks); } else if (term_is_local_reference(t)) { // Update also REF_AS_CSTRING_LEN when changing this format string uint64_t ref_ticks = term_to_ref_ticks(t); @@ -676,11 +682,15 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC uint32_t len, other_len; if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } if (term_is_resource_reference(other)) { other_len = 4; + } else if (term_is_process_reference(other)) { + other_len = 3; } else { other_len = 2; } @@ -694,6 +704,15 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC int64_t other_ticks = term_to_ref_ticks(other); other_data[0] = other_ticks >> 32; other_data[1] = (uint32_t) other_ticks; + } else if (len == 3) { + data[0] = term_process_ref_to_process_id(t); + int64_t t_ticks = term_to_ref_ticks(t); + data[1] = t_ticks >> 32; + data[2] = (uint32_t) t_ticks; + other_data[0] = term_process_ref_to_process_id(other); + int64_t other_ticks = term_to_ref_ticks(other); + other_data[1] = other_ticks >> 32; + other_data[2] = (uint32_t) other_ticks; } else { // len == 4 struct RefcBinary *refc = term_resource_refc_binary_ptr(t); diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index df1d411354..b68a0bb57b 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -129,7 +129,14 @@ extern "C" { #define TERM_BOXED_REFC_BINARY_SIZE 6 #define TERM_BOXED_BIN_MATCH_STATE_SIZE 4 #define TERM_BOXED_SUB_BINARY_SIZE 4 +#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_RESOURCE_SIZE 4 +#else +// Enough size would be 4, but reference types +// are distinguished by size and 4 conflicts with +// TERM_BOXED_REFERENCE_PROCESS_SIZE on 32bit arch. +#define TERM_BOXED_REFERENCE_RESOURCE_SIZE 5 +#endif #define TERM_BOXED_REFERENCE_RESOURCE_HEADER (((TERM_BOXED_REFERENCE_RESOURCE_SIZE - 1) << 6) | TERM_BOXED_REF) #define TERM_BOXED_RESOURCE_SIZE TERM_BOXED_REFERENCE_RESOURCE_SIZE @@ -151,7 +158,13 @@ extern "C" { #define BOXED_INT64_SIZE (BOXED_TERMS_REQUIRED_FOR_INT64 + 1) #define BOXED_FUN_SIZE 3 #define FLOAT_SIZE (sizeof(float_term_t) / sizeof(term) + 1) -#define REF_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) +// Reference types are distinguished by their size. +// If you change a reference size, make sure it doesn't +// conflict with other reference sizes on all architectures. +#define TERM_BOXED_REFERENCE_SHORT_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) +#define REF_SIZE _Pragma("REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead") TERM_BOXED_REFERENCE_SHORT_SIZE +#define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) +#define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) #if TERM_BYTES == 8 #define EXTERNAL_PID_SIZE 3 #elif TERM_BYTES == 4 @@ -167,10 +180,23 @@ extern "C" { #else #error #endif +#define EXTERNAL_REF_MAX_WORDS 5 +#define TERM_BOXED_REFERENCE_MAX_SIZE EXTERNAL_REF_SIZE(EXTERNAL_REF_MAX_WORDS) +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE < TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must be smaller than process ref size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE < TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must be smaller than reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 #define REFERENCE_RESOURCE_CONS_OFFSET 2 + +#if TERM_BYTES == 4 +#define REFERENCE_PROCESS_PID_OFFSET 3 + +#elif TERM_BYTES == 8 +#define REFERENCE_PROCESS_PID_OFFSET 2 +#endif + #define LIST_SIZE(num_elements, element_size) ((num_elements) * ((element_size) + CONS_SIZE)) #define TERM_STRING_SIZE(length) (2 * (length)) #define TERM_MAP_SIZE(num_elements) (3 + 2 * (num_elements)) @@ -179,6 +205,8 @@ extern "C" { #define LIST_HEAD_INDEX 1 #define LIST_TAIL_INDEX 0 +#define INVALID_PROCESS_ID 0 + #define TERM_BINARY_SIZE_IS_HEAP(size) ((size) < REFC_BINARY_MIN) #if TERM_BYTES == 4 @@ -210,6 +238,9 @@ extern "C" { // Local ref is at most 30 bytes: // 2^32-1 = 4294967295 (10 chars) // "#Ref<0." "." ">\0" (10 chars) +// Process ref is at most 39 bytes: +// 2^32-1 = 4294967295 (10 chars) +// "#Ref<" "." "." ">\0" (9 chars) // Resource ref is at most 52 bytes: // 2^32-1 = 4294967295 (10 chars) // "#Ref<0." "." "." "." ">\0" (12 chars) @@ -243,6 +274,21 @@ extern "C" { typedef struct GlobalContext GlobalContext; #endif +enum RefType +{ + RefTypeShort, + RefTypeProcess, + RefTypeResource, + RefTypeExternal +}; + +typedef struct RefData RefData; +struct RefData +{ + uint64_t ref_ticks; + int32_t process_id; +}; + typedef struct PrinterFun PrinterFun; typedef int (*printer_function_t)(PrinterFun *fun, const char *fmt, ...) PRINTF_FORMAT_ARGS(2, 3); @@ -915,6 +961,25 @@ static inline bool term_is_local_reference(term t) return false; } +/** + * @brief Checks if a term is a process reference + * + * @details See \c term_make_process_reference(). + * @param t the term that will be checked. + * @return \c true if check succeeds, \c false otherwise. + */ +static inline bool term_is_process_reference(term t) +{ + if (term_is_boxed(t)) { + const term *boxed_value = term_to_const_term_ptr(t); + if (boxed_value[0] == TERM_BOXED_REFERENCE_PROCESS_HEADER) { + return true; + } + } + + return false; +} + /** * @brief Checks if a term is an external reference * @@ -2166,8 +2231,8 @@ static inline int term_bs_insert_binary(term t, int offset, term src, int n) */ static inline term term_from_ref_ticks(uint64_t ref_ticks, Heap *heap) { - term *boxed_value = memory_heap_alloc(heap, REF_SIZE); - boxed_value[0] = ((REF_SIZE - 1) << 6) | TERM_BOXED_REF; + term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_SHORT_SIZE); + boxed_value[0] = ((TERM_BOXED_REFERENCE_SHORT_SIZE - 1) << 6) | TERM_BOXED_REF; #if TERM_BYTES == 8 boxed_value[1] = (term) ref_ticks; @@ -2200,6 +2265,50 @@ static inline uint64_t term_to_ref_ticks(term rt) #endif } +/** + * @brief Creates a process reference + * @details Process reference contains ref_ticks and process_id of a process. + * They are used by process aliases and monitors. + * + * @param process_id process_id of a process that the reference will identify. + * @param ref_ticks an unique uint64 value that will be used to create ref term. + * @param heap the heap to allocate memory in + * @return a ref term created using given ref ticks. + */ +static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ticks, Heap *heap) +{ + term *boxed_value = memory_heap_alloc(heap, TERM_BOXED_REFERENCE_PROCESS_SIZE); + boxed_value[0] = TERM_BOXED_REFERENCE_PROCESS_HEADER; + +#if TERM_BYTES == 4 + boxed_value[1] = (ref_ticks >> 32); + boxed_value[2] = (ref_ticks & 0xFFFFFFFF); + boxed_value[3] = process_id; + +#elif TERM_BYTES == 8 + boxed_value[1] = (term) ref_ticks; + boxed_value[2] = process_id; + +#else +#error "terms must be either 32 or 64 bit wide" +#endif + + return ((term) boxed_value) | TERM_PRIMARY_BOXED; +} + +static inline uint32_t term_process_ref_to_process_id(term rt) +{ + TERM_DEBUG_ASSERT(term_is_process_reference(rt)); + const term *boxed_value = term_to_const_term_ptr(rt); +#if TERM_BYTES == 4 + return (uint32_t) boxed_value[3]; +#elif TERM_BYTES == 8 + return (uint32_t) boxed_value[2]; +#else +#error "terms must be either 32 or 64 bit wide" +#endif +} + /** * @brief Make an external pid term from node, process_id, serial and creation * @@ -2336,7 +2445,7 @@ static inline uint64_t term_get_external_port_number(term t) * @param heap the heap to allocate memory in * @return an external heap term created using given parameters. */ -static inline term term_make_external_reference(term node, uint16_t len, uint32_t *data, uint32_t creation, Heap *heap) +static inline term term_make_external_reference(term node, uint16_t len, const uint32_t *data, uint32_t creation, Heap *heap) { TERM_DEBUG_ASSERT(term_is_atom(node)); @@ -2981,6 +3090,15 @@ static inline term term_from_resource(void *resource, Heap *heap) return ret; } +static inline term term_from_ref_data(RefData *ref_data, Heap *heap) +{ + if (ref_data->process_id == INVALID_PROCESS_ID) { + return term_from_ref_ticks(ref_data->ref_ticks, heap); + } else { + return term_make_process_reference(ref_data->process_id, ref_data->ref_ticks, heap); + } +} + /** * @brief Get a resource term from a resource type and a serialization reference * number. diff --git a/src/platforms/emscripten/src/lib/websocket_nifs.c b/src/platforms/emscripten/src/lib/websocket_nifs.c index a6d0bc8c89..ab8e084652 100644 --- a/src/platforms/emscripten/src/lib/websocket_nifs.c +++ b/src/platforms/emscripten/src/lib/websocket_nifs.c @@ -95,7 +95,7 @@ static void websocket_down(ErlNifEnv *caller_env, void *obj, ErlNifPid *pid, Erl } } -#define TERM_WEBSOCKET_RESOURCE_SIZE (TERM_BOXED_RESOURCE_SIZE + REF_SIZE + TUPLE_SIZE(3)) +#define TERM_WEBSOCKET_RESOURCE_SIZE (TERM_BOXED_RESOURCE_SIZE + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(3)) static term term_make_websocket_resource(struct WebsocketResource *rsrc, Heap *heap) { diff --git a/src/platforms/esp32/components/avm_builtins/adc_driver.c b/src/platforms/esp32/components/avm_builtins/adc_driver.c index 2ad34bccd0..210c8e8b98 100644 --- a/src/platforms/esp32/components/avm_builtins/adc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/adc_driver.c @@ -344,7 +344,7 @@ static term nif_adc_init(Context *ctx, int argc, term argv[]) #endif // {ok, {'$adc', Unit :: resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { enif_release_resource(unit_rsrc); @@ -492,7 +492,7 @@ static term nif_adc_acquire(Context *ctx, int argc, term argv[]) chan_rsrc->calibration = calibration; // {ok, {'$adc', resource(), ref()}} - size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + REF_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; + size_t requested_size = TUPLE_SIZE(2) + TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; ESP_LOGD(TAG, "Requesting memory size %u for return message", requested_size); if (UNLIKELY(memory_ensure_free(ctx, requested_size) != MEMORY_GC_OK)) { enif_release_resource(chan_rsrc); diff --git a/src/platforms/esp32/components/avm_builtins/dac_driver.c b/src/platforms/esp32/components/avm_builtins/dac_driver.c index 878cd15454..c06a5777af 100644 --- a/src/platforms/esp32/components/avm_builtins/dac_driver.c +++ b/src/platforms/esp32/components/avm_builtins/dac_driver.c @@ -127,7 +127,7 @@ static term nif_oneshot_new_channel_p(Context *ctx, int argc, term argv[]) enif_release_resource(chan_rsrc); if (!err) { - if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3) + REF_SIZE + TUPLE_SIZE(2), 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_with_roots(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2), 1, &chan_obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "failed to allocate memory for result: %s:%i.", __FILE__, __LINE__); dac_oneshot_del_channel(chan_rsrc->handle); chan_rsrc->handle = NULL; diff --git a/src/platforms/esp32/components/avm_builtins/i2c_resource.c b/src/platforms/esp32/components/avm_builtins/i2c_resource.c index c8f906c85e..08bb9c21dc 100644 --- a/src/platforms/esp32/components/avm_builtins/i2c_resource.c +++ b/src/platforms/esp32/components/avm_builtins/i2c_resource.c @@ -269,7 +269,7 @@ static term nif_i2c_open(Context *ctx, int argc, term argv[]) // // {'$i2c', Resource :: resource(), Ref :: reference()} :: i2c() - size_t requested_size = TUPLE_SIZE(3) + REF_SIZE; + size_t requested_size = TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_SHORT_SIZE; if (UNLIKELY(memory_ensure_free_with_roots(ctx, requested_size, 1, &obj, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { close_i2c_resource(rsrc_obj); ESP_LOGW(TAG, "Failed to allocate memory: %s:%i.", __FILE__, __LINE__); diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index ad0f3e924b..84eeb21167 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -75,7 +75,7 @@ #define BSSID_SIZE 6 #define TAG "network_driver" -#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) static const char *const ap_atom = ATOM_STR("\x2", "ap"); static const char *const ap_channel_atom = ATOM_STR("\xA", "ap_channel"); @@ -1886,7 +1886,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) return NativeContinue; } - // TODO: port this code to standard port (and gen_message) + //TODO: port this code to standard port (and gen_message) term pid = term_get_tuple_element(msg, 0); term ref = term_get_tuple_element(msg, 1); term cmd = term_get_tuple_element(msg, 2); @@ -1934,7 +1934,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) default: { ESP_LOGE(TAG, "Unrecognized command: %x", cmd); // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Unable to allocate heap space for error; no message sent"); return NativeContinue; @@ -1944,7 +1944,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) } } else { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Unable to allocate heap space for error; no message sent"); return NativeContinue; diff --git a/src/platforms/esp32/components/avm_builtins/socket_driver.c b/src/platforms/esp32/components/avm_builtins/socket_driver.c index 99f7933677..9330c69ce1 100644 --- a/src/platforms/esp32/components/avm_builtins/socket_driver.c +++ b/src/platforms/esp32/components/avm_builtins/socket_driver.c @@ -443,7 +443,7 @@ static struct UDPSocketData *udp_socket_data_new(Context *ctx, struct netconn *c } // When this method is called, ensure free was called with REPLY_SIZE -#define REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) static void do_send_reply(Context *ctx, term reply, uint64_t ref_ticks, int32_t pid) { GlobalContext *glb = ctx->global; diff --git a/src/platforms/esp32/components/avm_builtins/uart_driver.c b/src/platforms/esp32/components/avm_builtins/uart_driver.c index 761370840c..ace2aba894 100644 --- a/src/platforms/esp32/components/avm_builtins/uart_driver.c +++ b/src/platforms/esp32/components/avm_builtins/uart_driver.c @@ -237,7 +237,7 @@ EventListener *uart_interrupt_callback(GlobalContext *glb, EventListener *listen int bin_size = term_binary_heap_size(count); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { free(usj_buf); fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); @@ -804,7 +804,7 @@ static NativeHandlerResult uart_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "[uart_driver_consume_mailbox] Failed to allocate space for error tuple"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } diff --git a/src/platforms/rp2/src/lib/networkdriver.c b/src/platforms/rp2/src/lib/networkdriver.c index 84f280fed4..f7ceafbabb 100644 --- a/src/platforms/rp2/src/lib/networkdriver.c +++ b/src/platforms/rp2/src/lib/networkdriver.c @@ -41,7 +41,7 @@ #pragma GCC diagnostic pop -#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + REF_SIZE) +#define PORT_REPLY_SIZE (TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE) #define DEFAULT_HOSTNAME_FMT "atomvm-%02x%02x%02x%02x%02x%02x" #define DEFAULT_HOSTNAME_SIZE (strlen("atomvm-") + 12 + 1) @@ -758,7 +758,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) default: { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { return NativeContinue; } @@ -767,7 +767,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) } } else { // {Ref, {error, badarg}} - size_t heap_size = TUPLE_SIZE(2) + REF_SIZE + TUPLE_SIZE(2); + size_t heap_size = TUPLE_SIZE(2) + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2); if (UNLIKELY(memory_ensure_free(ctx, heap_size) != MEMORY_GC_OK)) { return NativeContinue; } diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 17d507c4a1..5bb2e58104 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -157,6 +157,7 @@ start() -> ok = test_safe_option(), ok = test_invalid_export_fun_encoding(), ok = test_atom_utf8_ext_node(), + ok = test_encode_process_ref(), 0. test_reverse(T, Interop) -> @@ -994,6 +995,16 @@ test_encode_resource(OTPVersion) -> AlteredResource4 = binary_to_term(AlteredResourceBin4), false = AlteredResource4 =:= Resource, ok. +test_encode_process_ref() -> + AliasesAvailable = is_atomvm_or_otp_version_at_least("24"), + if + AliasesAvailable -> + ProcessRef = erlang:alias(), + ProcessRef = binary_to_term(term_to_binary(ProcessRef)), + ok; + true -> + ok + end. % Verify term_to_binary(binary_to_term(Bin)) is idempotent. binary_to_term_idempotent(Binary, _OTPVersion) -> @@ -1001,6 +1012,12 @@ binary_to_term_idempotent(Binary, _OTPVersion) -> Binary = term_to_binary(Term), Term. +is_atomvm_or_otp_version_at_least(OTPVersion) -> + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= OTPVersion + end. + test_atom_encoding() -> true = compare_pair_encoding(latin1_as_utf8_1), true = compare_pair_encoding(latin1_as_utf8_2), diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 55b070e74a..7d742fee44 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -34,6 +34,33 @@ start() -> ok = test_monitor_demonitor_from_other(), ok = test_monitor_registered(), ok = test_monitor_registered_noproc(), + + AliasesAvailable = + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= "24" + end, + if + AliasesAvailable -> + ok = test_alias(), + ok = test_multiple_aliases(), + ok = test_multiple_unaliases(), + ok = test_unalias_from_wrong_process(), + ok = test_monitor_alias_dead_process(), + ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), + ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_down_alias(fun spawn_monitor/2), + ok = test_monitor_down_alias(fun spawn_and_monitor/2), + ok; + true -> + ok + end, 0. test_monitor_normal() -> @@ -229,7 +256,131 @@ test_monitor_demonitor_from_other() -> end, ok. +test_alias() -> + P = spawn_opt(fun echo_loop/0, []), + Alias = erlang:alias(), + do_test_alias(P, Alias), + ok. + +test_multiple_aliases() -> + P = spawn_opt(fun echo_loop/0, []), + A1 = erlang:alias(), + A2 = erlang:alias(), + A3 = erlang:alias(), + do_test_alias(P, A1), + do_test_alias(P, A3), + do_test_alias(P, A2), + ok. + +test_multiple_unaliases() -> + A = erlang:alias(), + true = erlang:unalias(A), + false = erlang:unalias(A), + false = erlang:unalias(A), + ok. + +test_unalias_from_wrong_process() -> + A = erlang:alias(), + TestProcess = self(), + spawn_opt(fun() -> TestProcess ! erlang:unalias(A) end, [link]), + false = recv_one(), + P = spawn_opt(fun echo_loop/0, []), + do_test_alias(P, A), + ok. + +do_test_alias(P, Alias) -> + do_test_alias(P, Alias, fun erlang:unalias/1). + +do_test_alias(P, Alias, UnaliasFun) -> + Ref = make_ref(), + P ! {{m1, Ref}, Alias}, + {m1, Ref} = recv_one(), + UnaliasFun(Alias), + P ! {{m2, Ref}, Alias}, + P ! {{m3, Ref}, self()}, + {m3, Ref} = recv_one(), + ok. + +test_monitor_alias_demonitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + do_test_alias(P, Mon, fun demonitor/1), + ok. + +test_monitor_alias_explicit_unalias(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, explicit_unalias}]), + P ! {m1, Mon}, + m1 = recv_one(), + demonitor(Mon), + do_test_alias(P, Mon), + ok. + +test_monitor_alias_reply_demonitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), + do_test_alias(P, Mon, fun(_Mon) -> ok end), + ok. + +test_monitor_down_alias(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + erlang:unalias(Mon), + P ! {m1, Mon}, + P ! {m2, self()}, + m2 = recv_one(), + P ! quit, + {'DOWN', Mon, process, P, normal} = recv_one(), + ok. + +test_monitor_multiple_aliases_monitors(SpawnFun) -> + {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), + Mon3 = erlang:monitor(process, P, [{alias, explicit_unalias}]), + Mon4 = erlang:monitor(process, P), + A1 = erlang:alias(), + A2 = erlang:alias(), + do_test_alias(P, A2), + do_test_alias(P, Mon3), + do_test_alias(P, A1), + do_test_alias(P, Mon1, fun demonitor/1), + P ! quit, + {'DOWN', Mon2, process, P, normal} = recv_one(), + {'DOWN', Mon3, process, P, normal} = recv_one(), + {'DOWN', Mon4, process, P, normal} = recv_one(), + ok. + +test_monitor_alias_dead_process() -> + {P, Mon0} = spawn_opt(fun() -> ok end, [monitor]), + {'DOWN', Mon0, process, P, normal} = recv_one(), + Mon1 = erlang:monitor(process, P, [{alias, demonitor}]), + {'DOWN', Mon1, process, P, noproc} = recv_one(), + Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), + {'DOWN', Mon2, process, P, noproc} = recv_one(), + Mon3 = erlang:monitor(process, P, [{alias, explicit_unalias}]), + {'DOWN', Mon3, process, P, noproc} = recv_one(), + ok. + +spawn_monitor(LoopFun, Opts) -> + spawn_opt(LoopFun, [{monitor, Opts}]). + +spawn_and_monitor(LoopFun, Opts) -> + P = spawn_opt(LoopFun, []), + Mon = erlang:monitor(process, P, Opts), + {P, Mon}. + normal_loop() -> receive {Caller, quit} -> Caller ! {self(), finished} end. + +echo_loop() -> + receive + quit -> + ok; + {Msg, ReplyTo} -> + ReplyTo ! Msg, + echo_loop() + end. + +recv_one() -> + receive + Msg -> Msg + after 500 -> timeout + end. diff --git a/tests/erlang_tests/test_refs_ordering.erl b/tests/erlang_tests/test_refs_ordering.erl index 5071ae5633..6b54f8bf24 100644 --- a/tests/erlang_tests/test_refs_ordering.erl +++ b/tests/erlang_tests/test_refs_ordering.erl @@ -20,17 +20,17 @@ -module(test_refs_ordering). --export([start/0, sort/1, insert/2, check/2, get_ref/2]). +-export([start/0, sort/1, insert/2, check/2, get_ref/3, make_alias_ref/0]). start() -> - A = get_ref(3, []), - B = get_ref(7, []), - C = get_ref(1, []), - D = get_ref(3, []), - E = get_ref(4, []), + A = get_ref(3, [], fun make_ref/0), + B = get_ref(7, [], fun make_alias_ref/0), + C = get_ref(1, [], fun make_ref/0), + D = get_ref(3, [], fun make_alias_ref/0), + E = get_ref(4, [], fun make_ref/0), Sorted = sort([E, C, D, A, B]), - check(Sorted, [A, B, C, D, E]) + - bool_to_n(Sorted < [make_ref()]) * 2 + + check(Sorted, [A, C, E, B, D]) + + bool_to_n(Sorted < [make_alias_ref()]) * 2 + bool_to_n(Sorted > {make_ref()}) * 4. sort(L) -> @@ -57,12 +57,25 @@ check(T, Expected) when T == Expected -> check(T, Expected) when T /= Expected -> 0. -get_ref(0, Acc) -> +get_ref(0, Acc, _Generator) -> Acc; -get_ref(N, _Acc) -> - get_ref(N - 1, make_ref()). +get_ref(N, _Acc, Generator) -> + get_ref(N - 1, Generator(), Generator). bool_to_n(true) -> 1; bool_to_n(false) -> 0. + +make_alias_ref() -> + AliasesAvailable = + case erlang:system_info(machine) of + "ATOM" -> true; + "BEAM" -> erlang:system_info(otp_release) >= "24" + end, + if + AliasesAvailable -> + erlang:alias(); + true -> + {mock_alias_ref, make_ref()} + end. From b6b55d3724f21966ba20108836d4f477837f602d Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 09:26:13 +0000 Subject: [PATCH 2/6] Fix the leaked process lock on a monitor badarg nif_erlang_monitor locks the target process, then raises badarg when the object type does not match the target, as in monitor(process, Port, ...) or the other way around. That error path returned without releasing the processes table lock, unlike the three sibling out of memory error paths in the same function, leaking the lock and deadlocking the next writer on SMP builds. Unlock the target before raising. Signed-off-by: Davide Bettio --- src/libAtomVM/nifs.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 6e6962cf48..004d46ef97 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -5086,6 +5086,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if ((object_type == PROCESS_ATOM && target->native_handler != NULL) || (object_type == PORT_ATOM && target->native_handler == NULL)) { + globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(BADARG_ATOM); } From 77b4984b48acfe3398e0b8e1a9e473a178b0ba8f Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Wed, 3 Jun 2026 10:09:13 +0000 Subject: [PATCH 3/6] Add the missing break in context_demonitor The CONTEXT_MONITOR_RESOURCE case fell through to the following monitor cases when the ref ticks did not match. It is benign today, since the next cases only break, but the process alias support added CONTEXT_MONITOR_ALIAS to the same switch, so tighten the fall through. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 14da1c327b..7fc2c2142d 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -1129,6 +1129,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) free(resource_monitor); return; } + break; } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_LINK_REMOTE: From 63544d88612b602e2c5c40823ab52480c3aa5165 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Thu, 4 Jun 2026 09:32:54 +0000 Subject: [PATCH 4/6] Remove destroyed processes from the waiting queue globalcontext_init_process appends every new context to the scheduler waiting queue, but context_destroy never removed it. Destroying a process that was never scheduled, as the spawn NIFs do on option errors and port drivers on initialization failures, left a dangling queue entry pointing into freed memory, corrupting the queue on the next scheduler operation. The defect predates the process alias support; it was first exercised by the spawn_opt atomicity test added later in this series. Dequeue in context_destroy under the processes spinlock, and reset the queue item at the scheduler's own dequeue sites so the destroy time dequeue is a no-op there. A concurrent re-queue cannot happen: scheduler_make_ready refuses Killed and Spawning processes under the same spinlock, and one of these flags is set on every destroy path. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 8 ++++++++ src/libAtomVM/scheduler.c | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 7fc2c2142d..32ce4f46ad 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -139,6 +139,14 @@ void context_destroy(Context *ctx) // Hold and release the spin lock for timers and cancel any timer scheduler_cancel_timeout(ctx); + // Remove the process from the scheduler queue. A process terminated by the scheduler was + // already dequeued (and its queue item was reset, making this a no-op), but a process that + // is destroyed before it was ever scheduled -- e.g. on a spawn_opt option error -- is still + // on the waiting queue, and destroying it without dequeuing would leave a dangling entry. + SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); + list_remove(&ctx->processes_list_head); + SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); + // Another process can get an access to our mailbox until this point. struct ListHead *processes_table_list = synclist_wrlock(&ctx->global->processes_table); UNUSED(processes_table_list); diff --git a/src/libAtomVM/scheduler.c b/src/libAtomVM/scheduler.c index a856d60012..ffe9eb30e7 100644 --- a/src/libAtomVM/scheduler.c +++ b/src/libAtomVM/scheduler.c @@ -289,6 +289,8 @@ Context *scheduler_run(GlobalContext *global) if (UNLIKELY(result->flags & Killed)) { SMP_SPINLOCK_LOCK(&global->processes_spinlock); list_remove(&result->processes_list_head); + // Reset the queue item so the dequeue in context_destroy is a no-op. + list_init(&result->processes_list_head); SMP_SPINLOCK_UNLOCK(&global->processes_spinlock); context_destroy(result); } else { @@ -429,6 +431,9 @@ void scheduler_terminate(Context *ctx) SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); context_update_flags(ctx, ~NoFlags, Killed); list_remove(&ctx->processes_list_head); + // Reset the queue item so the dequeue in context_destroy is a no-op. For a leader process, + // context_destroy is called after the scheduler loop returns. + list_init(&ctx->processes_list_head); SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); if (!ctx->leader) { context_destroy(ctx); From 236fa9942724ff2e1bcc5dae7362308fbcb61890 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 19 Jul 2026 12:23:03 +0000 Subject: [PATCH 5/6] Fix the issues found in the process alias support Address the issues found while reviewing the process alias support: alias message send races and ordering, monitor and alias lifecycle, out of memory unwinding, reference representation and comparison, and the delivery of alias messages through the mailbox. Complete the API with erlang:alias/1 and with incoming distributed alias sends, and add the missing tests and documentation. Each issue is described and discussed in the pull requests, so the individual findings are not repeated here. See also: GH #2326, GH #2027 Signed-off-by: Davide Bettio --- CHANGELOG.md | 7 + doc/src/apidocs/libatomvm/functions.rst | 1 + doc/src/distributed-erlang.md | 7 + libs/estdlib/src/erlang.erl | 49 +- src/libAtomVM/context.c | 128 +++- src/libAtomVM/context.h | 50 +- src/libAtomVM/defaultatoms.def | 2 + src/libAtomVM/dist_nifs.c | 23 + src/libAtomVM/external_term.c | 3 + src/libAtomVM/globalcontext.c | 14 + src/libAtomVM/globalcontext.h | 14 + src/libAtomVM/jit.c | 36 +- src/libAtomVM/mailbox.c | 172 ++++- src/libAtomVM/mailbox.h | 21 +- src/libAtomVM/nifs.c | 204 ++++-- src/libAtomVM/nifs.gperf | 1 + src/libAtomVM/opcodesswitch.h | 28 +- src/libAtomVM/otp_socket.c | 4 + src/libAtomVM/resources.c | 4 + src/libAtomVM/scheduler.c | 5 +- src/libAtomVM/term.c | 36 +- src/libAtomVM/term.h | 71 +- .../components/avm_builtins/network_driver.c | 2 +- .../components/avm_builtins/usb_cdc_driver.c | 8 +- src/platforms/rp2/src/lib/usb_cdc_driver.c | 6 +- src/platforms/stm32/src/lib/usb_cdc_driver.c | 6 +- tests/erlang_tests/test_binary_to_term.erl | 21 +- tests/erlang_tests/test_monitor.erl | 636 +++++++++++++++++- tests/erlang_tests/test_refs_ordering.erl | 12 +- tests/libs/eavmlib/test_file.erl | 20 + tests/libs/estdlib/test_net_kernel.erl | 27 + tests/libs/estdlib/test_udp_socket.erl | 18 + tests/test-mailbox.c | 12 +- 33 files changed, 1369 insertions(+), 279 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bb52ca3e6..17cbdad636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,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 support for process aliases: `erlang:alias/0,1`, `erlang:unalias/1`, + `erlang:monitor/3` with the `{alias, Mode}` option, `spawn_opt` `{monitor, MonitorOpts}` and + sending to an alias reference - Added xtensa JIT backend for esp32 platform - Added support for configuring pins and width for sdmmc on ESP32 - Added support for map comprehensions @@ -49,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 for this limit - Replaced the `exavmlib` `Protocol` module with a small runtime shim (only `Protocol.__concat__/2`), saving ~31 KB of flash. ExAtomVM uses precompiled, unconsolidated protocols and continue to function normally at runtime. +- Deprecated the C macro `REF_SIZE`: use `TERM_BOXED_REFERENCE_SHORT_SIZE` for references built + from ref ticks, `TERM_BOXED_REFERENCE_PROCESS_SIZE` for process references (aliases), or + `TERM_BOXED_REFERENCE_MAX_SIZE` to fit any reference. `REF_SIZE` still expands to the short + reference size, but now emits a compiler warning ### Removed - Removed `ahttp_client` support for obsolete line folding (RFC 9112 §5.2); folded header and diff --git a/doc/src/apidocs/libatomvm/functions.rst b/doc/src/apidocs/libatomvm/functions.rst index 3ae61e9fd4..e25a540eef 100644 --- a/doc/src/apidocs/libatomvm/functions.rst +++ b/doc/src/apidocs/libatomvm/functions.rst @@ -130,6 +130,7 @@ Functions .. doxygenfunction:: mailbox_next .. doxygenfunction:: mailbox_peek .. doxygenfunction:: mailbox_process_outer_list +.. doxygenfunction:: mailbox_process_outer_list_native .. doxygenfunction:: mailbox_remove_message .. doxygenfunction:: mailbox_reset .. doxygenfunction:: mailbox_send diff --git a/doc/src/distributed-erlang.md b/doc/src/distributed-erlang.md index 7d7c152365..05d2a9b87b 100644 --- a/doc/src/distributed-erlang.md +++ b/doc/src/distributed-erlang.md @@ -325,4 +325,11 @@ RPC (remote procedure call) from Erlang/OTP to AtomVM is also supported. Shell requires several OTP standard library modules. See [the example project](https://github.com/pguyot/atomvm_shell). +## Known Issues & Limitations + +- Sending to a remote process alias is not supported: a message sent from AtomVM to an alias + (a reference) of another node is silently dropped instead of being routed over distribution. + The other direction works: a message sent from a remote BEAM node to an alias of an AtomVM + process is delivered. + Please do not hesitate to file issues or pull requests for additional features. diff --git a/libs/estdlib/src/erlang.erl b/libs/estdlib/src/erlang.erl index 4fdb14225f..99872a94a0 100644 --- a/libs/estdlib/src/erlang.erl +++ b/libs/estdlib/src/erlang.erl @@ -177,6 +177,7 @@ tuple_size/1, tuple_to_list/1, alias/0, + alias/1, unalias/1 ]). @@ -221,7 +222,8 @@ -type send_destination() :: pid() | port() - | atom(). + | atom() + | reference(). % Current type until we make these references -type resource() :: binary(). @@ -1228,8 +1230,10 @@ spawn_monitor(Module, Function, Args) -> %%----------------------------------------------------------------------------- %% @param Function function to create a process from -%% @param Options additional options. -%% @returns pid of the new process +%% @param Options additional options, see `spawn_option()'. With `monitor' +%% or `{monitor, MonitorOpts}' the new process is also monitored, see +%% `monitor/3' for the monitor options. +%% @returns pid of the new process, or `{Pid, MonitorRef}' when monitoring %% @doc Create a new process. %% @end %%----------------------------------------------------------------------------- @@ -1242,8 +1246,10 @@ spawn_opt(_Name, _Options) -> %% @param Module module of the function to create a process from %% @param Function name of the function to create a process from %% @param Args arguments to pass to the function to create a process from -%% @param Options additional options. -%% @returns pid of the new process +%% @param Options additional options, see `spawn_option()'. With `monitor' +%% or `{monitor, MonitorOpts}' the new process is also monitored, see +%% `monitor/3' for the monitor options. +%% @returns pid of the new process, or `{Pid, MonitorRef}' when monitoring %% @doc Create a new process by calling exported Function from Module with Args. %% @end %%----------------------------------------------------------------------------- @@ -1283,10 +1289,11 @@ make_ref() -> erlang:nif_error(undefined). %%----------------------------------------------------------------------------- -%% @param Pid process to send the message to +%% @param Target process, registered name or alias to send the message to %% @param Message message to send %% @returns the sent message -%% @doc Send a message to a given process +%% @doc Send a message to a given process. A message sent to a reference +%% that is not an active alias is silently dropped. %% @end %%----------------------------------------------------------------------------- -spec send(Target :: send_destination(), Message :: Message) -> Message. @@ -1322,10 +1329,14 @@ monitor(_Type, _PidOrPort) -> %% makes the monitor also an alias on the calling process (see `alias/0'). %% `AliasMode' defines the behaviour of the alias: %% - explicit_unalias - the alias can be only removed with `unalias/1', -%% - demonitor - the alias is also removed when `demonitor/1' is called -%% on the monitor, -%% - reply_demonitor - the alias is also removed after a first message -%% is sent via it. +%% - demonitor - the alias is also removed when the monitor is removed, +%% by `demonitor/1' or by the delivery of a `DOWN' message, +%% - reply_demonitor - additionally, the alias is deactivated and the +%% monitor removed (as by `demonitor/1') when the +%% first message sent via the alias is delivered. +%% +%% Note: Unlike Erlang/OTP, the `{tag, Term}' option is not +%% supported and raises `unsupported'. %% @end %%----------------------------------------------------------------------------- -spec monitor @@ -2188,6 +2199,22 @@ tuple_to_list(_Tuple) -> alias() -> erlang:nif_error(undefined). +%%----------------------------------------------------------------------------- +%% @param Options alias options +%% @returns A reference aliasing the calling process. +%% @doc Creates an alias for the calling process, like `alias/0'. +%% With `explicit_unalias' (the default, so `alias([])' is `alias/0') +%% the alias stays active until `unalias/1'; with `reply' it is +%% deactivated when the first message sent via the alias is delivered. +%% +%% Note: Unlike Erlang/OTP, the `priority' option (OTP 28) is +%% not supported and raises `unsupported'. +%% @end +%%----------------------------------------------------------------------------- +-spec alias(Options) -> Alias when Options :: [explicit_unalias | reply], Alias :: reference(). +alias(_Options) -> + erlang:nif_error(undefined). + %%----------------------------------------------------------------------------- %% @param Alias the alias to be removed. %% @returns `true' if alias was removed, `false' if it was not found diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 32ce4f46ad..5b39f16e5e 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -53,6 +53,11 @@ #define DEFAULT_STACK_SIZE 8 #define BYTES_PER_TERM (TERM_BITS / 8) +// active_alias_count saturates at this value instead of wrapping to 0. A wrap would make +// context_find_alias skip the list walk and silently drop every alias of the process. A +// saturated count is only reset to 0 when the last monitor of the process goes away. +#define ACTIVE_ALIAS_COUNT_SATURATED 0xFF + static struct Monitor *context_monitors_handle_terminate(Context *ctx); static void context_distribution_handle_terminate(Context *ctx); static void destroy_extended_registers(Context *ctx, unsigned int live); @@ -83,6 +88,7 @@ Context *context_new(GlobalContext *glb) ctx->heap_growth_strategy = BoundedFreeHeapGrowth; ctx->has_min_heap_size = 0; ctx->has_max_heap_size = 0; + ctx->active_alias_count = 0; mailbox_init(&ctx->mailbox); @@ -139,10 +145,9 @@ void context_destroy(Context *ctx) // Hold and release the spin lock for timers and cancel any timer scheduler_cancel_timeout(ctx); - // Remove the process from the scheduler queue. A process terminated by the scheduler was - // already dequeued (and its queue item was reset, making this a no-op), but a process that - // is destroyed before it was ever scheduled -- e.g. on a spawn_opt option error -- is still - // on the waiting queue, and destroying it without dequeuing would leave a dangling entry. + // A process terminated by the scheduler was already dequeued and its queue item reset, so this + // is a no-op. A process destroyed before it was ever scheduled (e.g. a spawn_opt option error) + // is still on the waiting queue, and dequeuing it here avoids leaving a dangling entry. SMP_SPINLOCK_LOCK(&ctx->global->processes_spinlock); list_remove(&ctx->processes_list_head); SMP_SPINLOCK_UNLOCK(&ctx->global->processes_spinlock); @@ -163,7 +168,7 @@ void context_destroy(Context *ctx) // Process any link/unlink/monitor/demonitor signal that arrived recently // Also process ProcessInfoRequestSignal so caller isn't trapped waiting - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); + MailboxMessage *signal_message = mailbox_process_outer_list_native(&ctx->mailbox); while (signal_message) { switch (signal_message->type) { case ProcessInfoRequestSignal: { @@ -226,6 +231,7 @@ void context_destroy(Context *ctx) case FlushInfoMonitorSignal: case LinkExitSignal: // target will not be found when processing this link case MonitorDownSignal: // likewise + case AliasMessageSignal: // process is terminating; drop the alias message case CodeServerResumeSignal: break; case NormalMessage: { @@ -443,6 +449,14 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); + + // {alias, demonitor} / {alias, reply_demonitor}: the alias is deactivated when the + // monitor is removed, including the automatic removal at 'DOWN' delivery. + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(ctx, alias); + } + // Enqueue the term as a message. mailbox_send(ctx, signal->signal_term); break; @@ -463,6 +477,11 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal mailbox_send(ctx, signal->signal_term); END_WITH_STACK_HEAP(temp_heap, ctx->global); + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(ctx, alias); + } + free(monitoring_monitor); break; } @@ -472,6 +491,38 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal // (flush option removes messages that were already sent) } +term context_process_alias_message_signal(Context *ctx, struct TermSignal *signal) +{ + term ref = term_get_tuple_element(signal->signal_term, 0); + uint64_t ref_ticks = term_to_ref_ticks(ref); + term message = term_get_tuple_element(signal->signal_term, 1); + + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (IS_NULL_PTR(alias)) { + // Alias is not (or no longer) active: drop the message, matching OTP. + return term_invalid_term(); + } + + if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { + // Capture the monitored pid before context_demonitor removes the local monitoring entry. + bool is_monitoring = false; + term monitor_pid = context_get_monitor_pid(ctx, ref_ticks, &is_monitoring); + context_demonitor(ctx, ref_ticks); + if (!term_is_invalid_term(monitor_pid) && is_monitoring) { + // Takes the processes_table read lock: callers of mailbox_process_outer_list + // must not already hold it. + int32_t monitored_process_id = term_to_local_process_id(monitor_pid); + Context *target = globalcontext_get_process_lock(ctx->global, monitored_process_id); + if (target) { + mailbox_send_ref_signal(target, DemonitorSignal, ref_ticks); + globalcontext_get_process_unlock(ctx->global, target); + } + } + } + + return message; +} + void context_process_code_server_resume_signal(Context *ctx) { #ifndef AVM_NO_JIT @@ -795,14 +846,16 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - int required_terms = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5); + bool is_monitored_alias = monitored_monitor->ref_data.process_id != INVALID_PROCESS_ID; + int ref_size = is_monitored_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; + int required_terms = ref_size + TUPLE_SIZE(5); if (UNLIKELY(memory_ensure_free(ctx, required_terms) != MEMORY_GC_OK)) { // TODO: handle out of memory here fprintf(stderr, "Cannot handle out of memory.\n"); globalcontext_get_process_unlock(glb, target); AVM_ABORT(); } - // Prepare the message on ctx's heap which will be freed afterwards. + // Prepare the message on ctx's heap, which is freed afterwards. term ref = term_from_ref_data(&monitored_monitor->ref_data, &ctx->heap); term port_or_process = term_pid_or_port_from_context(ctx); @@ -822,6 +875,9 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); + if (LIKELY(ctx->active_alias_count != ACTIVE_ALIAS_COUNT_SATURATED)) { + ctx->active_alias_count--; + } free(alias); break; } @@ -864,7 +920,7 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring) { struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { @@ -881,7 +937,7 @@ struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitor return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -895,7 +951,7 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t return &monitor->monitor; } -struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type) +struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type) { struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); if (IS_NULL_PTR(monitor)) { @@ -922,6 +978,35 @@ struct Monitor *monitor_resource_monitor_new(void *resource, uint64_t ref_ticks) return &monitor->monitor; } +void monitor_destroy(struct Monitor *monitor) +{ + if (monitor == NULL) { + return; + } + + switch (monitor->monitor_type) { + case CONTEXT_MONITOR_LINK_LOCAL: + free(CONTAINER_OF(monitor, struct LinkLocalMonitor, monitor)); + break; + case CONTEXT_MONITOR_LINK_REMOTE: + free(CONTAINER_OF(monitor, struct LinkRemoteMonitor, monitor)); + break; + case CONTEXT_MONITOR_MONITORING_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL: + free(CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor)); + break; + case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: + free(CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor)); + break; + case CONTEXT_MONITOR_ALIAS: + free(CONTAINER_OF(monitor, struct MonitorAlias, monitor)); + break; + case CONTEXT_MONITOR_RESOURCE: + free(CONTAINER_OF(monitor, struct ResourceContextMonitor, monitor)); + break; + } +} + bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) { struct ListHead *item; @@ -964,7 +1049,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { - free(new_monitor); + free(new_alias_monitor); return false; } break; @@ -994,6 +1079,11 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) } } list_append(&ctx->monitors_head, &new_monitor->monitor_list_head); + if (new_monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { + if (LIKELY(ctx->active_alias_count < ACTIVE_ALIAS_COUNT_SATURATED)) { + ctx->active_alias_count++; + } + } return true; } @@ -1104,7 +1194,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) { struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { - context_unalias(alias); + context_unalias(ctx, alias); } struct ListHead *item; @@ -1149,6 +1239,11 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) { + // The vast majority of processes never create an alias: skip the monitor list walk + // entirely for them (this runs for every 'DOWN' and demonitor, not only for aliases). + if (LIKELY(ctx->active_alias_count == 0)) { + return NULL; + } struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); @@ -1163,11 +1258,18 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) return NULL; } -void context_unalias(struct MonitorAlias *alias) +void context_unalias(Context *ctx, struct MonitorAlias *alias) { TERM_DEBUG_ASSERT(alias != NULL); struct Monitor *monitor = &alias->monitor; list_remove(&monitor->monitor_list_head); + TERM_DEBUG_ASSERT(ctx->active_alias_count > 0); + if (LIKELY(ctx->active_alias_count != ACTIVE_ALIAS_COUNT_SATURATED)) { + ctx->active_alias_count--; + } else if (list_is_empty(&ctx->monitors_head)) { + // No monitors left means no active alias: a saturated count can recover. + ctx->active_alias_count = 0; + } free(alias); } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index cb9f7e0b1a..a4ac9ba7eb 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -138,6 +138,7 @@ struct Context unsigned int leader : 1; unsigned int has_min_heap_size : 1; unsigned int has_max_heap_size : 1; + unsigned int active_alias_count : 8; bool trap_exit : 1; #ifndef AVM_NO_EMU @@ -180,12 +181,12 @@ enum ContextMonitorType CONTEXT_MONITOR_ALIAS, }; -enum ContextMonitorAliasType +typedef enum { ContextMonitorAliasExplicitUnalias, ContextMonitorAliasDemonitor, ContextMonitorAliasReplyDemonitor, -}; +} context_monitor_alias_type_t; #define UNLINK_ID_LINK_ACTIVE 0x0 @@ -224,7 +225,7 @@ struct MonitorAlias { struct Monitor monitor; RefData ref_data; - enum ContextMonitorAliasType alias_type; + context_monitor_alias_type_t alias_type; }; // The other half is called ResourceMonitor and is a linked list of resources @@ -494,6 +495,21 @@ bool context_process_link_exit_signal(Context *ctx, struct TermSignal *signal); */ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal); +/** + * @brief Process an alias message signal. + * + * @details The signal term is a 2-tuple {Ref, Message}. If Ref is an active alias of this process the + * Message is returned for delivery as a normal message; otherwise an invalid term is returned and the + * message is dropped. For a reply_demonitor alias the alias is also deactivated and the monitor + * removed. Runs in the owner's own context (no race). The caller delivers the returned message in send + * order so an alias send is not reordered against a plain send. + * + * @param ctx the context being executed + * @param signal the signal with the {Ref, Message} tuple + * @return the message to deliver, or an invalid term to drop it + */ +term context_process_alias_message_signal(Context *ctx, struct TermSignal *signal); + /** * @brief Resume execution after module has been loaded * @@ -530,9 +546,16 @@ struct Monitor *monitor_link_new(term link_pid); * @param is_monitoring if ctx is the monitoring process * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, RefData *ref_data, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring); -struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasType alias_type); +/** + * @brief Create a process alias. + * + * @param ref_data reference of the alias + * @param alias_type when the alias is deactivated, see the erlang:monitor/3 alias option + * @return the allocated monitor or NULL if allocation failed + */ +struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type); /** * @brief Create a monitor on a process by registered name. @@ -542,7 +565,7 @@ struct Monitor *monitor_alias_new(RefData *ref_data, enum ContextMonitorAliasTyp * @param ref_data reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, RefData *ref_data); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data); /** * @brief Create a resource monitor. @@ -553,6 +576,18 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t */ struct Monitor *monitor_resource_monitor_new(void *resource, uint64_t ref_ticks); +/** + * @brief Destroy a monitor that was not installed yet. + * @details Frees the container struct the monitor is embedded in, recovering + * it from its monitor type with CONTAINER_OF instead of relying on the monitor + * being the first member. It doesn't remove the monitor from any list, so it + * must only be used on monitors that were never passed to context_add_monitor, + * e.g. on error paths. + * + * @param monitor the monitor to free, or NULL in which case nothing is done + */ +void monitor_destroy(struct Monitor *monitor); + /** * @brief Half-unlink process to another process * @details If process is found, an unlink id is generated and the link is @@ -611,9 +646,10 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks); /** * @brief Remove an alias of a process * + * @param ctx the context owning the alias * @param alias The alias to remove, can be obtained using context_find_alias */ -void context_unalias(struct MonitorAlias *alias); +void context_unalias(Context *ctx, struct MonitorAlias *alias); /** * @brief Get target of a monitor. diff --git a/src/libAtomVM/defaultatoms.def b/src/libAtomVM/defaultatoms.def index 229271306f..8911bc90a0 100644 --- a/src/libAtomVM/defaultatoms.def +++ b/src/libAtomVM/defaultatoms.def @@ -227,4 +227,6 @@ X(ALIAS_ATOM, "\x5", "alias") X(DEMONITOR_ATOM, "\x9", "demonitor") X(EXPLICIT_UNALIAS_ATOM, "\x10", "explicit_unalias") X(REPLY_DEMONITOR_ATOM, "\xF", "reply_demonitor") +X(REPLY_ATOM, "\x5", "reply") X(TAG_ATOM, "\x3", "tag") +X(PRIORITY_ATOM, "\x8", "priority") diff --git a/src/libAtomVM/dist_nifs.c b/src/libAtomVM/dist_nifs.c index dff76b6b5a..1b6d93f24a 100644 --- a/src/libAtomVM/dist_nifs.c +++ b/src/libAtomVM/dist_nifs.c @@ -560,6 +560,29 @@ static term nif_erlang_dist_ctrl_put_data(Context *ctx, int argc, term argv[]) globalcontext_send_message(ctx->global, target_process_id, payload); break; } + case OPERATION_ALIAS_SEND: + case OPERATION_ALIAS_SEND_TT: { + // {DOP_ALIAS_SEND, FromPid, Alias} or {DOP_ALIAS_SEND_TT, FromPid, Alias, TraceToken}, + // followed by the message payload. The trace token is ignored. + size_t expected_arity = (term_to_int(operation) == OPERATION_ALIAS_SEND) ? 3 : 4; + if (UNLIKELY(arity != expected_arity)) { + RAISE_ERROR(BADARG_ATOM); + } + term roots[3]; + roots[0] = argv[0]; // dist handle + roots[1] = argv[1]; + roots[2] = control; + term payload = external_term_from_binary_with_roots(ctx, 1, 1 + bytes_read, &bytes_read, 3, roots); + control = roots[2]; + term target = term_get_tuple_element(control, 2); + if (LIKELY(term_is_process_reference(target))) { + int32_t target_process_id = term_process_ref_to_process_id(target); + globalcontext_send_message_to_alias(ctx->global, target_process_id, target, payload); + } + // A ref minted by a previous incarnation of this node is not an active alias here. + // Drop the message instead of crashing the dist connection with badarg. + break; + } case OPERATION_SPAWN_REQUEST: { if (UNLIKELY(arity != 6)) { RAISE_ERROR(BADARG_ATOM); diff --git a/src/libAtomVM/external_term.c b/src/libAtomVM/external_term.c index 448a60d06b..f306e2e810 100644 --- a/src/libAtomVM/external_term.c +++ b/src/libAtomVM/external_term.c @@ -1013,6 +1013,9 @@ static term parse_external_terms(const uint8_t *external_term_buf, size_t *eterm } else if (len == 3 && node == this_node && creation == this_creation) { uint64_t ticks = ((uint64_t) data[0]) << 32 | data[1]; uint32_t process_id = data[2]; + if (UNLIKELY(process_id == INVALID_PROCESS_ID || process_id > TERM_MAX_LOCAL_PROCESS_ID)) { + return term_invalid_term(); + } return term_make_process_reference(process_id, ticks, heap); } else if (len == 4 && node == this_node && creation == this_creation) { // This is a resource diff --git a/src/libAtomVM/globalcontext.c b/src/libAtomVM/globalcontext.c index d00213284c..cc85e4620b 100644 --- a/src/libAtomVM/globalcontext.c +++ b/src/libAtomVM/globalcontext.c @@ -414,6 +414,20 @@ void globalcontext_send_message(GlobalContext *glb, int32_t process_id, term t) } } +void globalcontext_send_message_to_alias(GlobalContext *glb, int32_t process_id, term ref, term message) +{ + Context *p = globalcontext_get_process_lock(glb, process_id); + if (p) { + BEGIN_WITH_STACK_HEAP(TUPLE_SIZE(2), temp_heap) + term tuple = term_alloc_tuple(2, &temp_heap); + term_put_tuple_element(tuple, 0, ref); + term_put_tuple_element(tuple, 1, message); + mailbox_send_term_signal(p, AliasMessageSignal, tuple); + END_WITH_STACK_HEAP(temp_heap, glb) + globalcontext_get_process_unlock(glb, p); + } +} + void globalcontext_send_message_nolock(GlobalContext *glb, int32_t process_id, term t) { Context *p = globalcontext_get_process_nolock(glb, process_id); diff --git a/src/libAtomVM/globalcontext.h b/src/libAtomVM/globalcontext.h index 2876aaab80..b36465e28c 100644 --- a/src/libAtomVM/globalcontext.h +++ b/src/libAtomVM/globalcontext.h @@ -262,6 +262,20 @@ bool globalcontext_process_exists(GlobalContext *glb, int32_t process_id); */ void globalcontext_send_message(GlobalContext *glb, int32_t process_id, term t); +/** + * @brief Send a message to a process alias (process reference). + * + * @details Posts an AliasMessageSignal carrying {Ref, Message} to the owner process. The owner + * validates the alias against its own monitors when it drains signals and either delivers Message + * as a normal message or drops it. This avoids touching the owner's monitor list from the sender. + * + * @param glb the global context (that owns the process table). + * @param process_id the local process id of the alias owner. + * @param ref the process reference (alias) used as the send target. + * @param message the message to send. + */ +void globalcontext_send_message_to_alias(GlobalContext *glb, int32_t process_id, term ref, term message); + /** * @brief Send a message to a process from another process. * There should be a lock on the process table. This variant can be used by diff --git a/src/libAtomVM/jit.c b/src/libAtomVM/jit.c index f25e6aeef7..1bc3986765 100644 --- a/src/libAtomVM/jit.c +++ b/src/libAtomVM/jit.c @@ -850,33 +850,20 @@ static bool jit_send(Context *ctx, JITState *jit_state) set_error(ctx, jit_state, 0, BADARG_ATOM); return false; } - int local_process_id; - if (term_is_local_pid_or_port(recipient_term)) { - local_process_id = term_to_local_process_id(recipient_term); - } else { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; - } + int local_process_id = term_to_local_process_id(recipient_term); globalcontext_send_message(ctx->global, local_process_id, ctx->x[1]); ctx->x[0] = ctx->x[1]; + } else if (UNLIKELY(!term_is_reference(recipient_term))) { + set_error(ctx, jit_state, 0, BADARG_ATOM); + return false; } else if (term_is_process_reference(recipient_term)) { int32_t process_id = term_process_ref_to_process_id(recipient_term); - int64_t ref_ticks = term_to_ref_ticks(recipient_term); - Context *p = globalcontext_get_process_lock(ctx->global, process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (!IS_NULL_PTR(alias)) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, ctx->x[1]); - } - globalcontext_get_process_unlock(ctx->global, p); - } + globalcontext_send_message_to_alias(ctx->global, process_id, recipient_term, ctx->x[1]); + ctx->x[0] = ctx->x[1]; + } else { + // Drop the send but still return the message in x0, as OTP does for a non-active-alias + // reference. Outbound distributed aliases are unsupported. ctx->x[0] = ctx->x[1]; - } else if (!term_is_reference(recipient_term)) { - set_error(ctx, jit_state, 0, BADARG_ATOM); - return false; } return true; @@ -903,7 +890,7 @@ static term *jit_extended_register_ptr(Context *ctx, unsigned int index) static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) { TRACE("jit_process_signal_messages\n"); - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); + MailboxMessage *signal_message = mailbox_process_outer_list(ctx); bool handle_error = false; bool reprocess_outer = false; while (signal_message) { @@ -1030,6 +1017,7 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) #endif break; } + case AliasMessageSignal: case NormalMessage: { UNREACHABLE(); } @@ -1039,7 +1027,7 @@ static Context *jit_process_signal_messages(Context *ctx, JITState *jit_state) signal_message = next; if (UNLIKELY(reprocess_outer && signal_message == NULL)) { reprocess_outer = false; - signal_message = mailbox_process_outer_list(&ctx->mailbox); + signal_message = mailbox_process_outer_list(ctx); } } if (context_get_flags(ctx, Killed)) { diff --git a/src/libAtomVM/mailbox.c b/src/libAtomVM/mailbox.c index fe256df7ed..dea77740b7 100644 --- a/src/libAtomVM/mailbox.c +++ b/src/libAtomVM/mailbox.c @@ -22,6 +22,7 @@ #include +#include "context.h" #include "memory.h" #include "scheduler.h" #include "synclist.h" @@ -101,6 +102,7 @@ void mailbox_message_dispose(MailboxMessage *m, Heap *heap) case SetGroupLeaderSignal: case LinkExitSignal: case MonitorDownSignal: + case AliasMessageSignal: case UnlinkRemoteIDSignal: case UnlinkRemoteIDAckSignal: { struct TermSignal *term_signal = CONTAINER_OF(m, struct TermSignal, base); @@ -337,6 +339,7 @@ void mailbox_send_monitor_signal(Context *c, enum MessageType type, struct Monit { struct MonitorPointerSignal *monitor_signal = malloc(sizeof(struct MonitorPointerSignal)); if (IS_NULL_PTR(monitor_signal)) { + // FIXME this function returns void, so the caller is not told the allocation failed fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); return; } @@ -364,9 +367,10 @@ void mailbox_reset(Mailbox *mbox) mbox->receive_pointer_prev = NULL; } -MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) +// CAS-empty the outer list and return its raw head. The outer list is LIFO, so the head is the +// newest message and each message is older than its predecessor. +static inline MailboxMessage *detach_outer_list(Mailbox *mbox) { - // Empty outer list using CAS MailboxMessage *current = mbox->outer_first; #if !defined(AVM_NO_SMP) || defined(AVM_TASK_DRIVER_ENABLED) while (!ATOMIC_COMPARE_EXCHANGE_WEAK_PTR(&mbox->outer_first, ¤t, NULL)) { @@ -374,6 +378,42 @@ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) #else mbox->outer_first = NULL; #endif + return current; +} + +// Append a received-order run of normal messages, from first (oldest) to last (newest), at the end +// of the inner list, restoring the receive pointer when the inner list had been fully consumed. +// first and last are NULL together when no normal message was collected, making this a no-op. +static inline void append_normal_messages(Mailbox *mbox, MailboxMessage *first, MailboxMessage *last) +{ + if (last == NULL) { + return; + } + + // With no receive_pointer, it becomes the new list head. + if (mbox->receive_pointer == NULL) { + mbox->receive_pointer = first; + // If we had a prev, set the prev's next to the new current. + if (mbox->receive_pointer_prev) { + mbox->receive_pointer_prev->next = first; + } else if (mbox->inner_first == NULL) { + // If we had no first, this is the first message. + mbox->inner_first = first; + } + } + + // Append the new items at the end of the inner list. mbox->inner_last may be + // mbox->receive_pointer_prev, which is then updated a second time here. + if (mbox->inner_last) { + mbox->inner_last->next = first; + } + mbox->inner_last = last; +} + +MailboxMessage *mailbox_process_outer_list_native(Mailbox *mbox) +{ + MailboxMessage *current = detach_outer_list(mbox); + // Reverse the list MailboxMessage *previous_normal = NULL; MailboxMessage *previous_signal = NULL; @@ -393,33 +433,117 @@ MailboxMessage *mailbox_process_outer_list(Mailbox *mbox) } current = next; } - // If we did enqueue some normal messages, lastNormal is the first - // one in outer list (last received one) - if (last_normal) { - // previousNormal is new list head - // If we had no receive_pointer, it should be this list head - if (mbox->receive_pointer == NULL) { - mbox->receive_pointer = previous_normal; - // If we had a prev, set the prev's next to the new current. - if (mbox->receive_pointer_prev) { - mbox->receive_pointer_prev->next = previous_normal; - } else if (mbox->inner_first == NULL) { - // If we had no first, this is the first message. - mbox->inner_first = previous_normal; + + append_normal_messages(mbox, previous_normal, last_normal); + return previous_signal; +} + +MailboxMessage *mailbox_process_outer_list(Context *ctx) +{ + Mailbox *mbox = &ctx->mailbox; + MailboxMessage *current = detach_outer_list(mbox); + + MailboxMessage *normal_first = NULL; + MailboxMessage *normal_last = NULL; + MailboxMessage *signal_first = NULL; + + if (ctx->active_alias_count == 0) { + // Fast path (the common case): no active alias, so no alias message can be delivered. Same + // single-pass split as mailbox_process_outer_list_native, except a stale AliasMessageSignal + // (its alias is inactive) is freed now so it does not reach the signal loop, which would + // treat it as unreachable. + while (current) { + MailboxMessage *next = current->next; + if (current->type == AliasMessageSignal) { + mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); + } else if (current->type == NormalMessage) { + if (normal_last == NULL) { + normal_last = current; + } + current->next = normal_first; + normal_first = current; + } else { + current->next = signal_first; + signal_first = current; } + current = next; + } + } else { + // At least one active alias: alias side effects can deactivate the alias (e.g. + // reply_demonitor), so they must run in received order. Of several same-batch sends to one + // alias, only the first is delivered, like OTP. Reverse the LIFO list into received order. + MailboxMessage *received = NULL; + while (current) { + MailboxMessage *next = current->next; + current->next = received; + received = current; + current = next; } - // Update last and previous last's next. - // Append these new items at the end of inner list. - if (mbox->inner_last) { - // This may be mbox->receive_pointer_prev which we - // are updating a second time here. - mbox->inner_last->next = previous_normal; + // Walk oldest to newest, appending so both sublists keep received order. + MailboxMessage *signal_last = NULL; + current = received; + while (current) { + MailboxMessage *next = current->next; + if (current->type == NormalMessage) { + current->next = NULL; + if (normal_last == NULL) { + normal_first = current; + } else { + normal_last->next = current; + } + normal_last = current; + } else if (current->type == AliasMessageSignal) { + // Validate the alias (in the owner's own context) and convert to a normal message. + term message = context_process_alias_message_signal(ctx, CONTAINER_OF(current, struct TermSignal, base)); + if (!term_is_invalid_term(message)) { + // Re-type in place: struct TermSignal and struct Message share a layout + // (static-asserted above) and the message term already lives in this signal's + // storage, so nothing is copied. The conversion cannot fail on OOM, which + // matters because reply_demonitor's side effects already ran above. + Message *converted = CONTAINER_OF(current, Message, base); + converted->base.type = NormalMessage; + converted->message = message; + converted->base.next = NULL; + if (normal_last == NULL) { + normal_first = &converted->base; + } else { + normal_last->next = &converted->base; + } + normal_last = &converted->base; + } else { + // Inactive alias: never delivered, so nothing references the term. Free it + // now (sweeping refc binaries) instead of leaving it on the heap until GC. + mailbox_message_dispose_unsent(CONTAINER_OF(current, Message, base), ctx->global, false); + } + } else { + // A 'DOWN' auto-removing a {alias, _} monitor also deactivates its alias. Do that + // here, in received order, so a later same-batch alias send is dropped like OTP + // (alias messages are converted during this split, before the signal loop runs the + // 'DOWN'). The signal loop's own deactivation in context_process_monitor_down_signal + // is then an idempotent no-op. + if (current->type == MonitorDownSignal) { + struct TermSignal *down_signal = CONTAINER_OF(current, struct TermSignal, base); + uint64_t ref_ticks = term_to_ref_ticks(term_get_tuple_element(down_signal->signal_term, 1)); + struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); + if (alias != NULL && alias->alias_type != ContextMonitorAliasExplicitUnalias) { + context_unalias(ctx, alias); + } + } + current->next = NULL; + if (signal_last == NULL) { + signal_first = current; + } else { + signal_last->next = current; + } + signal_last = current; + } + current = next; } - mbox->inner_last = last_normal; } - return previous_signal; + append_normal_messages(mbox, normal_first, normal_last); + return signal_first; } void mailbox_next(Mailbox *mbox) @@ -498,7 +622,7 @@ Message *mailbox_first(Mailbox *mbox) void mailbox_crashdump(Context *ctx) { // Signal messages are now in reverse order but the process crashed anyway - ctx->mailbox.outer_first = mailbox_process_outer_list(&ctx->mailbox); + ctx->mailbox.outer_first = mailbox_process_outer_list_native(&ctx->mailbox); MailboxMessage *msg = ctx->mailbox.inner_first; while (msg) { Message *data_message = CONTAINER_OF(msg, Message, base); diff --git a/src/libAtomVM/mailbox.h b/src/libAtomVM/mailbox.h index 50e7dd1c2b..37d9dd59d4 100644 --- a/src/libAtomVM/mailbox.h +++ b/src/libAtomVM/mailbox.h @@ -99,6 +99,7 @@ enum MessageType DemonitorSignal, MonitorDownSignal, CodeServerResumeSignal, + AliasMessageSignal, }; struct MailboxMessage @@ -211,13 +212,27 @@ size_t mailbox_len(Mailbox *mbox); size_t mailbox_size(Mailbox *mbox); /** - * @brief Process the outer list of messages. + * @brief Process the outer list of messages, ignoring aliases. * - * @details To be called from the process only + * @details Alias-blind variant of mailbox_process_outer_list, for callers that never own an + * active alias: native (port) processes, context teardown and crashdump. To be called from the + * process only. * @param mbox the mailbox to work with * @return the signal messages in received order. */ -MailboxMessage *mailbox_process_outer_list(Mailbox *mbox); +MailboxMessage *mailbox_process_outer_list_native(Mailbox *mbox); + +/** + * @brief Process the outer list of messages, delivering alias messages in send order. + * + * @details AliasMessageSignals are validated in ctx's own monitor list and, when the alias is + * active, converted to normal messages in place so they keep send order relative to plain + * messages from the same sender. To be called from the process only, while not holding the + * processes table lock (the reply_demonitor path takes it). + * @param ctx the context whose mailbox is processed + * @return the remaining (non-alias) signal messages in received order. + */ +MailboxMessage *mailbox_process_outer_list(Context *ctx); /** * @brief Sends a message to a certain mailbox. diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index 004d46ef97..de05885723 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1340,7 +1340,7 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) { // msg is not in the port's heap NativeHandlerResult result = NativeContinue; - if (UNLIKELY(memory_ensure_free_opt(ctx, TUPLE_SIZE(3) + TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free_opt(ctx, MAX(TUPLE_SIZE(3), 2 * TUPLE_SIZE(2)), MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { fprintf(stderr, "Unable to allocate sufficient memory for console driver.\n"); AVM_ABORT(); } @@ -1358,7 +1358,6 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) term pid = term_get_tuple_element(msg, 1); term ref = term_get_tuple_element(msg, 2); term req = term_get_tuple_element(msg, 3); - uint64_t ref_ticks = term_to_ref_ticks(ref); if (is_tagged_tuple(req, PUT_CHARS_ATOM, 3)) { term chars = term_get_tuple_element(req, 2); @@ -1368,11 +1367,9 @@ static NativeHandlerResult process_console_message(Context *ctx, term msg) printf("%s", str); free(str); - term refcopy = term_from_ref_ticks(ref_ticks, &ctx->heap); - term reply = term_alloc_tuple(3, &ctx->heap); term_put_tuple_element(reply, 0, IO_REPLY_ATOM); - term_put_tuple_element(reply, 1, refcopy); + term_put_tuple_element(reply, 1, ref); term_put_tuple_element(reply, 2, OK_ATOM); port_send_message(ctx->global, pid, reply); @@ -1432,7 +1429,7 @@ static NativeHandlerResult process_console_mailbox(Context *ctx) return result; } -static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, enum ContextMonitorAliasType *alias_type) +static term parse_monitor_opts(Context *ctx, term monitor_opts, bool *is_alias, context_monitor_alias_type_t *alias_type) { *is_alias = false; while (term_is_nonempty_list(monitor_opts)) { @@ -1547,41 +1544,49 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } RefData ref_data; bool is_spawn_monitor = false; + bool is_alias = false; + context_monitor_alias_type_t alias_type; term new_pid = term_from_local_process_id(new_ctx->process_id); + // Do every fallible step before publishing any side effect: destroying a never-published + // new_ctx would send the caller a spurious {'EXIT', Pid, normal} for a spawn that raised. + struct Monitor *new_link = NULL; + struct Monitor *self_link = NULL; + struct Monitor *alias_monitor = NULL; + struct Monitor *new_monitor = NULL; + struct Monitor *self_monitor = NULL; + if (link_term == TRUE_ATOM) { - // We can call context_add_monitor directly on new process because it's not started yet - struct Monitor *new_link = monitor_link_new(term_from_local_process_id(ctx->process_id)); + new_link = monitor_link_new(term_from_local_process_id(ctx->process_id)); if (IS_NULL_PTR(new_link)) { context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_link = monitor_link_new(new_pid); + self_link = monitor_link_new(new_pid); if (IS_NULL_PTR(self_link)) { - free(new_link); + monitor_destroy(new_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - context_add_monitor(new_ctx, new_link); - context_add_monitor(ctx, self_link); } if (monitor_term == TRUE_ATOM) { monitor_term = term_nil(); } if (term_is_list(monitor_term)) { is_spawn_monitor = true; - bool is_alias; - enum ContextMonitorAliasType alias_type; if (UNLIKELY(term_is_invalid_term(parse_monitor_opts(ctx, monitor_term, &is_alias, &alias_type)))) { + monitor_destroy(new_link); + monitor_destroy(self_link); context_destroy(new_ctx); return term_invalid_term(); } - struct Monitor *alias_monitor = NULL; if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; alias_monitor = monitor_alias_new(&ref_data, alias_type); if (IS_NULL_PTR(alias_monitor)) { + monitor_destroy(new_link); + monitor_destroy(self_link); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -1589,21 +1594,52 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; } - struct Monitor *new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); + new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); if (IS_NULL_PTR(new_monitor)) { + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); context_destroy(new_ctx); - free(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - struct Monitor *self_monitor = monitor_new(new_pid, &ref_data, true); + self_monitor = monitor_new(new_pid, &ref_data, true); if (IS_NULL_PTR(self_monitor)) { - free(alias_monitor); - free(new_monitor); + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); + monitor_destroy(new_monitor); + context_destroy(new_ctx); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Reserve before publishing (see above). GC here is safe: new_pid and ref_data are immediates. + int ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; + int res_size = ref_size + TUPLE_SIZE(2); + if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + monitor_destroy(new_link); + monitor_destroy(self_link); + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); + monitor_destroy(new_monitor); context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + } else if (UNLIKELY(!term_is_invalid_term(monitor_term))) { + // {monitor, BadTerm} where BadTerm is neither a list nor 'true': raise badarg like OTP + // instead of spawning an unmonitored process. + monitor_destroy(new_link); + monitor_destroy(self_link); + context_destroy(new_ctx); + RAISE_ERROR(BADARG_ATOM); + } - // We can call context_add_monitor directly on new process because it's not started yet + // Nothing can fail from here on. Publish in order so the entries keep their relative position + // in the monitor lists. context_add_monitor on new_ctx is safe because it is not started yet. + if (new_link != NULL) { + context_add_monitor(new_ctx, new_link); + context_add_monitor(ctx, self_link); + } + if (is_spawn_monitor) { context_add_monitor(new_ctx, new_monitor); context_add_monitor(ctx, self_monitor); if (is_alias) { @@ -1612,12 +1648,6 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } if (is_spawn_monitor) { - int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(2); - if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - context_destroy(new_ctx); - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - scheduler_init_ready(new_ctx); term ref = term_from_ref_data(&ref_data, &ctx->heap); @@ -1819,20 +1849,6 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message(glb, local_process_id, argv[1]); - } else if (term_is_process_reference(target)) { - int32_t process_id = term_process_ref_to_process_id(target); - int64_t ref_ticks = term_to_ref_ticks(target); - Context *p = globalcontext_get_process_lock(glb, process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (alias != NULL) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, argv[1]); - } - globalcontext_get_process_unlock(glb, p); - } } else if (term_is_atom(target)) { // We need to hold a lock on the processes_table until the message is sent to avoid a race condition, // otherwise the receiving process could be killed at any point between checking it is registered, @@ -1857,9 +1873,16 @@ static term nif_erlang_send_2(Context *ctx, int argc, term argv[]) globalcontext_send_message_nolock(glb, local_process_id, argv[1]); synclist_unlock(&glb->processes_table); - } else if (!term_is_reference(target)) { + } else if (UNLIKELY(!term_is_reference(target))) { RAISE_ERROR(BADARG_ATOM); + } else if (term_is_process_reference(target)) { + int32_t process_id = term_process_ref_to_process_id(target); + globalcontext_send_message_to_alias(glb, process_id, target, argv[1]); } + // else: a non-local-process reference is silently dropped, as OTP drops a send to a + // non-active-alias reference. + // TODO: route sends to external references over distribution. Outbound distributed aliases are + // unsupported, so the message is currently lost. return argv[1]; } @@ -5014,14 +5037,13 @@ static term nif_erlang_memory(Context *ctx, int argc, term argv[]) static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) { - term object_type = argv[0]; term target_proc = argv[1]; term options = argc == 3 ? argv[2] : term_nil(); term target_pid; size_t target_proc_size = 0; bool is_alias; - enum ContextMonitorAliasType alias_type; + context_monitor_alias_type_t alias_type; if (object_type != PROCESS_ATOM && object_type != PORT_ATOM) { RAISE_ERROR(BADARG_ATOM); @@ -5039,35 +5061,47 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } Context *target; - int32_t local_process_id; - // gcc < 14 is not smart enough to find out local_process_id is not used initialized below -#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 14 - local_process_id = 0; -#endif + // Some compilers cannot tell that local_process_id is always set before it is used below + int32_t local_process_id = INVALID_PROCESS_ID; if (UNLIKELY(target_pid == UNDEFINED_ATOM)) { target = NULL; } else { local_process_id = term_to_local_process_id(target_pid); - // Monitoring self is possible but no monitor is actually created + // Monitoring self installs nothing, like OTP: no monitor and (with {alias, _}) no alias, so + // sends to the returned ref are dropped and unalias/1, demonitor(Ref, [info]) return false. if (UNLIKELY(local_process_id == ctx->process_id)) { if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_SHORT_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); - return ref; + return term_from_ref_ticks(ref_ticks, &ctx->heap); } target = globalcontext_get_process_lock(ctx->global, local_process_id); } + int ref_size = is_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; + if (IS_NULL_PTR(target)) { - int res_size = TERM_BOXED_REFERENCE_PROCESS_SIZE + TUPLE_SIZE(5) + target_proc_size; + int res_size = ref_size + TUPLE_SIZE(5) + target_proc_size; + RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = is_alias ? ctx->process_id : INVALID_PROCESS_ID }; + // The monitor is immediately removed by the noproc DOWN, so only an explicit_unalias alias + // stays active (demonitor / reply_demonitor would be deactivated right away, as at a DOWN). + struct Monitor *alias_monitor = NULL; + if (is_alias && alias_type == ContextMonitorAliasExplicitUnalias) { + alias_monitor = monitor_alias_new(&ref_data, alias_type); + if (IS_NULL_PTR(alias_monitor)) { + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + } if (UNLIKELY(memory_ensure_free_opt(ctx, res_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + monitor_destroy(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - uint64_t ref_ticks = globalcontext_get_ref_ticks(ctx->global); - term ref = term_from_ref_ticks(ref_ticks, &ctx->heap); + if (alias_monitor != NULL) { + context_add_monitor(ctx, alias_monitor); + } + term ref = term_from_ref_data(&ref_data, &ctx->heap); term down_message_tuple = term_alloc_tuple(5, &ctx->heap); term_put_tuple_element(down_message_tuple, 0, DOWN_ATOM); term_put_tuple_element(down_message_tuple, 1, ref); @@ -5110,17 +5144,29 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); - free(alias_monitor); + monitor_destroy(alias_monitor); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term monitoring_pid = term_from_local_process_id(ctx->process_id); struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); if (IS_NULL_PTR(other_monitor)) { - free(alias_monitor); - free(self_monitor); + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); + globalcontext_get_process_unlock(ctx->global, target); + RAISE_ERROR(OUT_OF_MEMORY_ATOM); + } + + // Reserve the result reference *before* publishing any monitor/alias state, so an OOM here + // cannot leave the target with a queued MonitorSignal while the caller gets an exception and + // never receives the reference. GC here is safe: the monitor structs hold only immediates. + if (UNLIKELY(memory_ensure_free_opt(ctx, ref_size, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { + monitor_destroy(alias_monitor); + monitor_destroy(self_monitor); + monitor_destroy(other_monitor); globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } + mailbox_send_monitor_signal(target, MonitorSignal, other_monitor); globalcontext_get_process_unlock(ctx->global, target); @@ -5129,10 +5175,6 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) context_add_monitor(ctx, alias_monitor); } - if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { - RAISE_ERROR(OUT_OF_MEMORY_ATOM); - } - return term_from_ref_data(&ref_data, &ctx->heap); } @@ -5209,7 +5251,7 @@ static term nif_erlang_link(Context *ctx, int argc, term argv[]) if (UNLIKELY(!context_add_monitor(ctx, self_link))) { globalcontext_get_process_unlock(ctx->global, target); - free(other_link); + monitor_destroy(other_link); return TRUE_ATOM; } @@ -7567,8 +7609,29 @@ static term nif_erlang_crc32_combine_3(Context *ctx, int argc, term argv[]) static term nif_erlang_alias(Context *ctx, int argc, term argv[]) { - UNUSED(argc); - UNUSED(argv); + // The reply option reuses the reply_demonitor machinery: with no monitor to remove, the alias + // is deactivated when the first message via it is delivered. + context_monitor_alias_type_t alias_type = ContextMonitorAliasExplicitUnalias; + if (argc == 1) { + term opts = argv[0]; + VALIDATE_VALUE(opts, term_is_list); + while (term_is_nonempty_list(opts)) { + term option = term_get_list_head(opts); + if (option == EXPLICIT_UNALIAS_ATOM) { + alias_type = ContextMonitorAliasExplicitUnalias; + } else if (option == REPLY_ATOM) { + alias_type = ContextMonitorAliasReplyDemonitor; + } else if (UNLIKELY(option == PRIORITY_ATOM)) { + RAISE_ERROR(UNSUPPORTED_ATOM); + } else { + RAISE_ERROR(BADARG_ATOM); + } + opts = term_get_list_tail(opts); + } + if (UNLIKELY(!term_is_nil(opts))) { + RAISE_ERROR(BADARG_ATOM); + } + } if (UNLIKELY(memory_ensure_free_opt(ctx, TERM_BOXED_REFERENCE_PROCESS_SIZE, MEMORY_CAN_SHRINK) != MEMORY_GC_OK)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -7576,7 +7639,7 @@ static term nif_erlang_alias(Context *ctx, int argc, term argv[]) RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; term process_ref = term_from_ref_data(&ref_data, &ctx->heap); - struct Monitor *monitor = monitor_alias_new(&ref_data, ContextMonitorAliasExplicitUnalias); + struct Monitor *monitor = monitor_alias_new(&ref_data, alias_type); if (IS_NULL_PTR(monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -7589,14 +7652,19 @@ static term nif_erlang_unalias(Context *ctx, int argc, term argv[]) UNUSED(argc); term process_ref = argv[0]; - VALIDATE_VALUE(process_ref, term_is_local_reference); + VALIDATE_VALUE(process_ref, term_is_reference); + if (UNLIKELY(!term_is_local_reference(process_ref))) { + // An external reference cannot be an alias of the calling process: return false like + // OTP, instead of raising badarg. + return FALSE_ATOM; + } uint64_t ref_ticks = term_to_ref_ticks(process_ref); struct MonitorAlias *alias = context_find_alias(ctx, ref_ticks); if (IS_NULL_PTR(alias)) { return FALSE_ATOM; } else { - context_unalias(alias); + context_unalias(ctx, alias); return TRUE_ATOM; } } diff --git a/src/libAtomVM/nifs.gperf b/src/libAtomVM/nifs.gperf index 6a4c4f6665..ddb4851620 100644 --- a/src/libAtomVM/nifs.gperf +++ b/src/libAtomVM/nifs.gperf @@ -160,6 +160,7 @@ erlang:module_loaded/1,&module_loaded_nif erlang:nif_error/1,&nif_error_nif erlang:list_to_bitstring/1, &list_to_bitstring_nif erlang:alias/0, &erlang_alias_nif +erlang:alias/1, &erlang_alias_nif erlang:unalias/1, &erlang_unalias_nif erts_debug:flat_size/1, &flat_size_nif erts_internal:cmp_term/2, &erts_internal_cmp_term_nif diff --git a/src/libAtomVM/opcodesswitch.h b/src/libAtomVM/opcodesswitch.h index 49d3b11342..8dbc1c6732 100644 --- a/src/libAtomVM/opcodesswitch.h +++ b/src/libAtomVM/opcodesswitch.h @@ -788,7 +788,7 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) #define PROCESS_SIGNAL_MESSAGES() \ { \ - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); \ + MailboxMessage *signal_message = mailbox_process_outer_list(ctx); \ bool handle_error = false; \ bool reprocess_outer = false; \ while (signal_message) { \ @@ -905,6 +905,7 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) RESUME(); \ break; \ } \ + case AliasMessageSignal: \ case NormalMessage: { \ UNREACHABLE(); \ } \ @@ -914,7 +915,7 @@ static void destroy_extended_registers(Context *ctx, unsigned int live) signal_message = next; \ if (UNLIKELY(reprocess_outer && signal_message == NULL)) { \ reprocess_outer = false; \ - signal_message = mailbox_process_outer_list(&ctx->mailbox); \ + signal_message = mailbox_process_outer_list(ctx); \ } \ } \ if (context_get_flags(ctx, Killed)) { \ @@ -2377,25 +2378,16 @@ HOT_FUNC int scheduler_entry_point(GlobalContext *glb) TRACE("send/0 target_pid=%i\n", local_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); globalcontext_send_message(ctx->global, local_process_id, x_regs[1]); + } else if (UNLIKELY(!term_is_reference(recipient_term))) { + RAISE_ERROR(BADARG_ATOM); } else if (term_is_process_reference(recipient_term)) { - int32_t local_process_id = term_process_ref_to_process_id(recipient_term); - TRACE("send/0 target_pid=%i\n", local_process_id); + int32_t target_process_id = term_process_ref_to_process_id(recipient_term); + TRACE("send/0 target_pid=%i\n", target_process_id); TRACE_SEND(ctx, x_regs[0], x_regs[1]); - int64_t ref_ticks = term_to_ref_ticks(recipient_term); - Context *p = globalcontext_get_process_lock(ctx->global, local_process_id); - if (p) { - struct MonitorAlias *alias = context_find_alias(p, ref_ticks); - if (alias != NULL) { - if (alias->alias_type == ContextMonitorAliasReplyDemonitor) { - context_unalias(alias); - } - mailbox_send(p, x_regs[1]); - } - globalcontext_get_process_unlock(ctx->global, p); - } - } else if (!term_is_reference(recipient_term)) { - RAISE_ERROR(BADARG_ATOM); + globalcontext_send_message_to_alias(ctx->global, target_process_id, recipient_term, x_regs[1]); } + // Silently dropped, as OTP does for a send to a non-active-alias reference. + // Outbound distributed aliases are unsupported. x_regs[0] = x_regs[1]; } break; diff --git a/src/libAtomVM/otp_socket.c b/src/libAtomVM/otp_socket.c index 37381fe344..a561e0392d 100644 --- a/src/libAtomVM/otp_socket.c +++ b/src/libAtomVM/otp_socket.c @@ -1032,6 +1032,10 @@ static term nif_socket_select_read(Context *ctx, int argc, term argv[]) term select_ref_term = argv[1]; if (select_ref_term != UNDEFINED_ATOM) { VALIDATE_VALUE(select_ref_term, term_is_local_reference); + // Unlike the BEAM, AtomVM does not support a process alias as a select handle. + if (UNLIKELY(term_is_process_reference(select_ref_term))) { + RAISE_ERROR(BADARG_ATOM); + } } struct SocketResource *rsrc_obj; if (UNLIKELY(!term_to_otp_socket(argv[0], &rsrc_obj, ctx))) { diff --git a/src/libAtomVM/resources.c b/src/libAtomVM/resources.c index 8ebabb5166..87ca0b5eaa 100644 --- a/src/libAtomVM/resources.c +++ b/src/libAtomVM/resources.c @@ -238,6 +238,10 @@ int enif_select(ErlNifEnv *env, ErlNifEvent event, enum ErlNifSelectFlags mode, if (UNLIKELY(mode & (ERL_NIF_SELECT_READ | ERL_NIF_SELECT_WRITE) && !term_is_local_reference(ref) && ref != UNDEFINED_ATOM)) { return ERL_NIF_SELECT_BADARG; } + // Unlike the BEAM, AtomVM does not support a process alias as an enif_select ref. + if (UNLIKELY(term_is_process_reference(ref))) { + return ERL_NIF_SELECT_BADARG; + } return enif_select_common(env, event, mode, obj, pid, ref, NULL); } diff --git a/src/libAtomVM/scheduler.c b/src/libAtomVM/scheduler.c index ffe9eb30e7..6628fc193d 100644 --- a/src/libAtomVM/scheduler.c +++ b/src/libAtomVM/scheduler.c @@ -80,7 +80,7 @@ static void scheduler_process_native_signal_messages(Context *ctx) { // This mirrors PROCESS_SIGNAL_MESSAGES macro of emulated processes, but // for native processes. - MailboxMessage *signal_message = mailbox_process_outer_list(&ctx->mailbox); + MailboxMessage *signal_message = mailbox_process_outer_list_native(&ctx->mailbox); bool reprocess_outer = false; while (signal_message) { switch (signal_message->type) { @@ -137,6 +137,7 @@ static void scheduler_process_native_signal_messages(Context *ctx) case UnlinkRemoteIDSignal: // ports can't be part of distributed links case UnlinkRemoteIDAckSignal: // id. case CodeServerResumeSignal: // ports do not wait for code server + case AliasMessageSignal: // ports cannot own aliases break; case NormalMessage: { UNREACHABLE(); @@ -147,7 +148,7 @@ static void scheduler_process_native_signal_messages(Context *ctx) signal_message = next; if (UNLIKELY(reprocess_outer && signal_message == NULL)) { reprocess_outer = false; - signal_message = mailbox_process_outer_list(&ctx->mailbox); + signal_message = mailbox_process_outer_list_native(&ctx->mailbox); } } } diff --git a/src/libAtomVM/term.c b/src/libAtomVM/term.c index 7fdfde55d3..03a1cadf47 100644 --- a/src/libAtomVM/term.c +++ b/src/libAtomVM/term.c @@ -705,14 +705,14 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_data[0] = other_ticks >> 32; other_data[1] = (uint32_t) other_ticks; } else if (len == 3) { - data[0] = term_process_ref_to_process_id(t); int64_t t_ticks = term_to_ref_ticks(t); - data[1] = t_ticks >> 32; - data[2] = (uint32_t) t_ticks; - other_data[0] = term_process_ref_to_process_id(other); + data[0] = t_ticks >> 32; + data[1] = (uint32_t) t_ticks; + data[2] = term_process_ref_to_process_id(t); int64_t other_ticks = term_to_ref_ticks(other); - other_data[1] = other_ticks >> 32; - other_data[2] = (uint32_t) other_ticks; + other_data[0] = other_ticks >> 32; + other_data[1] = (uint32_t) other_ticks; + other_data[2] = term_process_ref_to_process_id(other); } else { // len == 4 struct RefcBinary *refc = term_resource_refc_binary_ptr(t); @@ -755,6 +755,8 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC len = term_get_external_reference_len(t); } else if (term_is_resource_reference(t)) { len = 4; + } else if (term_is_process_reference(t)) { + len = 3; } else { len = 2; } @@ -762,6 +764,8 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC other_len = term_get_external_reference_len(other); } else if (term_is_resource_reference(other)) { other_len = 4; + } else if (term_is_process_reference(other)) { + other_len = 3; } else { other_len = 2; } @@ -786,6 +790,26 @@ TermCompareResult term_compare(term t, term other, TermCompareOpts opts, GlobalC local_data[1] = (uint32_t) ref_ticks; other_data = local_data; } + } else if (len == 3) { + uint32_t local_data[3]; + if (term_is_external(t)) { + data = term_get_external_reference_words(t); + } else { + int64_t ref_ticks = term_to_ref_ticks(t); + local_data[0] = ref_ticks >> 32; + local_data[1] = (uint32_t) ref_ticks; + local_data[2] = term_process_ref_to_process_id(t); + data = local_data; + } + if (term_is_external(other)) { + other_data = term_get_external_reference_words(other); + } else { + int64_t ref_ticks = term_to_ref_ticks(other); + local_data[0] = ref_ticks >> 32; + local_data[1] = (uint32_t) ref_ticks; + local_data[2] = term_process_ref_to_process_id(other); + other_data = local_data; + } } else { // len == 4 (one is a resource) uint32_t local_data[4]; diff --git a/src/libAtomVM/term.h b/src/libAtomVM/term.h index b68a0bb57b..1175d7f9dc 100644 --- a/src/libAtomVM/term.h +++ b/src/libAtomVM/term.h @@ -129,14 +129,7 @@ extern "C" { #define TERM_BOXED_REFC_BINARY_SIZE 6 #define TERM_BOXED_BIN_MATCH_STATE_SIZE 4 #define TERM_BOXED_SUB_BINARY_SIZE 4 -#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_RESOURCE_SIZE 4 -#else -// Enough size would be 4, but reference types -// are distinguished by size and 4 conflicts with -// TERM_BOXED_REFERENCE_PROCESS_SIZE on 32bit arch. -#define TERM_BOXED_REFERENCE_RESOURCE_SIZE 5 -#endif #define TERM_BOXED_REFERENCE_RESOURCE_HEADER (((TERM_BOXED_REFERENCE_RESOURCE_SIZE - 1) << 6) | TERM_BOXED_REF) #define TERM_BOXED_RESOURCE_SIZE TERM_BOXED_REFERENCE_RESOURCE_SIZE @@ -162,8 +155,15 @@ extern "C" { // If you change a reference size, make sure it doesn't // conflict with other reference sizes on all architectures. #define TERM_BOXED_REFERENCE_SHORT_SIZE ((int) ((sizeof(uint64_t) / sizeof(term)) + 1)) -#define REF_SIZE _Pragma("REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead") TERM_BOXED_REFERENCE_SHORT_SIZE +#define REF_SIZE _Pragma("GCC warning \"REF_SIZE is deprecated, use TERM_BOXED_REFERENCE_SHORT_SIZE instead\"") TERM_BOXED_REFERENCE_SHORT_SIZE +#if TERM_BYTES == 8 #define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 1) +#else +// Enough size would be 3 + 1, but that is the resource reference size on 32-bit. Pad the +// process reference instead of the resource reference: process references only exist when +// aliases are used, while resource references are everywhere on the embedded targets. +#define TERM_BOXED_REFERENCE_PROCESS_SIZE (TERM_BOXED_REFERENCE_SHORT_SIZE + 2) +#endif #define TERM_BOXED_REFERENCE_PROCESS_HEADER (((TERM_BOXED_REFERENCE_PROCESS_SIZE - 1) << 6) | TERM_BOXED_REF) #if TERM_BYTES == 8 #define EXTERNAL_PID_SIZE 3 @@ -182,9 +182,11 @@ extern "C" { #endif #define EXTERNAL_REF_MAX_WORDS 5 #define TERM_BOXED_REFERENCE_MAX_SIZE EXTERNAL_REF_SIZE(EXTERNAL_REF_MAX_WORDS) -_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE < TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must be smaller than process ref size"); -_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE < TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must be smaller than reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE != TERM_BOXED_REFERENCE_PROCESS_SIZE, "Short ref size must differ from process ref size"); +_Static_assert(TERM_BOXED_REFERENCE_SHORT_SIZE != TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Short ref size must differ from reference resource size"); +_Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE != TERM_BOXED_REFERENCE_RESOURCE_SIZE, "Process ref size must differ from reference resource size"); _Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); +_Static_assert(TERM_BOXED_REFERENCE_RESOURCE_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZE, "Max ref size can't be smaller than all other ref sizes"); #define TUPLE_SIZE(elems) ((int) (elems + 1)) #define CONS_SIZE 2 #define REFC_BINARY_CONS_OFFSET 4 @@ -192,7 +194,6 @@ _Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZ #if TERM_BYTES == 4 #define REFERENCE_PROCESS_PID_OFFSET 3 - #elif TERM_BYTES == 8 #define REFERENCE_PROCESS_PID_OFFSET 2 #endif @@ -274,14 +275,6 @@ _Static_assert(TERM_BOXED_REFERENCE_PROCESS_SIZE <= TERM_BOXED_REFERENCE_MAX_SIZ typedef struct GlobalContext GlobalContext; #endif -enum RefType -{ - RefTypeShort, - RefTypeProcess, - RefTypeResource, - RefTypeExternal -}; - typedef struct RefData RefData; struct RefData { @@ -2268,10 +2261,11 @@ static inline uint64_t term_to_ref_ticks(term rt) /** * @brief Creates a process reference * @details Process reference contains ref_ticks and process_id of a process. - * They are used by process aliases and monitors. + * They are used by process aliases and monitors. The ticks occupy the same + * words as in a short reference, so term_to_ref_ticks works on both shapes. * * @param process_id process_id of a process that the reference will identify. - * @param ref_ticks an unique uint64 value that will be used to create ref term. + * @param ref_ticks a unique uint64 value that will be used to create ref term. * @param heap the heap to allocate memory in * @return a ref term created using given ref ticks. */ @@ -2283,30 +2277,34 @@ static inline term term_make_process_reference(int32_t process_id, uint64_t ref_ #if TERM_BYTES == 4 boxed_value[1] = (ref_ticks >> 32); boxed_value[2] = (ref_ticks & 0xFFFFFFFF); - boxed_value[3] = process_id; #elif TERM_BYTES == 8 boxed_value[1] = (term) ref_ticks; - boxed_value[2] = process_id; #else #error "terms must be either 32 or 64 bit wide" +#endif + boxed_value[REFERENCE_PROCESS_PID_OFFSET] = process_id; +#if TERM_BYTES == 4 + // Initialize the trailing padding word so GC copies a defined value instead of uninitialized + // memory. + boxed_value[REFERENCE_PROCESS_PID_OFFSET + 1] = term_nil(); #endif return ((term) boxed_value) | TERM_PRIMARY_BOXED; } -static inline uint32_t term_process_ref_to_process_id(term rt) +/** + * @brief Get the process id out of a process reference + * + * @param rt the process reference term + * @return the process id of the process the reference identifies + */ +static inline int32_t term_process_ref_to_process_id(term rt) { TERM_DEBUG_ASSERT(term_is_process_reference(rt)); const term *boxed_value = term_to_const_term_ptr(rt); -#if TERM_BYTES == 4 - return (uint32_t) boxed_value[3]; -#elif TERM_BYTES == 8 - return (uint32_t) boxed_value[2]; -#else -#error "terms must be either 32 or 64 bit wide" -#endif + return (int32_t) boxed_value[REFERENCE_PROCESS_PID_OFFSET]; } /** @@ -3090,7 +3088,16 @@ static inline term term_from_resource(void *resource, Heap *heap) return ret; } -static inline term term_from_ref_data(RefData *ref_data, Heap *heap) +/** + * @brief Create a reference term from a RefData + * @details Builds a short reference when process_id is INVALID_PROCESS_ID, + * and a process reference carrying the owner process id otherwise. + * + * @param ref_data ref ticks and owner process id of the reference + * @param heap the heap to allocate memory in + * @return a reference term created from the given ref data + */ +static inline term term_from_ref_data(const RefData *ref_data, Heap *heap) { if (ref_data->process_id == INVALID_PROCESS_ID) { return term_from_ref_ticks(ref_data->ref_ticks, heap); diff --git a/src/platforms/esp32/components/avm_builtins/network_driver.c b/src/platforms/esp32/components/avm_builtins/network_driver.c index 84eeb21167..dfe0fc5941 100644 --- a/src/platforms/esp32/components/avm_builtins/network_driver.c +++ b/src/platforms/esp32/components/avm_builtins/network_driver.c @@ -1886,7 +1886,7 @@ static NativeHandlerResult consume_mailbox(Context *ctx) return NativeContinue; } - //TODO: port this code to standard port (and gen_message) + // TODO: port this code to standard port (and gen_message) term pid = term_get_tuple_element(msg, 0); term ref = term_get_tuple_element(msg, 1); term cmd = term_get_tuple_element(msg, 2); diff --git a/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c index af5701ad70..365cd6bb14 100644 --- a/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c +++ b/src/platforms/esp32/components/avm_builtins/usb_cdc_driver.c @@ -169,7 +169,7 @@ EventListener *usb_cdc_interrupt_callback(GlobalContext *glb, EventListener *lis size_t rx_size = 0; esp_err_t ret = tinyusb_cdcacm_read(cdc_data->itf, buf, sizeof(buf), &rx_size); if (ret != ESP_OK || rx_size == 0) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, err_heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, err_heap) term ref = term_from_ref_ticks(reader_ref_ticks, &err_heap); term error_tuple = term_alloc_tuple(2, &err_heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); @@ -186,7 +186,7 @@ EventListener *usb_cdc_interrupt_callback(GlobalContext *glb, EventListener *lis int local_pid = term_to_local_process_id(reader_pid); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); return listener; @@ -578,7 +578,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) SMP_MUTEX_UNLOCK(cdc_data->reader_lock); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -623,7 +623,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { ESP_LOGE(TAG, "Failed to allocate space for error tuple"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { diff --git a/src/platforms/rp2/src/lib/usb_cdc_driver.c b/src/platforms/rp2/src/lib/usb_cdc_driver.c index 89a11a2bee..2569dda785 100644 --- a/src/platforms/rp2/src/lib/usb_cdc_driver.c +++ b/src/platforms/rp2/src/lib/usb_cdc_driver.c @@ -150,7 +150,7 @@ static EventListener *usb_cdc_listener_handler(GlobalContext *glb, EventListener int bin_size = term_binary_heap_size(rx_size); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); } @@ -464,7 +464,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) SMP_MUTEX_UNLOCK(cdc_data->reader_lock); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -508,7 +508,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) int local_pid = term_to_local_process_id(gen_message.pid); if (is_closed) { - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { diff --git a/src/platforms/stm32/src/lib/usb_cdc_driver.c b/src/platforms/stm32/src/lib/usb_cdc_driver.c index fe53f87431..a5d9af0672 100644 --- a/src/platforms/stm32/src/lib/usb_cdc_driver.c +++ b/src/platforms/stm32/src/lib/usb_cdc_driver.c @@ -119,7 +119,7 @@ static void usb_cdc_check_rx(struct USBCDCData *cdc_data) int bin_size = term_binary_heap_size(rx_size); Heap heap; - if (UNLIKELY(memory_init_heap(&heap, bin_size + REF_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_init_heap(&heap, bin_size + TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2) != MEMORY_GC_OK)) { fprintf(stderr, "Failed to allocate memory: %s:%i.\n", __FILE__, __LINE__); AVM_ABORT(); } @@ -367,7 +367,7 @@ static void usb_cdc_driver_do_close(Context *ctx, GenMessage gen_message) cdc_data->reader_process_pid = term_invalid_term(); if (pending_reader_pid != term_invalid_term()) { - BEGIN_WITH_STACK_HEAP(REF_SIZE + TUPLE_SIZE(2) * 2, heap) + BEGIN_WITH_STACK_HEAP(TERM_BOXED_REFERENCE_SHORT_SIZE + TUPLE_SIZE(2) * 2, heap) term error_tuple = term_alloc_tuple(2, &heap); term_put_tuple_element(error_tuple, 0, ERROR_ATOM); term_put_tuple_element(error_tuple, 1, globalcontext_make_atom(glb, ATOM_STR("\x6", "closed"))); @@ -436,7 +436,7 @@ static NativeHandlerResult usb_cdc_driver_consume_mailbox(Context *ctx) if (is_closed) { GlobalContext *glb = ctx->global; - if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + REF_SIZE) != MEMORY_GC_OK)) { + if (UNLIKELY(memory_ensure_free(ctx, TUPLE_SIZE(2) * 2 + TERM_BOXED_REFERENCE_SHORT_SIZE) != MEMORY_GC_OK)) { fprintf(stderr, "usb_cdc: Failed to allocate error tuple\n"); globalcontext_send_message(glb, local_pid, OUT_OF_MEMORY_ATOM); } else { diff --git a/tests/erlang_tests/test_binary_to_term.erl b/tests/erlang_tests/test_binary_to_term.erl index 5bb2e58104..3a5a8007f1 100644 --- a/tests/erlang_tests/test_binary_to_term.erl +++ b/tests/erlang_tests/test_binary_to_term.erl @@ -995,16 +995,13 @@ test_encode_resource(OTPVersion) -> AlteredResource4 = binary_to_term(AlteredResourceBin4), false = AlteredResource4 =:= Resource, ok. + test_encode_process_ref() -> - AliasesAvailable = is_atomvm_or_otp_version_at_least("24"), - if - AliasesAvailable -> - ProcessRef = erlang:alias(), - ProcessRef = binary_to_term(term_to_binary(ProcessRef)), - ok; - true -> - ok - end. + ProcessRef = erlang:alias(), + Bin = term_to_binary(ProcessRef), + <<131, 90, _Len:16, 119, 13, "nonode@nohost", 0:32, _/binary>> = Bin, + ProcessRef = binary_to_term(?MODULE:id(Bin)), + ok. % Verify term_to_binary(binary_to_term(Bin)) is idempotent. binary_to_term_idempotent(Binary, _OTPVersion) -> @@ -1012,12 +1009,6 @@ binary_to_term_idempotent(Binary, _OTPVersion) -> Binary = term_to_binary(Term), Term. -is_atomvm_or_otp_version_at_least(OTPVersion) -> - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= OTPVersion - end. - test_atom_encoding() -> true = compare_pair_encoding(latin1_as_utf8_1), true = compare_pair_encoding(latin1_as_utf8_2), diff --git a/tests/erlang_tests/test_monitor.erl b/tests/erlang_tests/test_monitor.erl index 7d742fee44..2d0e2ac545 100644 --- a/tests/erlang_tests/test_monitor.erl +++ b/tests/erlang_tests/test_monitor.erl @@ -35,34 +35,85 @@ start() -> ok = test_monitor_registered(), ok = test_monitor_registered_noproc(), - AliasesAvailable = - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= "24" - end, - if - AliasesAvailable -> - ok = test_alias(), - ok = test_multiple_aliases(), - ok = test_multiple_unaliases(), - ok = test_unalias_from_wrong_process(), - ok = test_monitor_alias_dead_process(), - ok = test_monitor_multiple_aliases_monitors(fun spawn_monitor/2), - ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), - ok = test_monitor_alias_demonitor(fun spawn_monitor/2), - ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), - ok = test_monitor_alias_explicit_unalias(fun spawn_monitor/2), - ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), - ok = test_monitor_alias_reply_demonitor(fun spawn_monitor/2), - ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), - ok = test_monitor_down_alias(fun spawn_monitor/2), - ok = test_monitor_down_alias(fun spawn_and_monitor/2), - ok; - true -> - ok - end, + ok = test_alias(), + ok = test_multiple_aliases(), + ok = test_multiple_unaliases(), + ok = test_unalias_from_wrong_process(), + ok = test_monitor_alias_dead_process(), + ok = test_monitor_multiple_aliases_monitors(fun spawn_opt_monitor/2), + ok = test_monitor_multiple_aliases_monitors(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_opt_monitor/2), + ok = test_monitor_alias_demonitor(fun spawn_and_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_opt_monitor/2), + ok = test_monitor_alias_explicit_unalias(fun spawn_and_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_opt_monitor/2), + ok = test_monitor_alias_reply_demonitor(fun spawn_and_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_opt_monitor/2), + ok = test_reply_demonitor_removes_monitor(fun spawn_and_monitor/2), + ok = test_monitor_down_alias(fun spawn_opt_monitor/2), + ok = test_monitor_down_alias(fun spawn_and_monitor/2), + ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_opt_monitor/2), + ok = test_monitor_alias_demonitor_deactivates_on_down(fun spawn_and_monitor/2), + ok = test_alias_pid_send_order(), + ok = test_reply_demonitor_same_batch_order(), + ok = test_monitor_alias_noproc_returns_alias(), + ok = test_monitor_alias_self_installs_nothing(), + ok = test_spawn_opt_link_monitor_badarg_is_atomic(), + ok = test_spawn_opt_monitor_non_list_badarg(), + ok = test_monitor_alias_down_before_send_same_batch(), + ok = test_unalias_and_send_non_local_refs(), + ok = test_io_request_alias_reply(), + ok = test_alias_as_key(), + ok = test_monitor_alias_demonitor_flush(), + ok = test_monitor_alias_duplicate_option(), + ok = test_monitor_alias_registered_self_installs_nothing(), + ok = test_alias_1(), + ok = test_alias_reply_mode(), + ok = test_alias_send_after_owner_died(), + ok = test_alias_multi_sender_unalias(), + ok = test_alias_duplicate_options(), + ok = test_unalias_non_reference_badarg(), + ok = test_alias_count_saturation(), + ok = test_binary_to_term_invalid_process_ref(), + ok = test_alias_ref_ordering(), 0. +%% An alias sorts after every plain reference whichever was created first. Two +%% owners' aliases are distinct and strictly ordered, but that direction follows +%% the internal pid (not pid term order) and is implementation defined, so it is +%% not pinned here. +test_alias_ref_ordering() -> + R0 = make_ref(), + A0 = erlang:alias(), + true = R0 < A0, + A1 = erlang:alias(), + R1 = make_ref(), + true = R1 < A1, + Ea = erlang:alias(), + Eb = erlang:alias(), + true = Ea < Eb, + true = Eb =:= binary_to_term(term_to_binary(Eb)), + Parent = self(), + Child = spawn_opt( + fun() -> + receive + {get, P} -> P ! {child_alias, erlang:alias()} + end + end, + [] + ), + Child ! {get, Parent}, + ChildAlias = + receive + {child_alias, Ca} -> Ca + after 5000 -> error(child_alias_timeout) + end, + SelfAlias = erlang:alias(), + true = ChildAlias =/= SelfAlias, + true = (ChildAlias < SelfAlias) xor (SelfAlias < ChildAlias), + _ = [erlang:unalias(R) || R <- [A0, A1, Ea, Eb, SelfAlias]], + ok. + test_monitor_normal() -> Pid = spawn_opt(fun() -> normal_loop() end, []), Ref = monitor(process, Pid), @@ -260,6 +311,7 @@ test_alias() -> P = spawn_opt(fun echo_loop/0, []), Alias = erlang:alias(), do_test_alias(P, Alias), + P ! quit, ok. test_multiple_aliases() -> @@ -270,6 +322,7 @@ test_multiple_aliases() -> do_test_alias(P, A1), do_test_alias(P, A3), do_test_alias(P, A2), + P ! quit, ok. test_multiple_unaliases() -> @@ -286,6 +339,7 @@ test_unalias_from_wrong_process() -> false = recv_one(), P = spawn_opt(fun echo_loop/0, []), do_test_alias(P, A), + P ! quit, ok. do_test_alias(P, Alias) -> @@ -304,6 +358,7 @@ do_test_alias(P, Alias, UnaliasFun) -> test_monitor_alias_demonitor(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), do_test_alias(P, Mon, fun demonitor/1), + P ! quit, ok. test_monitor_alias_explicit_unalias(SpawnFun) -> @@ -312,11 +367,38 @@ test_monitor_alias_explicit_unalias(SpawnFun) -> m1 = recv_one(), demonitor(Mon), do_test_alias(P, Mon), + P ! quit, ok. test_monitor_alias_reply_demonitor(SpawnFun) -> {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), do_test_alias(P, Mon, fun(_Mon) -> ok end), + P ! quit, + ok. + +test_reply_demonitor_removes_monitor(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, reply_demonitor}]), + Ref = make_ref(), + P ! {{reply, Ref}, Mon}, + {reply, Ref} = recv_one(), + %% Monitors fire in installation order, so a stale 'DOWN' from a not-removed monitor + %% would arrive before this fence's. On the BEAM no such 'DOWN' exists. + Fence = monitor(process, P), + P ! quit, + {'DOWN', Fence, process, P, normal} = recv_one(), + ok = assert_no_message(), + ok. + +%% Self-sending to our own alias and only then receiving guarantees both alias signals +%% drain in a single outer-list batch, so this is deterministic on SMP and non-SMP. +test_reply_demonitor_same_batch_order() -> + P = spawn_opt(fun echo_loop/0, []), + Mon = erlang:monitor(process, P, [{alias, reply_demonitor}]), + Mon ! first, + Mon ! second, + first = recv_one(), + ok = assert_no_message(), + P ! quit, ok. test_monitor_down_alias(SpawnFun) -> @@ -329,6 +411,88 @@ test_monitor_down_alias(SpawnFun) -> {'DOWN', Mon, process, P, normal} = recv_one(), ok. +test_monitor_alias_demonitor_deactivates_on_down(SpawnFun) -> + {P, Mon} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), + P ! quit, + {'DOWN', Mon, process, P, normal} = recv_one(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {should_drop, Mon}, + %% Sends from one process keep their order, so once the fence reply arrives a dropped + %% alias message can no longer show up afterwards. + Echo ! {fence, self()}, + fence = recv_one(), + ok = assert_no_message(), + Echo ! quit, + ok. + +test_alias_pid_send_order() -> + Parent = self(), + P = spawn_opt( + fun() -> + Alias = erlang:alias(), + Parent ! {ready, self(), Alias}, + receive + A -> Parent ! {got, A} + end, + receive + B -> Parent ! {got, B} + end + end, + [] + ), + {ready, P, Alias} = recv_one(), + Alias ! m1, + P ! m2, + {got, m1} = recv_one(), + {got, m2} = recv_one(), + ok. + +test_monitor_alias_noproc_returns_alias() -> + {P, _} = spawn_opt(fun() -> ok end, [monitor]), + ok = + receive + {'DOWN', _, _, P, _} -> ok + after 5000 -> timeout + end, + Mon = erlang:monitor(process, P, [{alias, explicit_unalias}]), + {'DOWN', Mon, process, P, noproc} = recv_one(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {via_alias, Mon}, + via_alias = recv_one(), + true = erlang:unalias(Mon), + Echo ! quit, + ok. + +test_monitor_alias_self_installs_nothing() -> + Mon = erlang:monitor(process, self(), [{alias, explicit_unalias}]), + Mon ! hello, + ok = assert_no_message(), + false = erlang:unalias(Mon), + false = erlang:demonitor(Mon, [info]), + ok. + +%% The link is installed before the monitor options are parsed, so the badarg must +%% still unwind it: a surviving link would later deliver a spurious {'EXIT', Pid, normal}. +test_spawn_opt_link_monitor_badarg_is_atomic() -> + %% On the BEAM the test process is linked to init, so compare against the initial links + %% instead of []. + {links, LinksBefore} = erlang:process_info(self(), links), + false = erlang:process_flag(trap_exit, true), + ok = + try spawn_opt(fun() -> ok end, [link, {monitor, [bad_option]}]) of + Result -> {unexpected, Result} + catch + error:badarg -> ok + end, + {links, LinksBefore} = erlang:process_info(self(), links), + ok = + receive + Other -> {unexpected_message, Other} + after 200 -> ok + end, + true = erlang:process_flag(trap_exit, false), + ok. + test_monitor_multiple_aliases_monitors(SpawnFun) -> {P, Mon1} = SpawnFun(fun echo_loop/0, [{alias, demonitor}]), Mon2 = erlang:monitor(process, P, [{alias, reply_demonitor}]), @@ -357,7 +521,407 @@ test_monitor_alias_dead_process() -> {'DOWN', Mon3, process, P, noproc} = recv_one(), ok. -spawn_monitor(LoopFun, Opts) -> +%% A non-list, non-'true' monitor value fails before the monitor-option parser, unlike +%% {monitor, [BadOption]}, so it is exercised separately here. +test_spawn_opt_monitor_non_list_badarg() -> + ok = + try spawn_opt(fun() -> ok end, [{monitor, foo}]) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try spawn_opt(fun() -> ok end, [{monitor, 123}]) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end, + ok. + +%% A 'DOWN' that deactivates a {alias, demonitor} alias must drop an alias send that lands +%% in the SAME mailbox drain. The relay sends the alias message only after seeing the owner's +%% 'DOWN', so both reach the owner in one batch. The owner busy-waits on whereis/1 rather than +%% receiving, because a receive would drain its mailbox before the batch is assembled. +%% On a single scheduler the owner may drain the 'DOWN' alone first. The test then passes +%% through the cross-batch deactivation path instead. The same-batch path is reliably +%% exercised only on SMP builds. +test_monitor_alias_down_before_send_same_batch() -> + P = spawn_opt( + fun() -> + receive + quit -> ok + end + end, + [] + ), + %% Monitor P before the relay does, so the owner's 'DOWN' is posted before the relay's. + Mon = erlang:monitor(process, P, [{alias, demonitor}]), + Relay = spawn_opt( + fun() -> + erlang:monitor(process, P), + receive + {'DOWN', _, process, P, _} -> + Mon ! should_drop, + register(down_batch_relay, self()), + receive + release -> ok + end + end + end, + [] + ), + P ! quit, + %% The huge spin bound absorbs valgrind's unfair scheduling, which can starve the relay + %% while this process spins. It must busy-wait: receiving would drain its mailbox. + ok = wait_registered(down_batch_relay, 50000000), + {'DOWN', Mon, process, P, normal} = recv_one(), + ok = assert_no_message(), + Relay ! release, + ok. + +test_unalias_and_send_non_local_refs() -> + %% NEWER_REFERENCE_EXT (90): Len:16, Node atom, Creation:32, Len x 4-byte words. + ExtRef = binary_to_term( + <<131, 90, 2:16/integer-unsigned-big, 119, 3, "x@x", 1:32/integer-unsigned-big, + 1:32/integer-unsigned-big, 2:32/integer-unsigned-big>> + ), + true = is_reference(ExtRef), + false = unalias(ExtRef), + hello = (ExtRef ! hello), + false = unalias(make_ref()), + hello = (make_ref() ! hello), + ok = + receive + Unexpected -> {unexpected_message, Unexpected} + after 100 -> ok + end, + ok. + +%% An alias passed as ReplyAs must come back verbatim. A short reference rebuilt from its +%% ticks would not match it, so the receive below would not fire. +test_io_request_alias_reply() -> + %% On the BEAM the group leader is a full io server. On AtomVM the test process has no + %% group leader, so talk to the console port driver directly. + IoServer = + case erlang:system_info(machine) of + "BEAM" -> group_leader(); + _ -> open_port({spawn, "console"}, []) + end, + Alias = erlang:alias(), + IoServer ! {io_request, self(), Alias, {put_chars, unicode, <<>>}}, + ok = + receive + {io_reply, Alias, ok} -> ok + after 5000 -> io_reply_did_not_match_alias + end, + true = erlang:unalias(Alias), + ok. + +test_alias_as_key() -> + Alias = erlang:alias(), + Plain = make_ref(), + Map = #{Alias => alias_value, Plain => plain_value}, + alias_value = maps:get(Alias, Map), + plain_value = maps:get(Plain, Map), + Tid = ets:new(alias_key_table, []), + true = ets:insert(Tid, {Alias, alias_value}), + true = ets:insert(Tid, {Plain, plain_value}), + [{Alias, alias_value}] = ets:lookup(Tid, Alias), + [{Plain, plain_value}] = ets:lookup(Tid, Plain), + true = ets:delete(Tid, Alias), + [] = ets:lookup(Tid, Alias), + [{Plain, plain_value}] = ets:lookup(Tid, Plain), + true = erlang:unalias(Alias), + ok. + +test_monitor_alias_demonitor_flush() -> + P = spawn_opt( + fun() -> + receive + quit -> ok + end + end, + [] + ), + Mon = erlang:monitor(process, P, [{alias, demonitor}]), + Fence = monitor(process, P), + P ! quit, + %% Mon's 'DOWN' was enqueued first (installation order). This selective receive leaves it + %% queued for the flush below to remove. + ok = + receive + {'DOWN', Fence, process, P, normal} -> ok + after 5000 -> timeout + end, + true = demonitor(Mon, [flush]), + ok = assert_no_message(), + Echo = spawn_opt(fun echo_loop/0, []), + Echo ! {should_drop, Mon}, + Echo ! {fence, self()}, + fence = recv_one(), + ok = assert_no_message(), + Echo ! quit, + ok. + +%% With duplicate {alias, _} options the last one wins, like OTP 29. +test_monitor_alias_duplicate_option() -> + P = spawn_opt(fun echo_loop/0, []), + Mon = erlang:monitor(process, P, [{alias, demonitor}, {alias, explicit_unalias}]), + true = demonitor(Mon), + do_test_alias(P, Mon), + P ! quit, + ok. + +test_monitor_alias_registered_self_installs_nothing() -> + true = register(alias_self_name, self()), + Mon = erlang:monitor(process, alias_self_name, [{alias, explicit_unalias}]), + Mon ! hello, + ok = assert_no_message(), + false = erlang:unalias(Mon), + false = erlang:demonitor(Mon, [info]), + true = unregister(alias_self_name), + ok. + +test_alias_1() -> + A1 = alias([]), + A1 ! x1, + x1 = recv_one(), + true = unalias(A1), + A2 = alias([explicit_unalias]), + A2 ! x2, + x2 = recv_one(), + true = unalias(A2), + ok = + try alias([bogus]) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try alias(explicit_unalias) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end, + ok. + +%% A reply alias is deactivated when its first message is delivered, so a second message in +%% the same batch is dropped too, not just delayed. +test_alias_reply_mode() -> + A = alias([reply]), + A ! m1, + A ! m2, + m1 = recv_one(), + ok = assert_no_message(), + A ! m3, + %% A dropped refc binary exercises the mso sweep of the immediately-freed signal. + A ! <<0:1600>>, + ok = assert_no_message(), + false = unalias(A), + ok. + +%% AtomVM assigns process ids monotonically and ref ticks are globally unique, so a dead +%% owner's stale alias stays unmatchable even by a later process. The churn loop spawns such +%% processes to confirm a stale-alias send never surfaces anywhere. +test_alias_send_after_owner_died() -> + Parent = self(), + {P, Fence} = spawn_opt(fun() -> Parent ! {alias, erlang:alias()} end, [monitor]), + {alias, A} = recv_one(), + {'DOWN', Fence, process, P, normal} = recv_one(), + hello = (A ! hello), + ok = churn_and_send_stale(A, 20), + ok = assert_no_message(), + ok. + +churn_and_send_stale(_A, 0) -> + ok; +churn_and_send_stale(A, N) -> + Parent = self(), + {Q, Mon} = spawn_opt(fun() -> stale_alias_probe(Parent) end, [monitor]), + drop = (A ! drop), + Q ! quit, + {'DOWN', Mon, process, Q, normal} = recv_one(), + churn_and_send_stale(A, N - 1). + +%% A stale-alias signal is dropped against this process's empty alias list, so it must never +%% surface here as a plain message. Any non-quit message is reported as a misdelivery. +stale_alias_probe(Parent) -> + receive + quit -> + ok; + Other -> + Parent ! {misdelivered, Other}, + stale_alias_probe(Parent) + end. + +%% Several senders hammer one alias while the owner unaliases mid-stream. Synchronization is +%% per-sender send order plus explicit acks, with no sleeps or timing windows, so the test +%% cannot flake on slow hosts. A sender's alias messages all precede its sent_all fence in the +%% owner's queue, so once the last phase-1 fence is consumed Count1 has counted them all. +test_alias_multi_sender_unalias() -> + NSenders = 4, + NMsgs = 25, + A = erlang:alias(), + Senders = spawn_alias_senders(self(), A, NMsgs, NSenders), + ok = send_to_each(Senders, go), + Count1 = drain_alias_msgs(NSenders, sent_all, 0), + Count1 = NSenders * NMsgs, + ok = send_to_each(Senders, go2), + true = erlang:unalias(A), + Count2 = drain_alias_msgs(NSenders, sent_all2, 0), + true = Count2 =< NSenders * NMsgs, + false = erlang:unalias(A), + dead = (A ! dead), + ok = assert_no_message(), + ok. + +spawn_alias_senders(_Owner, _A, _NMsgs, 0) -> + []; +spawn_alias_senders(Owner, A, NMsgs, K) -> + Pid = spawn_opt(fun() -> alias_sender(Owner, A, NMsgs) end, []), + [Pid | spawn_alias_senders(Owner, A, NMsgs, K - 1)]. + +alias_sender(Owner, A, NMsgs) -> + receive + go -> ok + end, + ok = alias_blast(A, NMsgs), + Owner ! sent_all, + receive + go2 -> ok + end, + ok = alias_blast(A, NMsgs), + Owner ! sent_all2, + ok. + +alias_blast(_A, 0) -> + ok; +alias_blast(A, N) -> + {am, N} = (A ! {am, N}), + alias_blast(A, N - 1). + +send_to_each([], _Msg) -> + ok; +send_to_each([Pid | Rest], Msg) -> + Pid ! Msg, + send_to_each(Rest, Msg). + +drain_alias_msgs(0, _FenceMsg, Count) -> + Count; +drain_alias_msgs(FencesLeft, FenceMsg, Count) -> + case recv_one() of + {am, _} -> drain_alias_msgs(FencesLeft, FenceMsg, Count + 1); + FenceMsg -> drain_alias_msgs(FencesLeft - 1, FenceMsg, Count); + Other -> {unexpected, Other} + end. + +%% With duplicate alias/1 options the last one wins, like OTP 29. Both orders are checked. +test_alias_duplicate_options() -> + A1 = alias([explicit_unalias, reply]), + A1 ! r1, + A1 ! r2, + %% A dropped refc binary exercises the mso sweep on the received-order drop path too. + A1 ! <<0:1600>>, + r1 = recv_one(), + ok = assert_no_message(), + false = unalias(A1), + A2 = alias([reply, explicit_unalias]), + A2 ! e1, + A2 ! e2, + e1 = recv_one(), + e2 = recv_one(), + true = unalias(A2), + ok. + +test_unalias_non_reference_badarg() -> + ok = + try unalias(42) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + ok. + +%% The active alias count saturates at 255 aliases and stays saturated until the last monitor +%% is removed. Sends must keep delivering while saturated (a wrapped count would drop every +%% alias of the process) and through a fresh alias after the count recovered. +test_alias_count_saturation() -> + Parent = self(), + spawn_opt(fun() -> saturation_worker(Parent) end, []), + {survivor, Survivor} = recv_one(), + ping1 = (Survivor ! ping1), + {fresh, Stale, Fresh} = recv_one(), + drop = (Stale ! drop), + ping2 = (Fresh ! ping2), + done = recv_one(), + ok. + +%% The worker owns nothing but its aliases, so unaliasing the last one empties its monitor +%% list. It must stay unlinked and unmonitored for that to hold. +saturation_worker(Parent) -> + [Survivor | Rest] = make_aliases(300), + ok = unalias_all(Rest), + Parent ! {survivor, Survivor}, + receive + ping1 -> ok + end, + true = unalias(Survivor), + Fresh = erlang:alias(), + Parent ! {fresh, Survivor, Fresh}, + receive + ping2 -> ok + end, + ok = assert_no_message(), + Parent ! done. + +make_aliases(0) -> + []; +make_aliases(N) -> + [erlang:alias() | make_aliases(N - 1)]. + +unalias_all([]) -> + ok; +unalias_all([A | Rest]) -> + true = unalias(A), + unalias_all(Rest). + +%% The owner pid word of a wire-format alias is untrusted input: decoding must reject pid 0 +%% (the short-ref sentinel) and pids above the 28-bit maximum. The BEAM treats reference words +%% as opaque payload, so it decodes the patched binaries as plain references instead of +%% rejecting them. Only AtomVM gives the third word pid semantics, so the test forks on machine. +test_binary_to_term_invalid_process_ref() -> + A = erlang:alias(), + B = term_to_binary(A), + A = binary_to_term(B), + PrefixSize = byte_size(B) - 4, + <> = B, + TooBigPid = 1 bsl 28, + BadZero = <>, + BadBig = <>, + case erlang:system_info(machine) of + "BEAM" -> + true = is_reference(binary_to_term(BadZero)), + true = is_reference(binary_to_term(BadBig)); + _ -> + %% On AtomVM the alias serializes as a len-3 reference whose last word is the pid. + <<131, 90, 3:16, _/binary>> = B, + ok = + try binary_to_term(BadZero) of + R1 -> {unexpected, R1} + catch + error:badarg -> ok + end, + ok = + try binary_to_term(BadBig) of + R2 -> {unexpected, R2} + catch + error:badarg -> ok + end + end, + true = erlang:unalias(A), + ok. + +spawn_opt_monitor(LoopFun, Opts) -> spawn_opt(LoopFun, [{monitor, Opts}]). spawn_and_monitor(LoopFun, Opts) -> @@ -382,5 +946,21 @@ echo_loop() -> recv_one() -> receive Msg -> Msg - after 500 -> timeout + after 5000 -> timeout + end. + +%% Only call this once the would-be message is already settled, behind a fence reply or after +%% a same-process send, so the short timeout window is not a race. +assert_no_message() -> + receive + Msg -> {unexpected_message, Msg} + after 100 -> ok + end. + +wait_registered(_Name, 0) -> + timeout; +wait_registered(Name, N) -> + case whereis(Name) of + undefined -> wait_registered(Name, N - 1); + _ -> ok end. diff --git a/tests/erlang_tests/test_refs_ordering.erl b/tests/erlang_tests/test_refs_ordering.erl index 6b54f8bf24..f1766506b4 100644 --- a/tests/erlang_tests/test_refs_ordering.erl +++ b/tests/erlang_tests/test_refs_ordering.erl @@ -68,14 +68,4 @@ bool_to_n(false) -> 0. make_alias_ref() -> - AliasesAvailable = - case erlang:system_info(machine) of - "ATOM" -> true; - "BEAM" -> erlang:system_info(otp_release) >= "24" - end, - if - AliasesAvailable -> - erlang:alias(); - true -> - {mock_alias_ref, make_ref()} - end. + erlang:alias(). diff --git a/tests/libs/eavmlib/test_file.erl b/tests/libs/eavmlib/test_file.erl index c69a3a9f24..480e8f3db2 100644 --- a/tests/libs/eavmlib/test_file.erl +++ b/tests/libs/eavmlib/test_file.erl @@ -29,6 +29,7 @@ test() -> HasExecve = atomvm:platform() =/= emscripten, ok = test_basic_file(), ok = test_fifo_select(HasSelect), + ok = test_alias_select_handle_rejected(HasSelect), ok = test_gc(HasSelect), ok = test_crash_no_leak(HasSelect), ok = test_select_with_gone_process(HasSelect), @@ -126,6 +127,25 @@ test_fifo_select(_HasSelect) -> ok = atomvm:posix_close(WrFd), ok = atomvm:posix_unlink(Path). +test_alias_select_handle_rejected(false) -> + ok; +test_alias_select_handle_rejected(_HasSelect) -> + Path = "/tmp/atomvm.tmp." ++ integer_to_list(erlang:system_time(millisecond)), + ok = atomvm:posix_mkfifo(Path, 8#644), + {ok, RdFd} = atomvm:posix_open(Path, [o_rdonly]), + {ok, WrFd} = atomvm:posix_open(Path, [o_wronly]), + Alias = alias(), + ok = + try atomvm:posix_select_write(WrFd, self(), Alias) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + true = unalias(Alias), + ok = atomvm:posix_close(RdFd), + ok = atomvm:posix_close(WrFd), + ok = atomvm:posix_unlink(Path). + % 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)), diff --git a/tests/libs/estdlib/test_net_kernel.erl b/tests/libs/estdlib/test_net_kernel.erl index f67f7c5417..209ac58f28 100644 --- a/tests/libs/estdlib/test_net_kernel.erl +++ b/tests/libs/estdlib/test_net_kernel.erl @@ -44,6 +44,7 @@ test() -> ok = test_link_remote_exit_local(Platform), ok = test_link_local_unlink_remote(Platform), ok = test_link_local_unlink_local(Platform), + ok = test_alias_send_from_beam(Platform), ok = test_is_alive(), ok = test_ping_with_avm_dist_opts(Platform), ok; @@ -498,6 +499,32 @@ test_link_local_unlink_local(Platform) -> ok = stop_apply_loop(BeamMainPid, Pid, MonitorRef), ok. +test_alias_send_from_beam(Platform) -> + {BeamMainPid, Pid, MonitorRef} = start_apply_loop(Platform), + Alias = alias(), + {via_alias, Alias} = call_apply_loop( + BeamMainPid, {self(), apply, erlang, send, [Alias, {via_alias, Alias}]} + ), + ok = + receive + {via_alias, Alias} -> ok + after 30000 -> alias_message_timeout + end, + true = unalias(Alias), + should_be_dropped = call_apply_loop( + BeamMainPid, {self(), apply, erlang, send, [Alias, should_be_dropped]} + ), + %% The dropped send precedes this reply on the same connection, so `after 0` is + %% race-free. Match only alias messages: a stale 'EXIT' may be queued on BEAM. + ok = + receive + should_be_dropped -> unexpected_alias_message; + {via_alias, _} = Unexpected -> {unexpected_alias_message, Unexpected} + after 0 -> ok + end, + ok = stop_apply_loop(BeamMainPid, Pid, MonitorRef), + ok. + test_is_alive() -> false = is_alive(), {ok, _NetKernelPid} = net_kernel:start(atomvm, #{name_domain => shortnames}), diff --git a/tests/libs/estdlib/test_udp_socket.erl b/tests/libs/estdlib/test_udp_socket.erl index 359b001bee..ff57169669 100644 --- a/tests/libs/estdlib/test_udp_socket.erl +++ b/tests/libs/estdlib/test_udp_socket.erl @@ -213,6 +213,7 @@ test_nowait() -> ok = test_nowait(fun receive_loop_nowait_ref/2), ok = test_nowait(fun receive_loop_recvfrom_nowait/2), ok = test_nowait(fun receive_loop_recvfrom_nowait_ref/2), + ok = test_alias_select_handle_rejected(), ok. test_nowait(ReceiveFun) -> @@ -267,6 +268,23 @@ receive_loop_nowait_ref(Socket, Packet) -> Error end. +test_alias_select_handle_rejected() -> + case erlang:system_info(machine) of + "BEAM" -> + ok; + _ -> + {ok, Socket} = socket:open(inet, dgram, udp), + Alias = alias(), + ok = + try socket:recv(Socket, 1, Alias) of + R -> {unexpected, R} + catch + error:badarg -> ok + end, + true = unalias(Alias), + ok = socket:close(Socket) + end. + receive_loop_recvfrom_nowait(Socket, Packet) -> case socket:recvfrom(Socket, byte_size(Packet), nowait) of {ok, {_Source, ReceivedPacket}} when ReceivedPacket =:= Packet -> diff --git a/tests/test-mailbox.c b/tests/test-mailbox.c index d7af4b98ea..35374aa74a 100644 --- a/tests/test-mailbox.c +++ b/tests/test-mailbox.c @@ -35,7 +35,7 @@ void test_mailbox_send(void) mailbox_send(ctx, term_from_int(1)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_has_next(&ctx->mailbox)); assert(mailbox_peek(ctx, &t)); assert(1 == term_to_int(t)); @@ -58,7 +58,7 @@ void test_mailbox_next(void) // 1. send send peek next peek remove peek remove mailbox_send(ctx, term_from_int(1)); mailbox_send(ctx, term_from_int(2)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_peek(ctx, &t)); assert(1 == term_to_int(t)); @@ -79,12 +79,12 @@ void test_mailbox_next(void) // 2. send peek send next peek remove peek remove mailbox_send(ctx, term_from_int(1)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_peek(ctx, &t)); assert(1 == term_to_int(t)); mailbox_send(ctx, term_from_int(2)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_has_next(&ctx->mailbox)); mailbox_next(&ctx->mailbox); @@ -102,7 +102,7 @@ void test_mailbox_next(void) // 3. send peek next send peek remove peek remove mailbox_send(ctx, term_from_int(1)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_peek(ctx, &t)); assert(1 == term_to_int(t)); @@ -110,7 +110,7 @@ void test_mailbox_next(void) mailbox_next(&ctx->mailbox); mailbox_send(ctx, term_from_int(2)); - assert(mailbox_process_outer_list(&ctx->mailbox) == NULL); + assert(mailbox_process_outer_list_native(&ctx->mailbox) == NULL); assert(mailbox_peek(ctx, &t)); assert(2 == term_to_int(t)); From e11cc94cf854fdbc01302a42c0dd0c8685a695a6 Mon Sep 17 00:00:00 2001 From: Davide Bettio Date: Sun, 19 Jul 2026 12:23:03 +0000 Subject: [PATCH 6/6] Encode the DOWN ref shape in the monitor type Storing a RefData (ref ticks plus owner pid) in every local monitor grew each MonitorLocalMonitor, MonitorLocalRegisteredNameMonitor and MonitorAlias by 8 bytes (uint64_t alignment), paid by every plain monitor/2, that is by every gen_server:call, on the embedded targets. The pid was redundant everywhere: on the monitored side it duplicates monitor_obj, the monitoring process, which is exactly the alias owner; on the monitoring, registered name and alias entries it was never read. Keep a plain uint64_t ref_ticks in the monitor structs, their baseline layout, and carry the only new bit of information, that the 'DOWN' reference must be rebuilt alias shaped, as a monitor type: CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS. The 'DOWN' path derives the process reference pid from monitor_obj. monitor_new now takes the monitor type directly, replacing the is_monitoring bool as well. RefData stays as the NIF local value type used to build result references (term_from_ref_data) in monitor/3, spawn_opt and alias/1. Behavior is unchanged and pinned by the existing tests: test_monitor_down_alias asserts the 'DOWN' carries the very alias reference, and process_info(monitored_by) keeps listing alias shaped monitors. Signed-off-by: Davide Bettio --- src/libAtomVM/context.c | 85 ++++++++++++++++++++++++----------------- src/libAtomVM/context.h | 25 ++++++------ src/libAtomVM/nifs.c | 20 +++++----- 3 files changed, 74 insertions(+), 56 deletions(-) diff --git a/src/libAtomVM/context.c b/src/libAtomVM/context.c index 5b39f16e5e..bca423eb95 100644 --- a/src/libAtomVM/context.c +++ b/src/libAtomVM/context.c @@ -279,6 +279,7 @@ void context_destroy(Context *ctx) } case CONTEXT_MONITOR_LINK_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: case CONTEXT_MONITOR_ALIAS: @@ -445,7 +446,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL) { struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_obj == monitor_obj && monitoring_monitor->ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); free(monitoring_monitor); @@ -464,7 +465,7 @@ void context_process_monitor_down_signal(Context *ctx, struct TermSignal *signal } else if (monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME) { int32_t monitor_process_id = term_to_local_process_id(monitor_obj); struct MonitorLocalRegisteredNameMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_data.ref_ticks == ref_ticks) { + if (monitoring_monitor->monitor_process_id == monitor_process_id && monitoring_monitor->ref_ticks == ref_ticks) { // Remove link list_remove(&monitor->monitor_list_head); @@ -619,7 +620,8 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a ret_size = TUPLE_SIZE(2); LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); - if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL) { + if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { ret_size += CONS_SIZE; } else if (monitor->monitor_type == CONTEXT_MONITOR_RESOURCE) { ret_size += CONS_SIZE + TERM_BOXED_REFERENCE_RESOURCE_SIZE; @@ -728,7 +730,8 @@ bool context_get_process_info(Context *ctx, term *out, size_t *term_size, term a struct ListHead *item; LIST_FOR_EACH (item, &ctx->monitors_head) { struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); - if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL) { + if (monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS) { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); list = term_list_prepend(monitored_monitor->monitor_obj, list, heap); } else if (monitor->monitor_type == CONTEXT_MONITOR_RESOURCE) { @@ -786,7 +789,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); } free(monitoring_monitor); break; @@ -798,7 +801,7 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) Context *target = globalcontext_get_process_nolock(glb, local_process_id); if (LIKELY(target != NULL)) { // target can be null if we didn't process a MonitorDownSignal - mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_data.ref_ticks); + mailbox_send_ref_signal(target, DemonitorSignal, monitoring_monitor->ref_ticks); } free(monitoring_monitor); break; @@ -840,13 +843,14 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) } break; } - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); int32_t local_process_id = term_to_local_process_id(monitored_monitor->monitor_obj); Context *target = globalcontext_get_process_nolock(glb, local_process_id); // Target cannot be NULL as we processed Demonitor signals assert(target != NULL); - bool is_monitored_alias = monitored_monitor->ref_data.process_id != INVALID_PROCESS_ID; + bool is_monitored_alias = monitor->monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS; int ref_size = is_monitored_alias ? TERM_BOXED_REFERENCE_PROCESS_SIZE : TERM_BOXED_REFERENCE_SHORT_SIZE; int required_terms = ref_size + TUPLE_SIZE(5); if (UNLIKELY(memory_ensure_free(ctx, required_terms) != MEMORY_GC_OK)) { @@ -856,7 +860,12 @@ static struct Monitor *context_monitors_handle_terminate(Context *ctx) AVM_ABORT(); } // Prepare the message on ctx's heap, which is freed afterwards. - term ref = term_from_ref_data(&monitored_monitor->ref_data, &ctx->heap); + term ref; + if (is_monitored_alias) { + ref = term_make_process_reference(local_process_id, monitored_monitor->ref_ticks, &ctx->heap); + } else { + ref = term_from_ref_ticks(monitored_monitor->ref_ticks, &ctx->heap); + } term port_or_process = term_pid_or_port_from_context(ctx); term port_or_process_atom @@ -920,24 +929,23 @@ struct Monitor *monitor_link_new(term link_pid) } } -struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring) +struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, enum ContextMonitorType monitor_type) { + assert(monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL + || monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL + || monitor_type == CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS); struct MonitorLocalMonitor *monitor = malloc(sizeof(struct MonitorLocalMonitor)); if (IS_NULL_PTR(monitor)) { return NULL; } - if (is_monitoring) { - monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL; - } else { - monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORED_LOCAL; - } + monitor->monitor.monitor_type = monitor_type; monitor->monitor_obj = monitor_pid; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; return &monitor->monitor; } -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data) +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks) { struct MonitorLocalRegisteredNameMonitor *monitor = malloc(sizeof(struct MonitorLocalRegisteredNameMonitor)); if (IS_NULL_PTR(monitor)) { @@ -946,19 +954,19 @@ struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, t monitor->monitor.monitor_type = CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME; monitor->monitor_process_id = monitor_process_id; monitor->monitor_name = monitor_name; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; return &monitor->monitor; } -struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type) +struct Monitor *monitor_alias_new(uint64_t ref_ticks, context_monitor_alias_type_t alias_type) { struct MonitorAlias *monitor = malloc(sizeof(struct MonitorAlias)); if (IS_NULL_PTR(monitor)) { return NULL; } monitor->monitor.monitor_type = CONTEXT_MONITOR_ALIAS; - monitor->ref_data = *ref_data; + monitor->ref_ticks = ref_ticks; monitor->alias_type = alias_type; return &monitor->monitor; @@ -993,6 +1001,7 @@ void monitor_destroy(struct Monitor *monitor) break; case CONTEXT_MONITOR_MONITORING_LOCAL: case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: free(CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor)); break; case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: @@ -1024,10 +1033,11 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) break; } case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *new_local_monitor = CONTAINER_OF(new_monitor, struct MonitorLocalMonitor, monitor); struct MonitorLocalMonitor *existing_local_monitor = CONTAINER_OF(existing, struct MonitorLocalMonitor, monitor); - if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_data.ref_ticks == new_local_monitor->ref_data.ref_ticks)) { + if (UNLIKELY(existing_local_monitor->monitor_obj == new_local_monitor->monitor_obj && existing_local_monitor->ref_ticks == new_local_monitor->ref_ticks)) { free(new_local_monitor); return false; } @@ -1038,7 +1048,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorLocalRegisteredNameMonitor *existing_local_registeredname_monitor = CONTAINER_OF(existing, struct MonitorLocalRegisteredNameMonitor, monitor); if (UNLIKELY(existing_local_registeredname_monitor->monitor_process_id == new_local_registeredname_monitor->monitor_process_id && existing_local_registeredname_monitor->monitor_name == new_local_registeredname_monitor->monitor_name - && existing_local_registeredname_monitor->ref_data.ref_ticks == new_local_registeredname_monitor->ref_data.ref_ticks)) { + && existing_local_registeredname_monitor->ref_ticks == new_local_registeredname_monitor->ref_ticks)) { free(new_local_registeredname_monitor); return false; } @@ -1048,7 +1058,7 @@ bool context_add_monitor(Context *ctx, struct Monitor *new_monitor) struct MonitorAlias *new_alias_monitor = CONTAINER_OF(new_monitor, struct MonitorAlias, monitor); struct MonitorAlias *existing_alias_monitor = CONTAINER_OF(existing, struct MonitorAlias, monitor); - if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_data.ref_ticks == new_alias_monitor->ref_data.ref_ticks)) { + if (UNLIKELY(existing_alias_monitor->alias_type == new_alias_monitor->alias_type && existing_alias_monitor->ref_ticks == new_alias_monitor->ref_ticks)) { free(new_alias_monitor); return false; } @@ -1202,9 +1212,10 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); switch (monitor->monitor_type) { case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_monitor->ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_monitor); return; @@ -1213,7 +1224,7 @@ void context_demonitor(Context *ctx, uint64_t ref_ticks) } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_ticks == ref_ticks) { list_remove(&monitor->monitor_list_head); free(local_registeredname_monitor); return; @@ -1249,7 +1260,7 @@ struct MonitorAlias *context_find_alias(Context *ctx, uint64_t ref_ticks) struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); if (monitor->monitor_type == CONTEXT_MONITOR_ALIAS) { struct MonitorAlias *alias_monitor = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - if (alias_monitor->ref_data.ref_ticks == ref_ticks) { + if (alias_monitor->ref_ticks == ref_ticks) { return alias_monitor; } } @@ -1280,9 +1291,10 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori struct Monitor *monitor = GET_LIST_ENTRY(item, struct Monitor, monitor_list_head); switch (monitor->monitor_type) { case CONTEXT_MONITOR_MONITORING_LOCAL: - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *local_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); - if (local_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_monitor->ref_ticks == ref_ticks) { *is_monitoring = monitor->monitor_type == CONTEXT_MONITOR_MONITORING_LOCAL; return local_monitor->monitor_obj; } @@ -1290,7 +1302,7 @@ term context_get_monitor_pid(Context *ctx, uint64_t ref_ticks, bool *is_monitori } case CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME: { struct MonitorLocalRegisteredNameMonitor *local_registeredname_monitor = CONTAINER_OF(monitor, struct MonitorLocalRegisteredNameMonitor, monitor); - if (local_registeredname_monitor->ref_data.ref_ticks == ref_ticks) { + if (local_registeredname_monitor->ref_ticks == ref_ticks) { *is_monitoring = true; return term_from_local_process_id(local_registeredname_monitor->monitor_process_id); } @@ -1426,21 +1438,22 @@ COLD_FUNC void context_dump(Context *ctx) struct MonitorLocalMonitor *monitoring_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitor to "); term_display(stderr, monitoring_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_data.ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitoring_monitor->ref_ticks); fprintf(stderr, "\n"); break; } case CONTEXT_MONITOR_ALIAS: { struct MonitorAlias *monitor_alias = CONTAINER_OF(monitor, struct MonitorAlias, monitor); - fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_data.ref_ticks); + fprintf(stderr, "has alias ref=%lu", (long unsigned) monitor_alias->ref_ticks); fprintf(stderr, "\n"); break; } - case CONTEXT_MONITOR_MONITORED_LOCAL: { + case CONTEXT_MONITOR_MONITORED_LOCAL: + case CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS: { struct MonitorLocalMonitor *monitored_monitor = CONTAINER_OF(monitor, struct MonitorLocalMonitor, monitor); fprintf(stderr, "monitored by "); term_display(stderr, monitored_monitor->monitor_obj, ctx); - fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_data.ref_ticks); + fprintf(stderr, " ref=%lu", (long unsigned) monitored_monitor->ref_ticks); fprintf(stderr, "\n"); break; } @@ -1450,7 +1463,7 @@ COLD_FUNC void context_dump(Context *ctx) term_display(stderr, local_registeredname_monitor->monitor_name, ctx); fprintf(stderr, " ("); term_display(stderr, term_from_local_process_id(local_registeredname_monitor->monitor_process_id), ctx); - fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_data.ref_ticks); + fprintf(stderr, ") ref=%lu", (long unsigned) local_registeredname_monitor->ref_ticks); fprintf(stderr, "\n"); break; } diff --git a/src/libAtomVM/context.h b/src/libAtomVM/context.h index a4ac9ba7eb..9c33817efa 100644 --- a/src/libAtomVM/context.h +++ b/src/libAtomVM/context.h @@ -179,6 +179,7 @@ enum ContextMonitorType CONTEXT_MONITOR_LINK_REMOTE, CONTEXT_MONITOR_MONITORING_LOCAL_REGISTEREDNAME, CONTEXT_MONITOR_ALIAS, + CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS, }; typedef enum @@ -209,14 +210,14 @@ struct LinkLocalMonitor struct MonitorLocalMonitor { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; term monitor_obj; }; struct MonitorLocalRegisteredNameMonitor { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; int32_t monitor_process_id; term monitor_name; }; @@ -224,7 +225,7 @@ struct MonitorLocalRegisteredNameMonitor struct MonitorAlias { struct Monitor monitor; - RefData ref_data; + uint64_t ref_ticks; context_monitor_alias_type_t alias_type; }; @@ -541,31 +542,33 @@ struct Monitor *monitor_link_new(term link_pid); /** * @brief Create a monitor on a process. * - * @param monitor_pid monitored process - * @param ref_data reference of the monitor - * @param is_monitoring if ctx is the monitoring process + * @param monitor_pid monitored process (or monitoring process when ctx is the monitored one) + * @param ref_ticks reference of the monitor + * @param monitor_type \c CONTEXT_MONITOR_MONITORING_LOCAL for the monitoring process's half, + * \c CONTEXT_MONITOR_MONITORED_LOCAL or \c CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS (when the + * monitor was created with the {alias, _} option) for the monitored process's half * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_new(term monitor_pid, const RefData *ref_data, bool is_monitoring); +struct Monitor *monitor_new(term monitor_pid, uint64_t ref_ticks, enum ContextMonitorType monitor_type); /** * @brief Create a process alias. * - * @param ref_data reference of the alias + * @param ref_ticks reference of the alias * @param alias_type when the alias is deactivated, see the erlang:monitor/3 alias option * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_alias_new(const RefData *ref_data, context_monitor_alias_type_t alias_type); +struct Monitor *monitor_alias_new(uint64_t ref_ticks, context_monitor_alias_type_t alias_type); /** * @brief Create a monitor on a process by registered name. * * @param monitor_process_id monitored process id * @param monitor_name name of the monitor (atom) - * @param ref_data reference of the monitor + * @param ref_ticks reference of the monitor * @return the allocated monitor or NULL if allocation failed */ -struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, const RefData *ref_data); +struct Monitor *monitor_registeredname_monitor_new(int32_t monitor_process_id, term monitor_name, uint64_t ref_ticks); /** * @brief Create a resource monitor. diff --git a/src/libAtomVM/nifs.c b/src/libAtomVM/nifs.c index de05885723..6cb3e596fb 100644 --- a/src/libAtomVM/nifs.c +++ b/src/libAtomVM/nifs.c @@ -1583,7 +1583,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free } if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -1594,7 +1594,8 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = INVALID_PROCESS_ID }; } - new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), &ref_data, false); + new_monitor = monitor_new(term_from_local_process_id(ctx->process_id), ref_data.ref_ticks, + is_alias ? CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS : CONTEXT_MONITOR_MONITORED_LOCAL); if (IS_NULL_PTR(new_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -1602,7 +1603,7 @@ static term do_spawn(Context *ctx, Context *new_ctx, size_t arity, size_t n_free context_destroy(new_ctx); RAISE_ERROR(OUT_OF_MEMORY_ATOM); } - self_monitor = monitor_new(new_pid, &ref_data, true); + self_monitor = monitor_new(new_pid, ref_data.ref_ticks, CONTEXT_MONITOR_MONITORING_LOCAL); if (IS_NULL_PTR(self_monitor)) { monitor_destroy(new_link); monitor_destroy(self_link); @@ -5089,7 +5090,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) // stays active (demonitor / reply_demonitor would be deactivated right away, as at a DOWN). struct Monitor *alias_monitor = NULL; if (is_alias && alias_type == ContextMonitorAliasExplicitUnalias) { - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); } @@ -5128,7 +5129,7 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) struct Monitor *alias_monitor = NULL; if (is_alias) { ref_data = (RefData){ .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; - alias_monitor = monitor_alias_new(&ref_data, alias_type); + alias_monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(alias_monitor)) { globalcontext_get_process_unlock(ctx->global, target); RAISE_ERROR(OUT_OF_MEMORY_ATOM); @@ -5138,9 +5139,9 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) } struct Monitor *self_monitor; if (term_is_atom(target_proc)) { - self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, &ref_data); + self_monitor = monitor_registeredname_monitor_new(local_process_id, target_proc, ref_data.ref_ticks); } else { - self_monitor = monitor_new(target_pid, &ref_data, true); + self_monitor = monitor_new(target_pid, ref_data.ref_ticks, CONTEXT_MONITOR_MONITORING_LOCAL); } if (IS_NULL_PTR(self_monitor)) { globalcontext_get_process_unlock(ctx->global, target); @@ -5148,7 +5149,8 @@ static term nif_erlang_monitor(Context *ctx, int argc, term argv[]) RAISE_ERROR(OUT_OF_MEMORY_ATOM); } term monitoring_pid = term_from_local_process_id(ctx->process_id); - struct Monitor *other_monitor = monitor_new(monitoring_pid, &ref_data, false); + struct Monitor *other_monitor = monitor_new(monitoring_pid, ref_data.ref_ticks, + is_alias ? CONTEXT_MONITOR_MONITORED_LOCAL_ALIAS : CONTEXT_MONITOR_MONITORED_LOCAL); if (IS_NULL_PTR(other_monitor)) { monitor_destroy(alias_monitor); monitor_destroy(self_monitor); @@ -7639,7 +7641,7 @@ static term nif_erlang_alias(Context *ctx, int argc, term argv[]) RefData ref_data = { .ref_ticks = globalcontext_get_ref_ticks(ctx->global), .process_id = ctx->process_id }; term process_ref = term_from_ref_data(&ref_data, &ctx->heap); - struct Monitor *monitor = monitor_alias_new(&ref_data, alias_type); + struct Monitor *monitor = monitor_alias_new(ref_data.ref_ticks, alias_type); if (IS_NULL_PTR(monitor)) { RAISE_ERROR(OUT_OF_MEMORY_ATOM); }