%% Copyright 2026 Benoit Chesneau %% %% Licensed under the Apache License, Version 2.0 (the "License"); %% you may not use this file except in compliance with the License. %% You may obtain a copy of the License at %% %% http://www.apache.org/licenses/LICENSE-2.0 %% %% Unless required by applicable law or agreed to in writing, software %% distributed under the License is distributed on an "AS IS" BASIS, %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. %% See the License for the specific language governing permissions and %% limitations under the License. %%% @doc Callback registry for Python to Erlang function calls. %%% %%% This module manages registered Erlang functions that can be called %%% from Python code via the erlang.call() function. %%% %%% @private -module(py_callback). -behaviour(gen_server). -export([ start_link/0, init_tab/0, register/2, unregister/1, lookup/1, execute/2, register_callbacks/0 ]). %% gen_server callbacks -export([ init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2 ]). -define(TABLE, py_callbacks). %%% ============================================================================ %%% API %%% ============================================================================ start_link() -> gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). %% @doc Initialize the ETS table for callbacks. %% Called by supervisor for resilience - table survives gen_server restarts. -spec init_tab() -> ok. init_tab() -> ?TABLE = ets:new(?TABLE, [ named_table, public, set, {read_concurrency, true} ]), ok. %% @doc Register a function to be callable from Python. %% Also registers the name in the C-side registry so Python's erlang module %% __getattr__ will return an ErlangFunction wrapper for this name. -spec register(Name :: atom() | binary(), Fun :: fun((list()) -> term()) | {atom(), atom()}) -> ok. register(Name, Fun) -> NameBin = to_binary(Name), ets:insert(?TABLE, {NameBin, Fun}), %% Register name in C-side registry for Python __getattr__ py_nif:register_callback_name(NameBin), ok. %% @doc Unregister a function. %% Also unregisters the name from the C-side registry. -spec unregister(Name :: atom() | binary()) -> ok. unregister(Name) -> NameBin = to_binary(Name), ets:delete(?TABLE, NameBin), %% Unregister name from C-side registry py_nif:unregister_callback_name(NameBin), ok. %% @doc Lookup a registered function. -spec lookup(Name :: binary()) -> {ok, fun((list()) -> term()) | {atom(), atom()}} | {error, not_found}. lookup(Name) -> NameBin = to_binary(Name), case ets:lookup(?TABLE, NameBin) of [{_, Fun}] -> {ok, Fun}; [] -> {error, not_found} end. %% @doc Execute a registered function with arguments. -spec execute(Name :: binary(), Args :: list()) -> {ok, term()} | {error, term()}. execute(Name, Args) -> case lookup(Name) of {ok, Fun} when is_function(Fun, 1) -> try Result = Fun(Args), {ok, Result} catch Class:Reason:Stack -> {error, {Class, Reason, Stack}} end; {ok, {Module, Function}} -> try Result = apply(Module, Function, [Args]), {ok, Result} catch Class:Reason:Stack -> {error, {Class, Reason, Stack}} end; {error, not_found} -> {error, {not_found, Name}} end. %%% ============================================================================ %%% gen_server callbacks %%% ============================================================================ init([]) -> {ok, #{}}. handle_call(_Request, _From, State) -> {reply, {error, unknown_call}, State}. handle_cast(_Msg, State) -> {noreply, State}. handle_info(_Info, State) -> {noreply, State}. terminate(_Reason, _State) -> ok. %%% ============================================================================ %%% System Callbacks Registration %%% ============================================================================ %% @doc Register system callbacks used internally by the erlang Python module. %% This includes _whereis for process lookup. -spec register_callbacks() -> ok. register_callbacks() -> ?MODULE:register('_whereis', fun whereis_callback/1), ok. %% @doc Callback implementation for erlang.whereis(). %% Wraps erlang:whereis/1 to return the PID or undefined. -spec whereis_callback([atom()]) -> pid() | undefined. whereis_callback([Name]) when is_atom(Name) -> erlang:whereis(Name); whereis_callback(_) -> undefined. %%% ============================================================================ %%% Internal Functions %%% ============================================================================ to_binary(Term) -> py_util:to_binary(Term).