diff --git gi/function.c gi/function.c
index 5bdaaee..5d403e8 100644
--- gi/function.c
+++ gi/function.c
@@ -35,6 +35,11 @@
 #include <jsapi.h>
 
 #include <girepository.h>
+#include <girffi.h>
+#include <sys/mman.h>
+#include <unistd.h>
+#include <errno.h>
+#include <string.h>
 
 typedef struct {
     GIFunctionInfo *info;
@@ -43,6 +48,28 @@ typedef struct {
 
 static struct JSClass gjs_function_class;
 
+typedef struct {
+    GSList *arguments; /* List of jsval, that need to be unrooted after call*/
+    GSList *invoke_infos; /* gjs_callback_invoke_info_free for each*/
+    gint arg_index;
+} GjsCallbackInfo;
+
+typedef struct {
+    JSContext *context;
+    GICallableInfo *info;
+    jsval function;
+    ffi_cif *cif;
+    gboolean need_free_arg_types;
+    ffi_closure *closure;
+    GjsCallbackInfo callback_info;
+/* We can not munmap closure in ffi_callback.
+ * This memory page will be used after it.
+ * So we need to know when GjsCallbackInvokeInfo becoming unneeded.*/
+    gboolean in_use;
+} GjsCallbackInvokeInfo;
+
+static void gjs_callback_invoke_info_free_content(gpointer user_data);
+
 GJS_DEFINE_PRIV_FROM_JS(Function, gjs_function_class)
 
 /*
@@ -83,6 +110,343 @@ function_new_resolve(JSContext *context,
     return JS_TRUE;
 }
 
+static void
+gjs_callback_invoke_info_free(gpointer data)
+{
+    GjsCallbackInvokeInfo *invoke_info = (GjsCallbackInvokeInfo *)data;
+
+    gjs_callback_invoke_info_free_content(invoke_info);
+    if (invoke_info->info)
+        g_base_info_unref((GIBaseInfo*)invoke_info->info);
+    munmap(invoke_info->closure, sizeof(ffi_closure));
+    if (invoke_info->need_free_arg_types)
+        g_free(invoke_info->cif->arg_types);
+    g_slice_free(ffi_cif, invoke_info->cif);
+
+    g_slice_free(GjsCallbackInvokeInfo, data);
+}
+
+static void
+gjs_callback_invoke_info_free_content(gpointer user_data)
+{
+    GSList *l;
+    GjsCallbackInvokeInfo *invoke_info = (GjsCallbackInvokeInfo*)user_data;
+
+    for (l = invoke_info->callback_info.arguments; l; l = g_slist_next(l)) {
+        jsval *val = l->data;
+        JS_RemoveRoot(invoke_info->context, val);
+        g_slice_free(jsval, val);
+    }
+    g_slist_free(invoke_info->callback_info.arguments);
+    invoke_info->callback_info.arguments = NULL;
+
+    g_slist_foreach(invoke_info->callback_info.invoke_infos, (GFunc)gjs_callback_invoke_info_free, NULL);
+    g_slist_free(invoke_info->callback_info.invoke_infos);
+    invoke_info->callback_info.invoke_infos = NULL;
+}
+
+/* This is our main entry point for ffi_closure callbacks.
+ * (there's a similar one for GDestroyNotify callbacks below)
+ * ffi_prep_closure is doing pure magic and replaces the original
+ * function call with this one which gives us the ffi arguments,
+ * a place to store the return value and our use data.
+ * In other words, everything we need to call the JS function and
+ * getting the return value back.
+ */
+static void
+gjs_callback_closure(ffi_cif *cif,
+                     void *result,
+                     void **args,
+                     void *data)
+{
+    GjsCallbackInvokeInfo *invoke_info = (GjsCallbackInvokeInfo *) data;
+    int i, n_args;
+    jsval *jsargs, rval;
+    GITypeInfo *ret_type;
+
+    g_assert(invoke_info != NULL);
+
+    n_args = g_callable_info_get_n_args(invoke_info->info);
+
+    g_assert(n_args >= 0);
+
+    jsargs = (jsval*)g_new0(jsval, n_args);
+    for (i = 0; i < n_args; i++) {
+        GIArgInfo *arg_info;
+        GITypeInfo *type_info;
+
+        arg_info = g_callable_info_get_arg(invoke_info->info, i);
+        type_info = g_arg_info_get_type(arg_info);
+
+        if (g_type_info_get_tag(type_info) == GI_TYPE_TAG_VOID) {
+            jsargs[i] = *((jsval*)args[i]);
+        } else if (!gjs_value_from_g_argument(invoke_info->context,
+                                              &jsargs[i],
+                                              type_info,
+                                              args[i])) {
+            gjs_throw(invoke_info->context, "could not convert argument of callback");
+            g_free(jsargs);
+            return;
+        }
+        g_base_info_unref((GIBaseInfo*)arg_info);
+        g_base_info_unref((GIBaseInfo*)type_info);
+    }
+
+    if (!JS_CallFunctionValue(invoke_info->context,
+                              NULL,
+                              invoke_info->function,
+                              n_args,
+                              jsargs,
+                              &rval)) {
+        gjs_throw(invoke_info->context, "Couldn't call callback");
+        g_free(jsargs);
+        return;
+    }
+
+    ret_type = g_callable_info_get_return_type(invoke_info->info);
+
+    if (!gjs_value_to_g_argument(invoke_info->context,
+                                 rval,
+                                 ret_type,
+                                 "callback",
+                                 GJS_ARGUMENT_RETURN_VALUE,
+                                 FALSE,
+                                 TRUE,
+                                 result)) {
+        gjs_throw(invoke_info->context, "Couldn't convert res value");
+        g_base_info_unref((GIBaseInfo*)ret_type);
+        g_free(jsargs);
+        result = NULL;
+        return;
+    }
+    g_base_info_unref((GIBaseInfo*)ret_type);
+    g_free(jsargs);
+
+    gjs_callback_invoke_info_free_content(invoke_info);
+    invoke_info->in_use = FALSE;
+}
+
+/* For GI_SCOPE_TYPE_NOTIFIED callback we provide our own
+ * ffi closure so we can easily get the user data instead
+ * of always having to map the user_data argument on the
+ * invokation side.
+ */
+static void
+gjs_destroy_notify_callback_closure(ffi_cif *cif,
+                                    void *result,
+                                    void **args,
+                                    void *data)
+{
+    GjsCallbackInvokeInfo *info = (GjsCallbackInvokeInfo *)data;
+
+    info->in_use = FALSE;
+
+    gjs_callback_invoke_info_free_content(info);
+}
+
+static gboolean
+gjs_destroy_notify_create(GjsCallbackInvokeInfo *info)
+{
+    static const ffi_type *destroy_notify_args[] = {&ffi_type_pointer};
+    ffi_status status;
+
+    g_assert(info);
+
+    info->need_free_arg_types = FALSE;
+
+    info->closure = mmap(NULL, sizeof (ffi_closure),
+                         PROT_EXEC | PROT_READ | PROT_WRITE,
+                         MAP_ANON | MAP_PRIVATE, -1, sysconf (_SC_PAGE_SIZE));
+    if (!info->closure) {
+        gjs_throw(info->context, "mmap failed\n");
+        return FALSE;
+    }
+
+    info->cif = g_slice_new(ffi_cif);
+
+    status = ffi_prep_cif(info->cif, FFI_DEFAULT_ABI,
+                          1, &ffi_type_void,
+                          (ffi_type**)destroy_notify_args);
+    if (status != FFI_OK) {
+        gjs_throw(info->context, "ffi_prep_cif failed: %d\n", status);
+        munmap(info->closure, sizeof (ffi_closure));
+        return FALSE;
+    }
+
+    status = ffi_prep_closure(info->closure, info->cif,
+                              gjs_destroy_notify_callback_closure, info);
+    if (status != FFI_OK) {
+        gjs_throw(info->context, "ffi_prep_cif failed: %d\n", status);
+        munmap(info->closure, sizeof (ffi_closure));
+        return FALSE;
+    }
+
+    if (mprotect(info->closure, sizeof (ffi_closure), PROT_READ | PROT_EXEC) == -1) {
+        gjs_throw(info->context, "ffi_prep_closure failed: %s\n", strerror(errno));
+        munmap(info->closure, sizeof (ffi_closure));
+        return FALSE;
+    }
+
+    info->in_use = TRUE;
+
+    return TRUE;
+}
+
+static GjsCallbackInvokeInfo*
+gjs_callback_invoke_prepare(JSContext      *context,
+                            jsval           function,
+                            GICallableInfo *callable_info)
+{
+    GjsCallbackInvokeInfo *invoke_info;
+
+    g_assert(JS_TypeOfValue(context, function) == JSTYPE_FUNCTION);
+
+    invoke_info = g_slice_new0(GjsCallbackInvokeInfo);
+    invoke_info->context = context;
+    invoke_info->info = callable_info;
+
+    g_base_info_ref((GIBaseInfo*)invoke_info->info);
+    invoke_info->need_free_arg_types = TRUE;
+    invoke_info->function = function;
+    invoke_info->cif = g_slice_new0(ffi_cif);
+    invoke_info->in_use = TRUE;
+    invoke_info->callback_info.arguments = NULL;
+    invoke_info->callback_info.invoke_infos = NULL;
+    invoke_info->closure = g_callable_info_prepare_closure(callable_info, invoke_info->cif,
+                                                           gjs_callback_closure, invoke_info);
+
+    g_assert(invoke_info->closure);
+
+    return invoke_info;
+}
+
+static void
+gjs_callback_info_free (gpointer data)
+{
+    g_slice_free(GjsCallbackInfo, data);
+}
+
+static inline void
+gjs_callback_info_add_argument(JSContext       *context,
+                               GjsCallbackInfo *callback_info,
+                               jsval            arg)
+{
+    jsval *v;
+    /*JS_AddRoot/JS_RemoveRoot pair require same pointer*/
+    v = g_slice_new(jsval);
+    *v = arg;
+    JS_AddRoot(context, v);
+    callback_info->arguments = g_slist_prepend(callback_info->arguments, v);
+}
+
+static inline gboolean
+gjs_callback_from_arguments(JSContext *context,
+                            GIBaseInfo* interface_info,
+                            GIArgInfo *arg_info,
+                            int current_arg_pos,
+                            int n_args,
+                            int *argv_pos,
+                            jsval *argv,
+                            GSList **all_invoke_infos,
+                            GSList **data_for_notify,
+                            GSList **call_free_list,
+                            gpointer *closure)
+{
+    GjsCallbackInvokeInfo *invoke_info;
+    GSList *l;
+    gboolean is_notify = FALSE;
+    GjsCallbackInfo *callback_info = NULL;
+    int arg_n;
+
+    for (l = *data_for_notify; l; l = l->next) {
+        GjsCallbackInfo *callback_info = l->data;
+        if (callback_info->arg_index != current_arg_pos)
+            continue;
+
+        invoke_info = g_slice_new0(GjsCallbackInvokeInfo);
+        invoke_info->context = context;
+
+        if (!gjs_destroy_notify_create(invoke_info)) {
+            g_slice_free(GjsCallbackInvokeInfo, invoke_info);
+            return FALSE;
+        }
+
+        gjs_callback_info_add_argument(context, callback_info, argv[*argv_pos]);
+        (*argv_pos)--;
+        is_notify = TRUE;
+        invoke_info->callback_info = *callback_info;
+        *all_invoke_infos = g_slist_prepend(*all_invoke_infos, invoke_info);
+        break;
+    }
+
+    if (is_notify)
+        goto out;
+
+    invoke_info = gjs_callback_invoke_prepare(context,
+                                              argv[*argv_pos],
+                                              (GICallableInfo*)interface_info);
+
+    switch (g_arg_info_get_scope(arg_info)) {
+        case GI_SCOPE_TYPE_CALL:
+            *call_free_list = g_slist_prepend(*call_free_list, invoke_info);
+            break;
+        case GI_SCOPE_TYPE_NOTIFIED:
+            g_assert(callback_info == NULL);
+            callback_info = g_slice_new0(GjsCallbackInfo);
+
+            g_assert(g_arg_info_get_destroy(arg_info) < n_args);
+
+            gjs_callback_info_add_argument(context, callback_info, argv[*argv_pos]);
+            arg_n = g_arg_info_get_closure(arg_info);
+            if (arg_n > current_arg_pos && arg_n < n_args) {
+                gjs_callback_info_add_argument(context, callback_info, argv[arg_n]);
+            }
+            callback_info->arg_index = g_arg_info_get_destroy(arg_info);
+
+            callback_info->invoke_infos = g_slist_prepend(callback_info->invoke_infos, invoke_info);
+            *data_for_notify = g_slist_prepend(*data_for_notify, callback_info);
+            break;
+        case GI_SCOPE_TYPE_ASYNC:
+            gjs_callback_info_add_argument(context, &invoke_info->callback_info, argv[*argv_pos]);
+
+            arg_n = g_arg_info_get_closure(arg_info);
+            if (arg_n > current_arg_pos && arg_n < n_args) {
+                gjs_callback_info_add_argument(context, &invoke_info->callback_info, argv[arg_n]);
+            }
+            *all_invoke_infos = g_slist_prepend(*all_invoke_infos, invoke_info);
+            break;
+        default:
+            gjs_throw(context, "Unknown callback scope");
+            gjs_callback_invoke_info_free(invoke_info);
+            return FALSE;
+    }
+
+out:
+    *closure = invoke_info->closure;
+
+    return TRUE;
+}
+
+static void
+gjs_free_unused_invoke_infos(GSList **invoke_infos)
+{
+    GSList *l, *node_for_delete = NULL;
+
+    for (l = *invoke_infos; l; l = l->next) {
+        GjsCallbackInvokeInfo *info = l->data;
+
+         if (info->in_use)
+             continue;
+
+         gjs_callback_invoke_info_free(info);
+         node_for_delete = g_slist_prepend(node_for_delete, l);
+    }
+    for (l = node_for_delete; l; l = l->next) {
+        *invoke_infos = g_slist_delete_link(*invoke_infos, l->data);
+     }
+    g_slist_free(node_for_delete);
+}
+
 JSBool
 gjs_invoke_c_function(JSContext      *context,
                       GIFunctionInfo *info,
@@ -100,6 +464,7 @@ gjs_invoke_c_function(JSContext      *context,
     int expected_out_argc;
     int i;
     int argv_pos;
+    int destroy_notify_argc = 0;
     int in_args_pos;
     int out_args_pos;
     GError *error;
@@ -112,6 +477,20 @@ gjs_invoke_c_function(JSContext      *context,
     jsval *return_values;
     int n_return_values;
     int next_rval;
+    GSList *call_free_list = NULL; /* list of GjsCallbackInvokeInfo* */
+    GSList *data_for_notify = NULL; /* list of GjsCallbackInfo* */
+    GSList *callback_arg_indices = NULL; /* list of int */
+    static GSList *invoke_infos = NULL;
+
+    /* For async/notify callbacks we need to clean up some of the closure
+     * data in follow up calls due to the way mmap works. ffi closures works
+     * by rewriting the function trampolines, eg it modifies the page where
+     * the function is mapped, that page cannot be released/mumapped as long
+     * as we're currently using it, eg executing it. And as soon as we leave
+     * that function we loose control, it goes back into the control of the
+     * library where the function is called.
+     */
+    gjs_free_unused_invoke_infos(&invoke_infos);
 
     flags = g_function_info_get_flags(info);
     is_method = (flags & GI_FUNCTION_IS_METHOD) != 0;
@@ -123,9 +502,17 @@ gjs_invoke_c_function(JSContext      *context,
     for (i = 0; i < n_args; i++) {
         GIDirection direction;
         GIArgInfo *arg_info;
+        gint destroy;
 
         arg_info = g_callable_info_get_arg( (GICallableInfo*) info, i);
+        destroy = g_arg_info_get_destroy(arg_info);
         direction = g_arg_info_get_direction(arg_info);
+
+        if (destroy > 0 && destroy < n_args) {
+            expected_in_argc --;
+            destroy_notify_argc++;
+        }
+
         if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT)
             expected_in_argc += 1;
         if (direction == GI_DIRECTION_OUT || direction == GI_DIRECTION_INOUT)
@@ -145,7 +532,7 @@ gjs_invoke_c_function(JSContext      *context,
     /* We allow too many args; convenient for re-using a function as a callback.
      * But we don't allow too few args, since that would break.
      */
-    if (argc < (unsigned) expected_in_argc) {
+    if (argc + destroy_notify_argc < (unsigned) expected_in_argc) {
         gjs_throw(context, "Too few arguments to %s %s.%s expected %d got %d",
                   is_method ? "method" : "function",
                   g_base_info_get_namespace( (GIBaseInfo*) info),
@@ -203,11 +590,55 @@ gjs_invoke_c_function(JSContext      *context,
 
         if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT) {
             GArgument in_value;
+            GITypeTag type_tag;
+            GITypeInfo *ainfo;
+            gboolean convert_argument = TRUE;
+
+            ainfo = g_arg_info_get_type (arg_info);
+            type_tag = g_type_info_get_tag(ainfo);
+
+            if (g_slist_find(callback_arg_indices, GINT_TO_POINTER(i)) != NULL) {
+                g_assert(type_tag == GI_TYPE_TAG_VOID);
+                convert_argument = FALSE;
+                in_value.v_pointer = (gpointer)argv[argv_pos];
+            } else if (type_tag == GI_TYPE_TAG_VOID) {
+                /* FIXME: notify/throw saying the callback annotation is wrong */
+                convert_argument = FALSE;
+                in_value.v_pointer = (gpointer)argv[argv_pos];
+            }
 
-            if (!gjs_value_to_arg(context, argv[argv_pos], arg_info,
-                                  &in_value)) {
-                failed = TRUE;
+            if (type_tag == GI_TYPE_TAG_INTERFACE) {
+                GIBaseInfo* interface_info;
+                GIInfoType interface_type;
+
+                interface_info = g_type_info_get_interface(ainfo);
+
+                g_assert(interface_info != NULL);
+
+                interface_type = g_base_info_get_type(interface_info);
+                if (interface_type == GI_INFO_TYPE_CALLBACK) {
+                    if (!gjs_callback_from_arguments(context, interface_info, arg_info,
+                                                     i, n_args, &argv_pos, argv,
+                                                     &invoke_infos,
+                                                     &data_for_notify, &call_free_list,
+                                                     &(in_value.v_pointer))) {
+                        failed = TRUE;
+                        break;
+                    }
+                    callback_arg_indices = g_slist_prepend(callback_arg_indices,
+                                                           GINT_TO_POINTER(g_arg_info_get_closure(arg_info)));
+                    convert_argument = FALSE;
+                }
+
+                g_base_info_unref(interface_info);
             }
+            g_base_info_unref((GIBaseInfo*)ainfo);
+
+            if (convert_argument)
+                if (!gjs_value_to_arg(context, argv[argv_pos], arg_info,
+                                  &in_value)) {
+                    failed = TRUE;
+                }
 
             ++argv_pos;
 
@@ -233,12 +664,12 @@ gjs_invoke_c_function(JSContext      *context,
     if (failed)
         return JS_FALSE;
 
-    g_assert(in_args_pos == expected_in_argc);
+    g_assert(in_args_pos == expected_in_argc + destroy_notify_argc);
     g_assert(out_args_pos == expected_out_argc);
 
     error = NULL;
     invoke_ok = g_function_info_invoke( (GIFunctionInfo*) info,
-                                        in_args, expected_in_argc,
+                                        in_args, expected_in_argc + destroy_notify_argc,
                                         out_args, expected_out_argc,
                                         &return_arg,
                                         &error);
@@ -248,6 +679,12 @@ gjs_invoke_c_function(JSContext      *context,
      */
     failed = FALSE;
 
+    g_slist_foreach(call_free_list, (GFunc)gjs_callback_invoke_info_free, NULL);
+    g_slist_free(call_free_list);
+    g_slist_foreach(data_for_notify, (GFunc)gjs_callback_info_free, NULL);
+    g_slist_free(data_for_notify);
+    g_slist_free(callback_arg_indices);
+
     return_info = g_callable_info_get_return_type( (GICallableInfo*) info);
     g_assert(return_info != NULL);
 
@@ -305,7 +742,7 @@ gjs_invoke_c_function(JSContext      *context,
         arg_type_info = g_arg_info_get_type(arg_info);
 
         if (direction == GI_DIRECTION_IN) {
-            g_assert(in_args_pos < expected_in_argc);
+            g_assert(in_args_pos < expected_in_argc + destroy_notify_argc);
 
             if (!gjs_g_argument_release_in_arg(context,
                                                g_arg_info_get_ownership_transfer(arg_info),
@@ -351,7 +788,7 @@ gjs_invoke_c_function(JSContext      *context,
 
     g_assert(next_rval == n_return_values);
     g_assert(out_args_pos == expected_out_argc);
-    g_assert(in_args_pos == expected_in_argc);
+    g_assert(in_args_pos == expected_in_argc + destroy_notify_argc);
 
     if (invoke_ok && n_return_values > 0) {
         /* if we have 1 return value or out arg, return that item
diff --git test/js/testEverythingBasic.js test/js/testEverythingBasic.js
index 875c525..755ad45 100644
--- test/js/testEverythingBasic.js
+++ test/js/testEverythingBasic.js
@@ -185,6 +185,43 @@ function testClosureOneArg() {
     assertEquals('callback with one arg return value', 42, i);
 }
 
+function testCallback() {
+    let callback = function() {
+                       return 42;
+                   };
+    assertEquals('Callback', Everything.test_callback(callback), 42);
+}
+
+function testCallbackUserData() {
+    let callbackUserData = function(userData) {
+                               return userData.foo.length;
+                           };
+
+    let userData = {'foo': 'bar'};
+    assertEquals('CallbackUserData', Everything.test_callback_user_data(
+                                     callbackUserData, userData), userData.foo.length);
+}
+
+function testCallbackDestroyNotify() {
+    let called = 0;
+    let test = function(userData) {
+                   called++;
+                   return userData;
+               };
+    assertEquals('CallbackDestroyNotify', Everything.test_callback_destroy_notify(test, 42), 42);
+    assertEquals('CallbackDestroyNotify', called, 1);
+    assertEquals('CallbackDestroyNotify', Everything.test_callback_thaw_notifications(), 42);
+}
+
+function testCallbackAsync() {
+    let test = function(userData) {
+                   return 44;
+               };
+    Everything.test_callback_async(test, 44);
+    let i = Everything.test_callback_thaw_async();
+    assertEquals('testCallbackAsyncFinish', 44, i);
+}
+
 function testIntValueArg() {
     let i = Everything.test_int_value_arg(42);
     assertEquals('Method taking a GValue', 42, i);
