PrevUpHomeNext

metafunction adapt

adapt is a mechanism that takes a user function with a complex signature and adapts it to be pushed to lua.

It is used via the macro PRIMER_ADAPT.

You can think of this (roughly) as a function which takes a function pointer, and yields a new function pointer of signature int (lua_State *). However, PRIMER_ADAPT happens at compile-time, and the function pointer which you give it must be a constant expression -- usually, just some specific, named function in your program. You can't pass it an "unknown" function pointer which you obtained at run-time as a function parameter or something.

Examples
Synopsis

There are two things that adapt accomplishes:

adapt handles the first one using primer::read. adapt handles the second one using primer::result.

The core object of adapt is a class template:

// adapt is what does the actual work.
// The PRIMER_ADAPT macro provides a suitable interface to it
template <typename T, T>
class adapt;

When given a function pointer f, adapt is expected to provide a static member function of signature int (lua_State *) which is named adapted.

The PRIMER_ADAPT macro is defined:

// Simplified interface to the helper structure template
// (This seems to be necessary since we can't deduce non-type template parameter
// types, at least prior to C++17.)
//
#define PRIMER_ADAPT(F) &::primer::adapt<decltype(F), (F)>::adapted

so that PRIMER_ADAPT can be used to push complex functions directly to lua, e.g.:

lua_pushcfunction(L, PRIMER_ADAPT(&my_func));
Behavior

Given a function pointer of the form,

primer::result my_func(lua_State * L, std::string foo, bool bar, int baz);

PRIMER_ADAPT(&my_func) produces a function a somewhat similar to my_func_adapted below:

primer::result my_func_helper(lua_State * L) {
  auto arg1 = primer::read<std::string>(L, 1);
  if (!arg1) { return arg1.err(); }

  auto arg2 = primer::read<bool>(L, 2);
  if (!arg2) { return arg2.err(); }

  auto arg3 = primer::read<int>(L, 3);
  if (!arg3) { return arg3.err(); }

  return my_func(L, *arg1, *arg2, *arg3);
}

int my_func_adapted(lua_State * L) {
  if (auto r = my_func_helper(L)) {
    if (r->is_return_) {
      return r->n_;
    } else {
      return lua_yield(L, r->n_);
    }
  } else {
    lua_pushstring(L, r.err_c_str());
  }
  return lua_error(L);
}

Each parameter is read from the stack in succession using primer::read. If any cannot be read, then the input is invalid and an error is returned. If all can be read, then the user function is invoked, and its result returned.

[Note] Note

An important principle of adapt is that, it always gives you the stack as it found it from lua. primer::read is not supposed to have side-effects for the stack, and adapt is not supposed to clear the stack before your function is called. So, if for some reason you need to access the actual lua values that were the inputs, rather than their C++ versions, (as occasionally happens), you can easily do that.

This is also important because lua values may be destroyed by the garbage collector once they are removed from the stack. For instance, if your callback takes a const char * parameter, or retrieves a reference to a userdata object, those pointers and references can become dangling if the stack is cleared and there are no other references to these objects. They are guaranteed to be valid at least as long as the values are on the stack.

primer::result is implemented by a simple return in the case of a return signal, by calling lua_yield in case of a yield, and by raising a lua error in case of an error. The only tricky part is making sure that it works with no leaks, whether lua is compiled as C or C++. (Technically, the above example will create a small leak upon lua_yield call. In the actual code we do something different.)

[Caution] Caution

When using the adapt mechanism, if lua is compiled as C, the user function my_func must not trigger a lua error directly -- it must return objects of the form primer::error instead.

If it does not, then a lua error triggers a call to longjmp which will toss any automatic objects on the stack without calling destructors. This will leak all of the input parameters, and technically may cause undefined behavior according to the C++ standard.

A similar situation occurs if you call lua_yield(L, n). You should instead return primer::yield{n}.

The adapt mechanism is very useful when lua is compiled as C -- it means that when writing callbacks in C++, you can easily raise errors or yield without leaking your local C++ variables, since they will always be destroyed when you return.

primer::adapt also has a trivial specialization for functions which are already lua_CFunction:

