Search code examples
celixirerlang-nif

Proper resource handling in Elixir NIF


I am trying to implent a NIF for simple linear algebra. Here is my internal structure for matrix:

typedef struct la_matrix {
  uint rows, columns;
  double **data;
} la_matrix;

And here is a "constructor" for it:

la_result
la_matrix_constructor(la_matrix **res,
                      const uint rows,
                      const uint columns)
{
  if (rows == 0 || columns == 0)
    return dimensional_problems;

  // allocate memory for meta-structure
  *res = malloc(sizeof(la_matrix));
  if (*res == NULL)
    return null_ptr;

  // allocater memory for array of pointers to rows
  (*res)->data = malloc(rows * sizeof(double*));
  if ((*res)->data == NULL) {
    free(*res);
    return null_ptr;
  }

  //allocate memory for each row
  uint i = 0;
  bool failed = false;
  for (; i < rows; i++) {
    (*res)->data[i] = malloc(columns * sizeof(double));
    if ((*res)->data[i] == NULL) {
      failed = true;
      break;
    }
  }

  if (failed) {
    // one step back, since i-th row wasn't allocated
    i -= 1;
    for(; i < ~((uint) 0); i--)
      free((*res)->data[i]);

    free((*res)->data);
    free(*res);
    return null_ptr;
  }

  (*res)->rows    = rows;
  (*res)->columns = columns;
  
  return ok;
}

Then I have two wrappers for NIF -- one for constructor:

static ERL_NIF_TERM
nif_matrix_constructor(ErlNifEnv *env,
                       int argc,
                       const ERL_NIF_TERM *argv)
{
  uint rows, columns;
  enif_get_uint(env, argv[0], &rows);
  enif_get_uint(env, argv[1], &columns);
  
  la_matrix **mat_res = enif_alloc_resource(LA_MATRIX_TYPE, sizeof(la_matrix *));
  
  la_matrix *mat_ptr;
  la_result result = la_matrix_constructor(&mat_ptr, rows, columns);
  if (result != ok)
    return enif_make_atom(env, "err");

  memcpy((void *) mat_res, (void *) &mat_ptr, sizeof(la_matrix *));

  ERL_NIF_TERM term = enif_make_resource(env, mat_res);
  enif_release_resource(mat_res);


  return term;
}

And one to test if the constructor works correctly:

static ERL_NIF_TERM
nif_matrix_rows(ErlNifEnv *env,
                int argc,
                const ERL_NIF_TERM *argv)
{
  la_matrix *mat_ptr;
  if(!enif_get_resource(env, argv[0], LA_MATRIX_TYPE, (void **) &mat_ptr))
    return enif_make_atom(env, "err");

  return enif_make_uint(env, mat_ptr->rows);
}

It seems that the constructor wrapper works perfectly fine (I've tested it with using printf), but nif_matrix_rows returns strange results, e.g.

iex(1)> mat = LinearAlgebra.matrix(2,3)
""
iex(2)> LinearAlgebra.rows(mat)
1677732752

And directly passing LinearAlgebra.matrix(2,3) to LinearAlgebra.rows twice results in segfault:

iex(3)> LinearAlgebra.rows(LinearAlgebra.matrix(2,3))
1543520864
iex(4)> LinearAlgebra.rows(LinearAlgebra.matrix(2,3))
zsh:  (core dumped)  iex -S mix

(Note different results for the "same" matrices).

I was following Andrea Leopardi's tutorial with minor (I don't really sure if they are so) changes to fight gcc warnings. Most important, IMHO, was this part

  la_matrix *mat_ptr;
  if(!enif_get_resource(env, argv[0], LA_MATRIX_TYPE, (void **) &mat_ptr))
    return enif_make_atom(env, "err");

while Andrea Leopardi uses

db_conn_t **conn_res;
enif_get_resource(env, argv[0], DB_RES_TYPE, (void *) conn_res);

db_conn_t *conn = *conn_res;

But it looks invalid for me since, AFAIR, (void *) conn_res assumes that conn_res was initalized.

Here is an error which occurs when I use Andrea's way:

src/nif.c: In function ‘nif_matrix_rows’:
src/nif.c:72:3: warning: ‘mat_res’ is used uninitialized in this function [-Wuninitialized]
   enif_get_resource(env, argv[0], LA_MATRIX_TYPE, (void *) mat_res);

And calling LinearAlgebra.rows from iex causes segfault.

Could one tell me a proper way for handling structures in NIFs?

P.S. Sorry for C code, I've never wrote something more than bunch of helloworlds.


Solution

  • The problem was indeed in nif_matrix_rows: with my code Elixir passes a pointer to a pointer to a structure (la_matrix **) and I assumed that it would be a proper pointer.

    So, quick fix is

    static ERL_NIF_TERM
    nif_matrix_rows(ErlNifEnv *env,
                    int argc,
                    const ERL_NIF_TERM *argv)
    {
      la_matrix const **mat_res;
      if(!enif_get_resource(env, argv[0], LA_MATRIX_TYPE,(void **) &mat_res))
        return enif_make_atom(env, "err");
    
      la_matrix const *mat_ptr = *mat_res;
    
      return enif_make_uint(env, mat_ptr->rows);
    }
    

    However, I'll wait some time for more elegant solution and won't accept this answer so far.