scott2e

scott2e

How do I pass variables to Protox's __using__ macro?

Apologies, this question has been asked in numerous variations before, but it seems I haven’t found the answer I’m looking for.

I’ve found myself in a situation where I need to support multiple versions of protobuf messages. This was due to some poor decisions by the vendor in changing protobuf files for different versions of software they’re running. i.e. they’ve kept the same protobuf files between different software versions but changed some data types on existing fields in messages. I need to be able to interface with both versions of their software…

So I need to generate both sets of protobuf files but with different namespaces

I did start doing this (which works) but it’s very tedious:

defmodule MyModule do
  use Protox, files: ["./proto/v1/foo.proto"], namespace: V1
  use Protox, files: ["./proto/v2/foo.proto"], namespace: V2
  ...
  use Protox, files: ["./proto/vN/foo.proto"], namespace: VN
end

I’ve attempted this (does not work):

defmodule MyModule do
  MyOtherModule.get_files_and_namespaces()
  |> Enum.each(fn {files, namespace} ->
    use Protox, files: files, namespace: namespace
  end)
end

This responds with the usual:

variable "files" does not exist and is being expanded to "files()", please use parentheses to remove the ambiguity or change the variable name

I’ve attempted to use unquote/1

defmodule MyModule do
  MyOtherModule.get_files_and_namespaces()
  |> Enum.each(fn {files, namespace} ->
    use Protox, unquote(files: files, namespace: namespace)
  end)
end

This responds with:

== Compilation error in file lib/mymodule.ex ==
** (CompileError) nofile:4: unquote called outside quote
    (elixir 1.12.3) lib/code.ex:1036: Code.eval_quoted/3
    (protox 1.7.0) expanding macro: Protox.__using__/1
    lib/mymodule.ex:4: MyModule (module)
    (elixir 1.12.3) expanding macro: Kernel.use/2
    lib/mymodule.ex:4: MyModule (module)
    (elixir 1.12.3) expanding macro: Kernel.|>/2

The __using__/1 macro from the Protox module looks like this:

 defmacro __using__(opts) do
    {opts, _} = Code.eval_quoted(opts)

    {paths, opts} = get_paths(opts)
    {files, opts} = get_files(opts)

    {:ok, file_descriptor_set} = Protox.Protoc.run(files, paths)

    %{enums: enums, messages: messages} = Protox.Parse.parse(file_descriptor_set, opts)

    quote do
      unquote(make_external_resources(files))
      unquote(Protox.Define.define(enums, messages, opts))
    end
  end

Can anyone relieve me from my macro ignorance?

Thank-you in advance :slight_smile:

Marked As Solved

zachallaun

zachallaun

Hi Scott!

On my phone so I can’t give in depth code examples, but a few notes to hopefully nudge you in the right direction:

The important thing you remember with macros is that they run at compile time (a step called macro expansion). So in your attempt with Enum.each, the call to use is being expanded before the loop runs. As seen in the pasted using code, they then evaluate the passes options — at that point, files isn’t bound to anything because the function isn’t being run simultaneously.

So, the question now is how do we get those values at macro expansion time? The answer is macros! What you need is to define a macro that generates quoted expressions containing the use calls that you need, with the right arguments injected into the expressions. You might have something like this:

defmacro define_protox_versions do
  for {files, namespaces} <- get_files_and_namespaces() do
    quote do
      use Protox, files: unquote(files), namespace: unquote(namespace)
    end
  end
end

and then you can do:

defmodule MyModule do
  require MyOtherModule
  MyOtherModule.define_protox_versions()
end

So, now your macro is going to return a list of quoted expressions, injecting them into MyModule. Those expressions contain macros, so they will be expanded as well, but now they also contain the literal lists and namespaces that use Protox needs.

Hopefully this helps!

Also Liked

dimitarvp

dimitarvp

Not to be mistaken with the at least 20 answers I posted when on phone and they were all wrong, lol. :003:

But yeah I was about to suggest the same as @zachallaun. Try it and let us know.

zachallaun

zachallaun

Yep, this is always the fear :joy: it’s amazing how reliant I sometimes am on just being able to quickly try something out in a scratch file.

zachallaun

zachallaun

One other thing I’ll mention is this: after you get this working, you may want to consider still co-locating the files/namespaces, or maybe a shortened version (I’m not sure what your theoretical get_files_and_namespaces() is doing).

define_protox_versions(
  v1: V1,
  v2: V2,
  …
  vN: VN
)

As cool as macros are, someone (even yourself sometime later!) will have to go through multiple layers of indirection to figure out which versions are being defined. I don’t begrudge someone wanting to save some keystrokes, but explicit is often better than implicit. Also might make some things easier down the road, like when you decide that V4 was a total buggy piece of crap that absolutely shouldn’t be used — now you can just delete that version from the list instead of having to add some special exclusion somewhere. :slight_smile:

scott2e

scott2e

OMG it works!! I was stuck on this for longer than I care to admit. Thank-you :slight_smile:

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers' Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30048 115
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
977 41022 311
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 45766 226
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? https://hexdocs.pm/ecto/Ecto.Repo.h...
New
josevalim
Hi everyone, One of the features added to Elixir early on to help integration with Erlang code was the idea of overridable function defi...
New

We're in Beta

About us Mission Statement