Metaprogramming Make VI — The eval Function

Metaprogramming is the ability of a program generate other programs, or even modify itself while running. This is the sixth in a series of posts exploring how to use metaprogramming in GNU make makefiles, discussing the eval function.

The eval Function

In previous posts we’ve discussed the concepts of evaluation and expansion, and ways to build lists of prerequisites from constructed variable names. Last time we discussed a way to create entirely new makefiles, programmatically, using constructed include files. That’s a very powerful capability but it has drawbacks. In this post we’ll discuss the most powerful, but also trickiest to get right, metaprogramming technique yet: GNU make’s eval function.

The eval function allows you to ask make to parse some text that you provide (as the argument to the function) as if it were a makefile. You can think of this as similar to the include directive except instead of including a separate makefile, we “include” the provided text. The eval function is evaluated using standard procedures, and the result of the expansion is always the empty string. This means that the eval function can appear essentially anywhere inside a makefile.

As a very simple example, these two lines have identical behavior:

VAR := one
$(eval VAR := one)

Of course there’s no point in doing that. You may recall, however that creating multi-line included makefiles was problematic because a multi-line macro could not be used within a recipe. Multi-line macros can be given to eval, however, and they work just as you’d expect:

define ONERULE
$(TARGET): $(PREREQUISITES)
        build $(PREREQUISITES) > $(TARGET)
endef

TARGET = t1
PREREQUISITES = p1 p2 p3
$(eval $(ONERULE))

TARGET = t2
PREREQUISITES = r1 r2 r3
$(eval $(ONERULE))

Of course ONERULE can be arbitrarily complicated… and that’s where using eval can get tricky.

Debugging the eval Function

Because the results of eval are parsed by make it can be difficult to understand exactly what is going on. You can use the --print-database (-p) option to show the results of parsing the entire makefile, but that both gives too much information and at the same time not enough detail.

A better solution is to use GNU make’s info function, which will print its argument to the terminal. The info function uses identical expansion and evaluation methods as the eval function, even expanding to the empty string just as eval does. As a result replacing the eval function with an info function, leaving everything else the same, will print out the text that will be evaluated. In the example above we could write:

define ONERULE
$(TARGET): $(PREREQUISITES)
        build $(PREREQUISITES) > $(TARGET)
endef

TARGET = t1
PREREQUISITES = p1 p2 p3
$(info $(ONERULE))
$(eval $(ONERULE))

TARGET = t2
PREREQUISITES = r1 r2 r3
$(info $(ONERULE))
$(eval $(ONERULE))

which would result in output like this:

t1: p1 p2 p3
        build p1 p2 p3 > t1
t2: r1 r2 r3
        build r1 r2 r3 > t2

You can do even more; for example using the warning function instead of info will print the makefile name and line number as well, although the formatting will be a little wonky. You could use the value function to show the “before” text as well; for example $(info '$(value ONERULE)' expands to '$(ONERULE)'). However, for most debugging sessions just adding an invocation of info will be sufficient.

How eval Evaluates

You will notice in our example above we don’t use automatic variables in the recipe of ONERULE. Now that we have a useful debugging method, we can examine how using automatic variables might change things:

define ONERULE
$(TARGET): $(PREREQUISITES)
        build $^ > $@
endef

TARGET = t1
PREREQUISITES = p1 p2 p3
$(info $(ONERULE))

The result of the info function will be:

t1: p1 p2 p3
        build  > 

Whoa, where did our arguments go? The problem is that the value passed to eval is expanded before it is evaluated; that means that all variables are expanded before the text is parsed as a makefile, when our normal immediate/deferred expansion rules come into effect. This gives a lot of flexibility, but at a cost: variables you want to be expanded before evaluation should be written as normal variables. Variables you want to be expanded during or after evaluation (as part of makefile parsing or while running recipes) must be escaped:

define ONERULE
$(TARGET): $(PREREQUISITES)
        build $$^ > $$@
endef

TARGET = t1
PREREQUISITES = p1 p2 p3
$(info $(ONERULE))

Yields:

t1: p1 p2 p3
        build $^ > $@

N-ary Evaluation

You can think of any macro in GNU make as a nullary function: a function that takes no arguments. The expanded value of the macro can be thought of as the returned value from the function. Although the macro does not take arguments in the traditional sense, it can certainly contain references to other macros; in essence, global variables.

In our examples above the ONERULE “function” contained references to “global variables” TARGET and PREREQUISITES. Thus, when we wanted to evaluate ONERULE with different target/prerequisite pairs, we had to reset the global variables before each invocation of eval. This is clearly less than ideal.

The foreach Function

There is a way in GNU make to create a macro which acts as a unary function (a function that takes a single argument): the foreach function. foreach has a single localized loop variable: thus any macro expanded in the context of a foreach function which uses the loop variable can be considered to take a single argument. We can even utilize constructed macro names to parameterize multiple variables using the single loop variable, like so:

define ONERULE
$T: $($T_PREREQUISITES)
        build $$^ > $$@
endef

TARGETS = t1 t2

t1_PREREQUISITES = p1 p2 p3
t2_PREREQUISITES = r1 r2 r3

$(foreach T,$(TARGETS),$(eval $(ONERULE)))

This is very useful, and sufficient for many uses of eval. However, more complex uses may require multiple arguments and using constructed variable names is not always convenient.

The call Function

There is another feature in GNU make that comes to our rescue here and is commonly used in conjunction with the eval function: the call function. The call function allows you to create your own n-ary user-defined function by defining a normal macro that uses special variables ($1, $2, etc.) to represent the arguments. To invoke the user-defined function you pass the name of the macro to the call function along with the values for the arguments.

A very simple example of a function that reverses its arguments might be:

REVERSE = $2 $1

When you use the call function to invoke REVERSE the first argument will be used in place of $1 and the second argument will be used in place of $2, and the resulting expansion will be to reverse them:

$ cat Makefile
REVERSE = $2 $1
all: ; @echo '$(call REVERSE,first,second)'

$ make
second first

The call function is immensely powerful in its own right and probably deserves a separate discussion; it can invoke built-in functions, other user-defined macros, and can even be recursive. However, for now we’re mainly interested in call because it can be combined with eval to produce complete metaprogramming environments.

An alternate way to write our ONERULE macro with call might be:

define ONERULE
$1: $2
        build $$^ > $$@
endef

$(eval $(call ONERULE,t1,p1 p2 p3))
$(eval $(call ONERULE,t2,r1 r2 r3))

Note how call takes the name ONERULE as the first argument, not the expansion of the variable. Further note how the arguments to call are delimited by commas, not whitespace, allowing arguments to contain whitespace. As before we need to escape the automatic variables so they won’t be expanded too early.

Many people prefer to use call even for unary functions, rather than foreach, as they prefer to use the more generic $1 rather than require a specific loop variable. Of course you could always write your foreach loop using 1 as the loop variable, as well :-).

Leave a Reply

Your email address will not be published. Required fields are marked *