Is there a way to get information of multiple specific modules?

I’m developing some features for https://github.com/tony612/protobuf-elixir/, but I need a way to know what modules are protobuf modules and even want to store more information for them

A protobuf module is defined like this

defmodule Foo do
  use Protobuf, syntax: :proto3
end

defmodule Bar do
  use Protobuf, syntax: :proto3
end

I want to get the information in runtime/compile time like

> Protobuf.all_messages
[
  {Foo, some_foo_metadata},
  {Bar, some_bar_metadata}
]

Is there any way to achieve this?

A direct way I can think of is to define a function for each protobuf module, and call them when the program starts. I can use ets or persistent_term to build “all_messages”. But I can’t find a callback like this.

I don’t know if this can be implemented in compiling time, but it seems hard because we can’t make sure every protobuf module is compiled every time.

If you want to do it dynamically, you could try :code.all_loaded(). It returns a list of tuples, the first element is an atom of each module name, the second is a string of their location. You could stringify and check the first element for a constant module string. So instead of Foo you could do FooProtobuf and check for the Protobuf string. A bit hackey, but it’d work.

As for the metadata, I am unsure.

@tony612 Last time I was thinking about really similar case. The best solution I found is to define a separate module:

defmodule Foo do
  use Protobuf, syntax: :proto3
end

defmodule Bar do
  use Protobuf, syntax: :proto3
end

defmodule Example do
  use Protobuf.Agent

  add_proto(Foo)
  add_proto(Bar)
end

Each add_proto/1 call would append value to module attribute which should be registered as accumulate. Based on such attribute use Protobuf.Agent should generate some code which would be executed on before_compile callback. Finally Example should be added to app supervisor tree.

This allows you to:

  1. Define multiple modules like Example which may be helpful in case developer do not want to have all modules from all libraries.
  2. Possibility to further generate __using__/1 macro in order to allow “extend” list of modules.
defmodule Example do
  use Protobuf.Agent
  use MyLib.ProtobufAgent
  use MyOtherLib.ProtobufAgent

  add_proto(Foo)
  add_proto(Bar)
end

Of course there are ways to work on .beam files, persistent attributes and so on, but it adds way too many magic to your code and I would personally avoid such practices.

Just for sure instead of Protobuf.Agent you may have Protobuf.ETS and so on …

The difficult thing is it’s hard to let users write add_proto for every module, especially the protobuf modules code are generated.

This is interesting. A better way could be checking if the protobuf modules have some functions like def __metadata__(:is_protobuf). And once we have all the protobuf modules, it will be easy to get all metadata, we just need to call their function, like def __meta__() do.