Writing simple primitives

The language of Orchids includes several primitives, but you may want to add new ones.

The main primitives of Orchids are coded in lang.c. Some others are provided by modules.  Let us look at a simple example, that of the function int_from_str, which converts a string such as "12" to the corresponding integer (12 here).

In writing a primitive, you should first decide of its type.  This is a good safeguard again bugs, and will be needed at the end of this post.  The int_from_str function should take a string (either a string or a virtual string) and return an integer.

We then start to write the primitive.  Let us name it issdl_int_from_str. We write:

static void issdl_int_from_str(orchids_t *ctx, state_instance_t *state,
                               void *data)
{
  ovm_var_t *str;
  ovm_var_t *i;

The ctx parameter holds the general Orchids context, as for most other Orchids functions, and state holds the current state of the machine.  We shall use the latter mostly to handle the machine’s stack: all primitives consume their arguments from the stack and push back the result onto the stack.  I will discuss the data parameter much later in this post, and only briefly.

The variable i will eventually hold the result. As I said, our primitive will take its arguments from the Orchids stack.  We get the value of our unique argument, from the stack, into str as follows:

   str = (ovm_var_t *)STACK_ELT(ctx->ovm_stack, 1);

Here ctx->ovm_stack is the stack of the Orchids virtual machine.  We get our arguments from the stack.  We shall also need to pop the arguments from the stack and push the result back, see below.  For now, we just read elements from the stack using the STACK_ELT macro.

In case we had several arguments, you should pay attention to the fact that STACK_ELT(ctx->ovm_stack, 1) returns the last argument.  STACK_ELT(ctx->ovm_stack, 2) returns the next-to-last argument, and so on. For example, to obtain the three arguments of a 3 argument function, you should write arg3 = (ovm_var_t *)STACK_ELT(ctx->ovm_stack, 1); arg2 = (ovm_var_t *)STACK_ELT(ctx->ovm_stack, 2); arg1 = (ovm_var_t *)STACK_ELT(ctx->ovm_stack, 3);.

There is another macro (STACK_POP) that allows you to read the element at the top of the stack and pop it at the same time.  Don’t use it unless you really know what you do.  The nice thing about STACK_ELT is that, since it keeps the element read on the stack, it won’t be freed by the garbage-collector: anything on the stack is considered in use, and won’t be freed.

 


 

Now we convert the string to an integer, using the orchids_atoi function. This works just like atoi, except the string need not be 0-terminated. Recall that Orchids strings are given with an explicit length.

Before we do so, though, we must check that str is not NULL. Indeed, any argument may be NULL, as a result of a run-time error typically (division by zero, type error, etc.)

We therefore write:

  if (str==NULL)
    {
err:
      DebugLog(DF_OVM, DS_DEBUG, "issdl_int_from_str(): param error\n");
      STACK_DROP(ctx->ovm_stack, 1);
      PUSH_VALUE(ctx, NULL);
    }

The call to the macro DebugLog is not necessary, but helps in case of bugs.

(By the way, this is not the actual code. I modified it so as to make it easier to explain how you could write it.)

Note that even though this is an error case, we still have to pop the argument from the stack, using STACK_DROP, and push a value: here, NULL, since we ran into an error.

If we had been given three arguments, we should have called STACK_DROP(ctx->ovm_stack, 3), as you might expect.

 


 

We also explicit check the type.  This is not needed, since Orchids embeds a static type-checker that guarantees that the function will only ever be called with the right type.  I will do as though we needed to check the type at run-time (this switch is anyway needed to handle the difference between real and virtual strings, I will discuss this below):

  else switch (TYPE(str))
    {
      long value;

      case T_STR:
        value = orchids_atoi(STR(str), STRLEN(str));

We now build an Orchids integer value, pop the stack, and push the resulting value:

        i = ovm_int_new(ctx->gc_ctx, value);
        STACK_DROP(ctx->ovm_stack, 1);
        PUSH_VALUE(ctx,i);
        break;

Let me stress another point.  Since the argument to int_from_str is meant to be a string, it can actually be either a real string or a virtual string, and both cases must be handled, with results that should look identical to the outside observer.  So we proceed and do the same thing for virtual strings:

      case T_VSTR:
        value = orchids_atoi(VSTR(str), VSTRLEN(str));
        i = ovm_int_new(ctx->gc_ctx, value);
        STACK_DROP(ctx->ovm_stack, 1);
        PUSH_VALUE(ctx,i);
        break;
      default:
        goto err; // No need to duplicate code
    }
}

The code of our primitive is ready.

 


 

The final step is to make sure that Orchids knows about our primitive.  We must link it into Orchids’ virtual machine.  There are several ways to do that.

  • If you are writing an Orchids module, the simplest way is probably to call register_lang_function in your module’s preconfig code. Here, write:
    register_lang_function(ctx, issdl_int_from_str, "int_from_str",
                           1, int_of_str_sigs,
                           m_unknown_1,
                           "convert a string to the integer it represents in decimal."
                           NULL);
    

    where ctx is the Orchids context, issdl_int_from_str is our function, m_unknown_1 is a function pointer that instructs Orchids about the monotonicity of the int_of_str_sigs function, "int_from_str" is the name by which we wish to call it in Orchids signatures, 1 is its expected number of arguments, int_of_str_sigs is required for static type-checking (see below), the final string is a documentation string, and the final parameter (here, NULL) will be passed as the data argument to issdl_int_from_str.  (The latter is useful, for example, if you write a new primitive in a module, and you wish to use module-specific data in your primitive: pass the relevant mod_entry_t * data to register_lang_function() instead of NULL; in the primitive, cast back its data argument to mod_entry_t *mod, and obtain your private, module-specific data as mod->config, say.)

    For the purposes of static type-checking, int_of_str_sigs is an array of possible type signatures for our function.  Our function has only one intended signature, namely it should map strings to ints.  Here is this signature, as a C declaration:

    static type_t *int_of_str_sig[] = { &t_int, &t_str };

    The structs t_int and t_str are predefined, and mean int and string (either real or virtual), respectively.  Beware that the return type comes first, followed by the list of argument types.  There should be exactly n+1 pointers in this array, where n is the arity of the function.
    Finally, we need to provide register_lang_function() with an array of possible type signatures, so the array we pass it as 5th argument is:

    static type_t **int_of_str_sigs[] = { int_of_str_sig, NULL };

    This is a NULL-terminated array of signatures. This kind of complication is needed for functions that have several signatures, such as comparison functions for example, whose array of signatures is:

    static type_t **cmp_sigs[] = {
      int_cmp_sig, uint_cmp_sig, ipv4_cmp_sig, ipv6_cmp_sig,
      str_cmp_sig, bstr_cmp_sig, ctime_cmp_sig, timeval_cmp_sig,
      float_cmp_sig, NULL
    };
    
  • If you wish to add it to the code of Orchids, it is probably simpler to add it as a new row in the issdl_function_g table of known functions, in lang.c, say:
      { issdl_int_from_str, id, "int_from_str", 1, int_of_str_sigs, m_unknown_1,
        "convert a string to the integer it represents in decimal.", NULL },
    

    The id field is unused. It was probably meant to be some sort of unique identification number, but does not serve any purpose actually.

That’s it!  Now you can use code such as $x = int_from_str($y); in your Orchids signatures.