Is apply bad/spooky?

I want to dynamically call a function from one of many modules which all use the same behaviour. I was thinking of doing something like this:

apply(String.to_existing_atom("Elixir.MyApp.#{module_name}", :some_func, [args]))

is this bad practice, is there a potential for exploit?

Unless args represent some sort of external input that is not being properly validated, I don’t think so. But this is valid for any other function call and not specific to apply.

I might be unaware of some undocumented behavior though, so I’d wait for other opinions on the matter.

1 Like

yeah so args and module_name would be internal info

You can use Module.concat/2 as well:

MyApp
|> Module.concat(module_name)
|> apply(:some_func, [args])
2 Likes

Oh nice, that’s cool!

There are a lot of examples of using apply within Elixir itself and popular libraries, if you want to study how it can be used and if it is consistent with your usage. This is from the internal implementation of DynamicSupervisor.start_child/2

  defp start_child(m, f, a) do
    try do
      apply(m, f, a)
    catch
      kind, reason ->
        {:error, exit_reason(kind, reason, __STACKTRACE__)}
    else
      {:ok, pid, extra} when is_pid(pid) -> {:ok, pid, extra}
      {:ok, pid} when is_pid(pid) -> {:ok, pid}
      :ignore -> :ignore
      {:error, _} = error -> error
      other -> {:error, other}
    end
  end

From GenServer.whereis/1

  def whereis({:via, mod, name}) do
    case apply(mod, :whereis_name, [name]) do
      pid when is_pid(pid) -> pid
      :undefined -> nil
    end
  end

Task.start_link/1

 def start_link(fun) when is_function(fun, 0) do
    start_link(:erlang, :apply, [fun, []])
  end

Postgrex.SimpleConnection, maintained by Elixir core members, I believe this is from an implementation of a behaviour:

  def connect(_, %{state: {mod, mod_state}} = state) do
    opts =
      case Keyword.get(opts(mod), :configure) do
        {module, fun, args} -> apply(module, fun, [opts(mod) | args])
        fun when is_function(fun, 1) -> fun.(opts(mod))
        nil -> opts(mod)
      end

    case Protocol.connect(opts) do
      {:ok, protocol} ->
        state = %{state | protocol: protocol}

        with {:noreply, state, _} <- maybe_handle(mod, :handle_connect, [mod_state], state) do
          {:ok, state}
        end

      {:error, reason} ->
        if state.auto_reconnect do
          {:backoff, state.reconnect_backoff, state}
        else
          {:stop, reason, state}
        end
    end
  end

I feel like if you are using it with internal modules and functions then you are probably in the clear, but maybe there are gotchas I don’t know about. As @thiagomajesk said, if your args are unsafe then using apply probably doesn’t make a difference.

2 Likes

Thanks for the examples @msimonborg. I think my use is consistent with what you listed especially paired with String.to_existing_atom("MyApp.Context.#{module_name}") as this basically serves as an existing module lookup. I mentioned using it internally and someone much smarter than I seemed hesitant at the idea without elaborating much on why in discussion. So, just wanted to double check on best practices.

You’re welcome! Someone much smarter than me may come by and say I’ve got it wrong :slightly_smiling_face:

But I think you can point to a lot of cases in plain Elixir where we can pass in an MFA arg, e.g. all of the Task functions that take an MFA, and even the the :start option in every child spec takes an MFA (__MODULE__, :start_link, [args]), so somewhere behind the scenes there must be an apply call to make that work

2 Likes

Just because…
You could create an apply macro.

defmodule Foo do
  defmacro apply(module, function, args) do
    quote do
      unquote(module).unquote(function)(unquote_splicing(args))
    end
  end
end

require Foo
Foo.apply(IO, :puts, ["hello"])
Foo.apply(Enum, :sum, [[1, 2, 3]])
5 Likes

apply is not bad, but I think it is possible to avoid it in almost 90% of use cases.
First of all, try to use Elixir’s protocols, they’re more efficient and can be verified with dialyzer.
Second, you can specify behaviour and then use something like

module = MyModule
module.function(arg)

Because this can also provide some information for dialyzer.
Then, I’d try to use closures via fn, because they can be typed and be used with dialyzer.

And only if all suggestions above are not applicable, I’d use apply/3

4 Likes

Would it be possible to have the module variable (an atom) directly instead of module_name as a string ? For instance by writing (or generating) lookup functions like this:

defp lookup("my_mod"), do: MyApp.MyMod
defp lookup("other_mod"), do: MyApp.OtherMod

So you can just call lookup(module_name).some_func(arg0, arg1) if the length of the arguments list is always the same, or apply(module, :some_funct, args)

Also note that by calling apply(String.to_existing_atom("Elixir.MyApp.#{module_name}", :some_func, [args])) you are passing a single argument, args, since you put that variable in a list itself, whereas the variable name tends to tell that it is already the list of arguments.

3 Likes

yeah i suppose it could be done that way. are you suggesting this is better practice than using apply? my reasoning for wanting to use apply as above is to avoid having to maintain a file of lookup clauses.

If the only thing that is dynamic is the module name, you can just do:

some_module = build_me_my_fancy_module()
some_module.some_func(arg)

For instance:

defmodule Foo do
  def prepend_myapp(module), do: Module.concat(MyApp, module)
end

defmodule MyApp.Bar do
  def hello(name), do: IO.puts("Hello, #{name}!")
end
iex> Foo.prepend_myapp(Bar).hello("Cherry")
Hello, Cherry!
3 Likes

To add on this:

  • If all three are static, use ModuleName.myfun(arg1, arg2, arg3)
  • If only the module is dynamic, use module_in_a_variable.myfun(arg1, arg2, arg3)
  • If only the function name is static, use Function.capture(MyModule, function_name_in_a_variable, 3).(arg1, arg2, arg3)
  • If both the function and the module name are variable, you can still use Function.capture.
  • If the function arity (the amount of arguments) is dynamic, use apply.
5 Likes

cool, yeah i also like this idea.

Here’s a wrapper function called maybe_apply which we use to call functions in modules that may or may not be present in the current app (or even disabled in config), in which case a fallback function is called (which by default is just for error handling): bonfire_common/utils.ex at main · bonfire-networks/bonfire_common · GitHub
Feedback appreciated, as I’m sure it can be improved!

PS: you’re also able to pass a list of function names, and the first match that exists will be applied.