06 08 calling convention call by copy in copy out
P4 provides multiple constructs for writing modular programs: extern methods, parsers, controls, actions. All these constructs behave similarly to procedures in standard general-purpose programming languages:
- They have named and typed parameters.
- They introduce a new local scope for parameters and local variables.
- They allow arguments to be passed by binding them to their parameters.
Invocations are executed using copy-in/copy-out semantics.
Each parameter may be labeled with a direction:
inparameters are read-only. It is an error to use aninparameter on the left-hand side of an assignment or to pass it to a callee as a non-inargument.inparameters are initialized by copying the value of the corresponding argument when the invocation is executed.outparameters are, with a few exceptions listed below, uninitialized and are treated as l-values (See Section L-values) within the body of the method or function. An argument passed as anoutparameter must be an l-value; after the execution of the call, the value of the parameter is copied to the corresponding storage location for that l-value.inoutparameters behave like a combination ofinandoutparameters simultaneously: On entry the value of the arguments is copied to the parameters. On return the value of the parameters is copied back to the arguments. In consequence, an argument passed as aninoutparameter must be an l-value.- The meaning of parameters with no direction depends upon the kind of
entity the parameter is for:
- For anything other than an action, e.g. a control, parser, or function, a directionless parameter means that the value supplied as an argument in a call must be a compile-time known value (see Section Compile-time known and local compile-time known values).
- For an action, a directionless parameter indicates that it is
“action data”. See Section Actions for the meaning
of action data, but its meaning includes the following
possibilities:
- The parameter’s value is provided in the P4 program. In this
case, the parameter behaves as if the direction were
in. Such an argument expression need not be a compile-time known value. - The parameter’s value is provided by the control plane software when an entry is added to a table that uses that action. See Section Actions.
- The parameter’s value is provided in the P4 program. In this
case, the parameter behaves as if the direction were
A directionless parameter of extern object type is passed by reference.
Direction out parameters are always initialized at the beginning of
execution of the portion of the program that has the out parameters,
e.g. control, parser, action, function, etc. This initialization
is not performed for parameters with any direction that is not out.
- If a direction
outparameter is of typeheaderorheader_union, it is set to “invalid”. - If a direction
outparameter is of type header stack, all elements of the header stack are set to “invalid”, and itsnextIndexfield is initialized to 0 (see Section Operations on header stacks). - If a direction
outparameter is a compound type, e.g. a struct or tuple, other than one of the types listed above, then apply these rules recursively to its members. - If a direction
outparameter has any other type, e.g.bit<W>, an implementation need not initialize it to any predictable value.
For example, if a direction out parameter has type s2_t named p:
header h1_t {
bit<8> f1;
bit<8> f2;
}
struct s1_t {
h1_t h1a;
bit<3> a;
bit<7> b;
}
struct s2_t {
h1_t h1b;
s1_t s1;
bit<5> c;
}
then at the beginning of execution of the part of the program that has
the out parameter p, it must be initialized so that p.h1b and and
p.s1.h1a are invalid. No other parts of p are required to be
initialized.
Arguments are evaluated from left to right prior to the invocation of the function itself. The order of evaluation is important when the expression supplied for an argument can have side-effects. Consider the following example:
extern void f(inout bit x, in bit y);
extern bit g(inout bit z);
bit a;
f(a, g(a));
Note that the evaluation of g may mutate its argument a, so the
compiler has to ensure that the value passed to f for its first
parameter is not changed by the evaluation of the second argument. The
semantics for evaluating a function call is given by the following
algorithm (implementations can be different as long as they provide the
same result):
-
Arguments are evaluated from left to right as they appear in the function call expression.
-
If a parameter has a default value and no corresponding argument is supplied, the default value is used as an argument.
-
For each
outandinoutargument the corresponding l-value is saved (so it cannot be changed by the evaluation of the following arguments). This is important if the argument contains indexing operations into a header stack. -
The value of each argument is saved into a temporary.
-
The function is invoked with the temporaries as arguments. We are guaranteed that the temporaries that are passed as arguments are never aliased to each other, so this “generated” function call can be implemented using call-by-reference if supported by the architecture.
-
On function return, the temporaries that correspond to
outorinoutarguments are copied in order from left to right into the l-values saved in Step 3.
According to this algorithm, the previous function call is equivalent to the following sequence of statements:
bit tmp1 = a; // evaluate a; save result
bit tmp2 = g(a); // evaluate g(a); save result; modifies a
f(tmp1, tmp2); // evaluate f; modifies tmp1
a = tmp1; // copy inout result back into a
To see why Step 3 in the above algorithm is important, consider the following example:
header H { bit z; }
H[2] s;
f(s[a].z, g(a));
The evaluation of this call is equivalent to the following sequence of statements:
bit tmp1 = a; // save the value of a
bit tmp2 = s[tmp1].z; // evaluate first argument
bit tmp3 = g(a); // evaluate second argument; modifies a
f(tmp2, tmp3); // evaluate f; modifies tmp2
s[tmp1].z = tmp2; // copy inout result back; dest is not s[a].z
When used as arguments, extern objects can only be passed as
directionless parameters—e.g., see the packet argument in the very
simple switch example.
Justification
The main reason for using copy-in/copy-out semantics (instead of the
more common call-by-reference semantics) is for controlling the
side-effects of extern functions and methods. extern methods and
functions are the main mechanism by which a P4 program communicates with
its environment. With copy-in/copy-out semantics extern functions
cannot hold references to P4 program objects; this enables the compiler
to limit the side-effects that extern functions may have on the P4
program both in space (they can only affect out parameters) and in
time (side-effects can only occur at function call time).
In general, extern functions are arbitrarily powerful: they can store
information in global storage, spawn separate threads, “collude” with
each other to share information — but they cannot access any variable in
a P4 program. With copy-in/copy-out semantics the compiler can still
reason about P4 programs that invoke extern functions.
There are additional benefits of using copy-in copy-out semantics:
- It enables P4 to be compiled for architectures that do not support references (e.g., where all data is allocated to named registers. Such architectures may require indices into header stacks that appear in a program to be compile-time known values.) It simplifies some compiler analyses, since function parameters can never alias to each other within the function body.
parameterList
: /* empty */
| nonEmptyParameterList
;
nonEmptyParameterList
: parameter
| nonEmptyParameterList "," parameter
;
parameter
: optAnnotations direction typeRef name
| optAnnotations direction typeRef name "=" expression
;
direction
: IN
| OUT
| INOUT
| /* empty */
;
Following is a summary of the constraints imposed by the parameter directions:
- When used as arguments, extern objects can only be passed as directionless parameters.
- All constructor parameters are evaluated at compilation-time, and in
consequence they must all be directionless (they cannot be
in,out, orinout); this applies topackage,control,parser, andexternobjects. Expressions for these parameters must be supplied at compile-time, and they must evaluate to compile-time known values. See Section Parameterization for further details. - For actions all directionless parameters must be at the end of the
parameter list. When an action appears in a
table’sactionslist, only the parameters with a direction must be bound. See Section Actions for further details. - Actions can also be explicitly invoked using function call syntax,
either from a control block or from another action. In this case,
values for all action parameters must be supplied explicitly,
including values for the directionless parameters. The directionless
parameters in this case behave like
inparameters. See Section Invoking actions for further details. - Default expressions are only allowed for ‘in’ or direction-less parameters, and the expressions supplied as defaults must be compile-time known values. If parameters with default values do not appear at the end of the list of parameters, invocations that use the default values must use named arguments, as in the following example:
extern void f(in bit a, in bit<3> b = 2, in bit<5> c);
void g()
{
f(a = 1, b = 2, c = 3); // ok
f(a = 1, c = 3); // ok, equivalent to the previous call, b uses default value
f(1, 2, 3); // ok, equivalent to the previous call
// f(1, 3); // illegal, since the parameter b is not the last in the list
}
Optional parameters
A parameter that is annotated with the @optional annotation is
optional: the user may omit the value for that parameter in an
invocation. Optional parameters can only appear for arguments of:
packages, parser types, control types, extern functions, extern methods,
and extern object constructors. Optional parameters cannot have default
values. If a procedure-like construct has both optional parameters and
default values then it can only be called using named arguments. It is
recommended, but not mandatory, for all optional parameters to be at the
end of a parameter list.
The implementation of such objects is not expressed in P4, so the meaning and implementation of optional parameters should be specified by the target architecture. For example, we can imagine a two-stage switch architecture where the second stage is optional. This could be declared as a package with an optional parameter:
package pipeline(/* parameters omitted */);
package switch(pipeline first, @optional pipeline second);
pipeline(/* arguments omitted */) ingress;
switch(ingress) main; // a switch with a single-stage pipeline
Here the target architecture could implement the elided optional argument using an empty pipeline.
The following example shows optional parameters and parameters with default values.
extern void h(in bit<32> a, in bool b = true); // default value
// function calls
h(10); // same as h(10, true);
h(a = 10); // same as h(10, true);
h(a = 10, b = true);
struct Empty {}
control nothing(inout Empty h, inout Empty m) {
apply {}
}
parser parserProto<H, M>(packet_in p, out H h, inout M m);
control controlProto<H, M>(inout H h, inout M m);
package pack<HP, MP, HC, MC>(
@optional parserProto<HP, MP> _parser, // optional parameter
controlProto<HC, MC> _control = nothing()); // default parameter value
pack() main; // No value for _parser, _control is an instance of nothing()