Module horus

Library to create standalone modules from anonymous functions.

Description

Library to create standalone modules from anonymous functions.

This module is responsible for extracting the code of an anonymous function. The goal is to be able to store the extracted function and execute it later, regardless of the availability of the initial Erlang module which declared it.

This module also provides a way for the caller to indicate forbidden operations or function calls.

This module works on assembly code to perform all checks and prepare the storable copy of a function. It uses beam_disasm:file/1 from the compiler application to extract the assembly code. After the assembly code was extracted and modified, the compiler is used again to compile the code back to an executable module.

If the anonymous function calls other functions, either in the same module or in another one, the code of the called functions is extracted and copied as well. This is to make sure the result is completely standalone.

Calls to functions from standard Erlang APIs or Horus itself are left as external calls (i.e. their code is not copied). To avoid any copies of other modules, it is possible to specify a list of modules which should not be copied. In this case, calls to functions in those modules are left unmodified.

Once the code was extracted and verified, a new module is generated as an "assembly form", ready to be compiled again to an executable module. The generated module has a single run/N exported function. This function contains the code of the extracted anonymous function.

Because this process works on the assembly code, it means that if the initial module hosting the anonymous function was compiled with Erlang version N, it will probably not compile or run on older versions of Erlang. The reason is that a newer compiler may use instructions which are unknown to older runtimes.

There is a special treatment for anonymous functions evaluated by erl_eval (e.g. in the Erlang shell). "erl_eval functions" are lambdas parsed from text and are evaluated using erl_eval.

This kind of lambdas becomes a local function in the erl_eval module.

Their assembly code isn't available in the erl_eval module. However, the abstract code (i.e. after parsing but before compilation) is available in the env. We compile that abstract code and extract the assembly from that compiled beam.

Data Types

asm()

asm() = {module(), [{atom(), arity()}], [], [#function{name = atom(), arity = byte(), entry = beam_lib:label(), code = [beam_instr()]}], label()}

The assembly form passed to the compiler.

It should be exported by the compiler application ideally.

beam_instr()

beam_instr() = atom() | tuple()

A beam assembly instruction.

debug_info()

debug_info() = #{asm := asm()}

Optional details added to the standalone function record.

They are only added if the debug_info options() is set to true.

ensure_instruction_is_permitted_fun()

ensure_instruction_is_permitted_fun() = fun((Instruction::beam_instr()) -> ok)

Function which evaluates the given instruction and returns ok if it is permitted, throws an exception otherwise.

Example:

Fun = fun
          ({jump, _})    -> ok;
          ({move, _, _}) -> ok;
          ({trim, _, _}) -> ok;
          (Unknown)      -> throw({unknown_instruction, Unknown})
      end.

horus_fun()

horus_fun() = #horus_fun{module = module(), beam = binary(), arity = arity(), literal_funs = [horus:horus_fun()], env = list(), debug_info = horus:debug_info() | undefined} | function()

The result of an extraction, as returned by to_standalone_fun/2.

It can be stored, passed between processes and Erlang nodes. To execute the extracted function, simply call exec/2 which works like erlang:apply/2.

is_standalone_fun_still_needed_fun()

is_standalone_fun_still_needed_fun() = fun((#{calls := #{Call::mfa() => true}, errors := [Error::any()]}) -> IsNeeded::boolean())

Function which evaluates if the extracted function is still relevant in the end. It returns true if it is, false otherwise.

It takes a map with the following members:

label()

label() = pos_integer()

a beam assembly label.

Other assembly instructions can jump to this label.

options()

options() = #{ensure_instruction_is_permitted => ensure_instruction_is_permitted_fun(), should_process_function => should_process_function_fun(), is_standalone_fun_still_needed => is_standalone_fun_still_needed_fun(), add_module_info => boolean(), debug_info => boolean()}

Options to tune the extraction of an anonymous function.

should_process_function_fun()

should_process_function_fun() = fun((Module::module(), Function::atom(), Arity::arity(), FromModule::module()) -> ShouldProcess::boolean())

Function which returns true if a called function should be extracted and followed, false otherwise.

Module, Function and Arity qualify the function being called.

FromModule indicates the module performing the call. This is useful to distinguish local calls (FromModule == Module) from remote calls.

Example:

Fun = fun(Module, Name, Arity, FromModule) ->
              Module =:= FromModule orelse
              erlang:function_exported(Module, Name, Arity)
      end.

Function Index

to_standalone_fun/1Extracts the given anonymous function.
to_standalone_fun/2Extracts the given anonymous function.
exec/2Executes a previously extracted anonymous function.

Function Details

to_standalone_fun/1

to_standalone_fun(Fun) -> StandaloneFun

Fun: the anonymous function to extract

returns: a standalone function record or the same anonymous function if no extraction was needed.

Extracts the given anonymous function.

This is the same as:
horus:to_standalone_fun(Fun, #{}).

to_standalone_fun/2

to_standalone_fun(Fun, Options) -> StandaloneFun

Fun: the anonymous function to extract
Options: a map of options

returns: a standalone function record or the same anonymous function if no extraction was needed.

Extracts the given anonymous function.

Example of successful extraction:
Fun = fun(Term) ->
          io:format(standard_error, "~p~n", [Term])
      end,
StandaloneFun = horus:to_standalone_fun(Fun),
 
horus:exec(StandaloneFun, [calendar:local_time()]).
Example of an error, trying to export a non-existing function:
horus:to_standalone_fun(fun not_a_module:not_a_function/0).
 
%% ** exception throw: {horus, call_to_unexported_function,
%%                             #{mfa => {not_a_module, not_a_function, 0}}}

exec/2

exec(StandaloneFun, Args) -> Ret

StandaloneFun: the extracted function as returned by to_standalone_fun/2.
Args: the list of arguments to pass to the extracted function.

returns: the return value of the extracted function.

Executes a previously extracted anonymous function.

This is the equivalent of erlang:apply/2 but it supports extracted anonymous functions.

The list of Args must match the arity of the anonymous function.

Example:
Fun = fun(Term) ->
          io:format(standard_error, "~p~n", [Term])
      end,
StandaloneFun = horus:to_standalone_fun(Fun),
 
horus:exec(StandaloneFun, [calendar:local_time()]).


Generated by EDoc