// Traditional "raw" C-style lua callbacks.
// We don't have to do any work
template <lua_CFunction target_func>
class adapt<lua_CFunction, target_func> {
public:
  static int adapted(lua_State * L) { return target_func(L); }
};
Customization

If you would like to implement a custom parameter reading / error handling mechanism, you can do that by introducing a new return type for such functions, and specializing the adapt class template for function pointers with your given return type.

Here's an example:

In this code, an exception "raise_lua_error" is defined, and a try-catch block is setup for functions which return my_int. Otherwise, it just defers to the regular adapt mechanism.

// This is a custom exception type, which is supposed to be handled by raising
// a lua error with this error message.
struct raise_lua_error : std::runtime_error {
  raise_lua_error(const std::string & str)
    : std::runtime_error(str) {}
};

// To make our own partial specialization of adapt, we make our own return type,
// separate from primer::result. Since we are throwing exceptions rather than
// returning error objects, the return can just be an int basically.
struct my_int {
  int value;
};

// Now we specialize adapt within the primer namespace
// In order to make it as simple as possible, we implement it by translating the
// `my_int + exception` return signal to primer::result, and adapt that version
// using PRIMER_ADAPT.
//
// Because PRIMER_ADAPT takes place at compile-time, this is all transparent to
// the optimizer and all of this can be inlined.
//
// Of course, you could also implement it without reference to `primer::result`
// at all, but you'd likely have to dig into the implementation details for the
// argument parsing off of the stack, or reimplement that yourself.
namespace primer {

template <typename... Args, my_int (*target_func)(lua_State *, Args...)>
class adapt<my_int (*)(lua_State *, Args...), target_func> {
  static primer::result adapt_target(lua_State * L, Args... args) {
    try {
      my_int r = target_func(L, std::forward<Args>(args)...);
      return r.value;
    } catch (raise_lua_error & e) { return primer::error{e.what()}; }
  }

public:
  // This is the `output`, that is, the function pointer which we produce.
  static int adapted(lua_State * L) { return (PRIMER_ADAPT(&adapt_target))(L); }
};

} // end namespace primer

With this specialization installed, any function passed to PRIMER_ADAPT with return value my_int gets the automatic argument parsing feature, but it now can signal lua errors by throwing an exception derived from raise_lua_error rather than in the default way that Primer does it.

my_int
reverse_palindrome(lua_State * L, std::string p) {
  auto it = p.begin();
  auto it2 = p.end() - 1;

  while (it < it2) {
    if (*it != *it2) { throw raise_lua_error("not a palidrome"); }
    ++it;
    --it2;
  }

  if (it == it2) {
    std::string s{it, p.end()};
    s += std::string{p.begin() + 1, it + 1};
    primer::push(L, s);
  } else {
    std::string s{it, p.end()};
    s += std::string{p.begin(), it};
    primer::push(L, s);
  }
  return {1};
}

void
test_adapt_example() {
  lua_raii L;

  lua_CFunction f = PRIMER_ADAPT(&reverse_palindrome);

  lua_pushcfunction(L, f);
  lua_pushstring(L, "amanapanama");
  TEST_LUA_OK(L, lua_pcall(L, 1, 1, 0));
  TEST(lua_isstring(L, 1), "expected a string");
  TEST_EQ(lua_tostring(L, 1), std::string{"panamamanap"});

  lua_pushcfunction(L, f);
  lua_insert(L, 1);
  TEST_LUA_OK(L, lua_pcall(L, 1, 1, 0));
  TEST(lua_isstring(L, 1), "expected a string");
  TEST_EQ(lua_tostring(L, 1), std::string{"amanapanama"});

  lua_pushcfunction(L, f);
  lua_pushstring(L, "abfxxxx");
  TEST(LUA_OK != lua_pcall(L, 1, 1, 0), "expected failure");
}

Your functions using the new return type would then be automatically recognized also by PRIMER_ADAPT_USERDATA and USE_LUA_CALLBACK in an api::base object, see later in the docs.

Another option for specialization is to use tag-dispatch to select different behaviors.


PrevUpHomeNext