/* -*- c-file-style: "ruby"; indent-tabs-mode: nil -*- */
/*
 *  Copyright (C) 2012-2014  Ruby-GNOME2 Project Team
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 *  MA  02110-1301  USA
 */

#include "rb-gi-private.h"

#ifdef HAVE_RUBY_THREAD_H
#  include <ruby/thread.h>
#  define RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_TYPE void *
#  define RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_VALUE NULL
#else
#  define rb_thread_call_without_gvl(func, func_data, ubf, ubf_data) \
    rb_thread_blocking_region(func, func_data, ubf, ubf_data)
#  define RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_TYPE VALUE
#  define RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_VALUE Qnil
#endif

#define RG_TARGET_NAMESPACE rb_cGIFunctionInfo
#define SELF(self) RVAL2GI_FUNCTION_INFO(self)

static VALUE RG_TARGET_NAMESPACE;
static VALUE rb_cGLibError;
static const char *callbacks_key = "gi_callbacks";
static GPtrArray *callback_finders;

GType
gi_function_info_get_type(void)
{
    static GType type = 0;
    if (type == 0) {
        type = g_boxed_type_register_static("GIFunctionInfo",
                                            (GBoxedCopyFunc)g_base_info_ref,
                                            (GBoxedFreeFunc)g_base_info_unref);
    }
    return type;
}

static VALUE
rg_symbol(VALUE self)
{
    GIFunctionInfo *info;

    info = SELF(self);
    return CSTR2RVAL(g_function_info_get_symbol(info));
}

static VALUE
rg_flags(VALUE self)
{
    GIFunctionInfo *info;

    info = SELF(self);
    return GI_FUNCTION_INFO_FLAGS2RVAL(g_function_info_get_flags(info));
}

static VALUE
rg_property(VALUE self)
{
    GIFunctionInfo *info;

    info = SELF(self);
    return GI_BASE_INFO2RVAL(g_function_info_get_property(info));
}

static VALUE
rg_vfunc(VALUE self)
{
    GIFunctionInfo *info;

    info = SELF(self);
    return GI_BASE_INFO2RVAL(g_function_info_get_vfunc(info));
}

static void
allocate_arguments(GICallableInfo *info,
                   GArray *in_args, GArray *out_args,
                   GPtrArray *args_metadata)
{
    gint i, n_args;

    n_args = g_callable_info_get_n_args(info);
    for (i = 0; i < n_args; i++) {
        GIArgument argument;
        RBGIArgMetadata *metadata;
        GIArgInfo *arg_info;
        GIDirection direction;

        memset(&argument, 0, sizeof(GIArgument));

        metadata = ALLOC(RBGIArgMetadata);
        arg_info = &(metadata->arg_info);
        g_callable_info_load_arg(info, i, arg_info);
        metadata->scope_type = g_arg_info_get_scope(arg_info);
        metadata->direction = g_arg_info_get_direction(arg_info);
        metadata->callback_p = (metadata->scope_type != GI_SCOPE_TYPE_INVALID);
        metadata->closure_p = FALSE;
        metadata->destroy_p = FALSE;
        metadata->array_p = FALSE;
        metadata->array_length_p = FALSE;
        metadata->in_arg_index = -1;
        metadata->closure_in_arg_index = -1;
        metadata->destroy_in_arg_index = -1;
        metadata->array_in_arg_index = -1;
        metadata->array_length_in_arg_index = -1;
        metadata->array_length_arg_index = -1;
        metadata->rb_arg_index = -1;
        metadata->out_arg_index = -1;

        direction = metadata->direction;
        if (direction == GI_DIRECTION_IN || direction == GI_DIRECTION_INOUT) {
            metadata->in_arg_index = in_args->len;
            g_array_append_val(in_args, argument);
        }
        if (direction == GI_DIRECTION_OUT || direction == GI_DIRECTION_INOUT) {
            metadata->out_arg_index = out_args->len;
            g_array_append_val(out_args, argument);
        }

        g_ptr_array_add(args_metadata, metadata);
    }
}

static void
fill_metadata_callback(GPtrArray *args_metadata)
{
    guint i;

    for (i = 0; i < args_metadata->len; i++) {
        RBGIArgMetadata *metadata;
        GIArgInfo *arg_info;
        gint closure_index;
        gint destroy_index;

        metadata = g_ptr_array_index(args_metadata, i);
        if (!metadata->callback_p) {
            continue;
        }

        arg_info = &(metadata->arg_info);
        closure_index = g_arg_info_get_closure(arg_info);
        if (closure_index != -1) {
            RBGIArgMetadata *closure_metadata;
            closure_metadata = g_ptr_array_index(args_metadata, closure_index);
            closure_metadata->closure_p = TRUE;
            metadata->closure_in_arg_index = closure_metadata->in_arg_index;
        }

        destroy_index = g_arg_info_get_destroy(arg_info);
        if (destroy_index != -1) {
            RBGIArgMetadata *destroy_metadata;
            destroy_metadata = g_ptr_array_index(args_metadata, destroy_index);
            destroy_metadata->destroy_p = TRUE;
            metadata->destroy_in_arg_index = destroy_metadata->in_arg_index;
        }
    }
}

static void
fill_metadata_array(GPtrArray *args_metadata)
{
    guint i;

    for (i = 0; i < args_metadata->len; i++) {
        RBGIArgMetadata *metadata;
        RBGIArgMetadata *array_length_metadata;
        GIArgInfo *arg_info;
        GITypeInfo type_info;
        gint array_length_index = -1;

        metadata = g_ptr_array_index(args_metadata, i);
        arg_info = &(metadata->arg_info);

        g_arg_info_load_type(arg_info, &type_info);
        if (g_type_info_get_tag(&type_info) != GI_TYPE_TAG_ARRAY) {
            continue;
        }
        metadata->array_p = TRUE;

        array_length_index = g_type_info_get_array_length(&type_info);
        if (array_length_index == -1) {
            continue;
        }

        array_length_metadata = g_ptr_array_index(args_metadata,
                                                  array_length_index);
        array_length_metadata->array_length_p = TRUE;
        array_length_metadata->rb_arg_index = -1;
        array_length_metadata->array_in_arg_index =
            metadata->in_arg_index;
        metadata->array_length_in_arg_index =
            array_length_metadata->in_arg_index;
        metadata->array_length_arg_index = array_length_index;
    }
}

static void
fill_metadata_rb_arg_index(GPtrArray *args_metadata)
{
    guint i;
    gint rb_arg_index = 0;

    for (i = 0; i < args_metadata->len; i++) {
        RBGIArgMetadata *metadata;

        metadata = g_ptr_array_index(args_metadata, i);

        if (metadata->closure_p) {
            continue;
        }

        if (metadata->destroy_p) {
            continue;
        }

        if (metadata->array_length_p) {
            continue;
        }

        if (metadata->in_arg_index == -1) {
            continue;
        }

        metadata->rb_arg_index = rb_arg_index;
        rb_arg_index++;
    }
}

static void
fill_metadata(GPtrArray *args_metadata)
{
    fill_metadata_callback(args_metadata);
    fill_metadata_array(args_metadata);
    fill_metadata_rb_arg_index(args_metadata);
}

static void
callback_data_guard_from_gc(RBGICallbackData *callback_data)
{
    VALUE rb_callbacks;

    rb_callbacks = rb_iv_get(RG_TARGET_NAMESPACE, callbacks_key);
    callback_data->rb_gc_guard_key = rb_class_new_instance(0, NULL, rb_cObject);
    rb_hash_aset(rb_callbacks,
                 callback_data->rb_gc_guard_key,
                 callback_data->rb_callback);
}

static void
callback_data_unguard_from_gc(RBGICallbackData *callback_data)
{
    VALUE rb_callbacks;

    rb_callbacks = rb_iv_get(RG_TARGET_NAMESPACE, callbacks_key);
    rb_hash_delete(rb_callbacks, callback_data->rb_gc_guard_key);
}

void
rb_gi_callback_data_free(RBGICallbackData *callback_data)
{
    callback_data_unguard_from_gc(callback_data);
    xfree(callback_data->metadata);
    xfree(callback_data);
}

static void
destroy_notify(gpointer data)
{
    RBGICallbackData *callback_data = data;
    rb_gi_callback_data_free(callback_data);
}

static gpointer
find_callback_function(GIArgInfo *info)
{
    guint i;
    gpointer callback = NULL;

    for (i = 0; i < callback_finders->len; i++) {
        RBGICallbackFinderFunc finder = g_ptr_array_index(callback_finders, i);
        callback = finder(info);
        if (callback) {
            break;
        }
    }

    return callback;
}

void
rb_gi_callback_register_finder(RBGICallbackFinderFunc finder)
{
    g_ptr_array_add(callback_finders, finder);
}

static gboolean
source_func_p(GIArgInfo *info)
{
    GITypeInfo type_info;
    GIBaseInfo *interface_info;
    GICallableInfo *callback_info;
    GITypeInfo return_type_info;
    GIArgInfo first_arg_info;
    GITypeInfo first_arg_type_info;

    g_arg_info_load_type(info, &type_info);
    if (g_type_info_get_tag(&type_info) != GI_TYPE_TAG_INTERFACE) {
        return FALSE;
    }

    interface_info = g_type_info_get_interface(&type_info);
    if (g_base_info_get_type(interface_info) != GI_INFO_TYPE_CALLBACK) {
        g_base_info_unref(interface_info);
        return FALSE;
    }

    callback_info = (GICallableInfo *)interface_info;
    g_callable_info_load_return_type(callback_info, &return_type_info);
    if (g_type_info_get_tag(&return_type_info) != GI_TYPE_TAG_BOOLEAN) {
        g_base_info_unref(interface_info);
        return FALSE;
    }

    if (g_callable_info_get_n_args(interface_info) != 1) {
        g_base_info_unref(interface_info);
        return FALSE;
    }

    g_callable_info_load_arg(interface_info, 0, &first_arg_info);
    g_arg_info_load_type(&first_arg_info, &first_arg_type_info);
    if (g_type_info_get_tag(&first_arg_type_info) != GI_TYPE_TAG_VOID) {
        g_base_info_unref(interface_info);
        return FALSE;
    }

    g_base_info_unref(interface_info);
    return TRUE;
}

static gboolean
source_func_callback(gpointer user_data)
{
    RBGICallbackData *callback_data = user_data;
    VALUE rb_keep;
    ID id_call;

    CONST_ID(id_call, "call");
    rb_keep = rb_funcall(callback_data->rb_callback, id_call, 0);
    if (callback_data->metadata->scope_type == GI_SCOPE_TYPE_ASYNC) {
        rb_gi_callback_data_free(callback_data);
    }
    return RVAL2CBOOL(rb_keep);
}

static gpointer
source_func_callback_finder(GIArgInfo *arg_info)
{
    if (!source_func_p(arg_info)) {
        return NULL;
    }
    return source_func_callback;
}

static void
in_callback_argument_from_ruby(RBGIArgMetadata *metadata, GArray *in_args)
{
    gpointer callback;
    GIArgInfo *arg_info;
    GIArgument *callback_argument;

    arg_info = &(metadata->arg_info);
    callback = find_callback_function(arg_info);
    if (!callback) {
        GITypeInfo type_info;
        GIBaseInfo *interface_info;
        VALUE rb_type_name;
        g_arg_info_load_type(arg_info, &type_info);
        interface_info = g_type_info_get_interface(&type_info);
        rb_type_name = CSTR2RVAL(g_base_info_get_name(interface_info));
        g_base_info_unref(interface_info);
        rb_raise(rb_eNotImpError,
                 "TODO: <%s>(%s) callback is not supported yet.",
                 RVAL2CSTR(rb_type_name),
                 g_base_info_get_name(arg_info));
    }

    callback_argument = &(g_array_index(in_args,
                                        GIArgument,
                                        metadata->in_arg_index));
    callback_argument->v_pointer = callback;

    if (metadata->closure_in_arg_index != -1) {
        RBGICallbackData *callback_data;
        GIArgument *closure_argument;

        callback_data = ALLOC(RBGICallbackData);
        callback_data->metadata = metadata;
        callback_data->rb_callback = rb_block_proc();
        callback_data_guard_from_gc(callback_data);
        closure_argument = &(g_array_index(in_args,
                                           GIArgument,
                                           metadata->closure_in_arg_index));
        closure_argument->v_pointer = callback_data;
    }

    if (metadata->destroy_in_arg_index != -1) {
        GIArgument *destroy_argument;
        destroy_argument = &(g_array_index(in_args,
                                           GIArgument,
                                           metadata->destroy_in_arg_index));
        destroy_argument->v_pointer = destroy_notify;
    }
}

static void
in_argument_from_ruby(RBGIArgMetadata *metadata, VALUE rb_arguments,
                      GArray *in_args, GPtrArray *args_metadata)
{
    if (metadata->rb_arg_index == -1) {
        return;
    }

    if (metadata->callback_p) {
        in_callback_argument_from_ruby(metadata, in_args);
    } else if (metadata->array_p) {
        GIArgument *array_argument;
        GIArgument *length_argument = NULL;
        GIArgInfo *length_arg_info = NULL;
        VALUE rb_argument = Qnil;

        if (RARRAY_LEN(rb_arguments) > metadata->rb_arg_index) {
            rb_argument = RARRAY_PTR(rb_arguments)[metadata->rb_arg_index];
        }
        array_argument = &(g_array_index(in_args,
                                         GIArgument,
                                         metadata->in_arg_index));
        if (metadata->array_length_in_arg_index != -1) {
            RBGIArgMetadata *length_metadata;
            length_argument =
                &(g_array_index(in_args,
                                GIArgument,
                                metadata->array_length_in_arg_index));
            length_metadata =
                g_ptr_array_index(args_metadata,
                                  metadata->array_length_arg_index);
            length_arg_info = &(length_metadata->arg_info);
        }
        RVAL2GI_IN_ARRAY_ARGUMENT(array_argument,
                                  length_argument,
                                  &(metadata->arg_info),
                                  length_arg_info,
                                  rb_argument);
    } else {
        GIArgument *argument;
        VALUE rb_argument = Qnil;

        if (RARRAY_LEN(rb_arguments) > metadata->rb_arg_index) {
            rb_argument = RARRAY_PTR(rb_arguments)[metadata->rb_arg_index];
        }
        argument = &(g_array_index(in_args, GIArgument, metadata->in_arg_index));
        RVAL2GI_IN_ARGUMENT(argument,
                            &(metadata->arg_info),
                            rb_argument);
    }
}

static void
out_argument_from_ruby(RBGIArgMetadata *metadata, GArray *out_args)
{
    GIArgument *argument;

    argument = &(g_array_index(out_args, GIArgument, metadata->out_arg_index));
    rb_gi_out_argument_init(argument, &(metadata->arg_info));
}

static void
arg_metadata_free(gpointer data)
{
    RBGIArgMetadata *metadata = data;
    if (metadata->scope_type == GI_SCOPE_TYPE_ASYNC ||
        metadata->scope_type == GI_SCOPE_TYPE_NOTIFIED) {
        return;
    }
    xfree(metadata);
}

static void
arguments_from_ruby(GICallableInfo *info, VALUE rb_arguments,
                    GArray *in_args, GArray *out_args,
                    GPtrArray *args_metadata)
{
    gint i, n_args;

    allocate_arguments(info, in_args, out_args, args_metadata);
    fill_metadata(args_metadata);

    n_args = g_callable_info_get_n_args(info);
    for (i = 0; i < n_args; i++) {
        RBGIArgMetadata *metadata;

        metadata = g_ptr_array_index(args_metadata, i);
        if (metadata->in_arg_index != -1) {
            in_argument_from_ruby(metadata, rb_arguments,
                                  in_args, args_metadata);
        } else {
            out_argument_from_ruby(metadata, out_args);
        }
    }
}

static VALUE
out_arguments_to_ruby(GICallableInfo *callable_info,
                      GArray *in_args, GArray *out_args,
                      GPtrArray *args_metadata)
{
    gint i, n_args;
    VALUE rb_out_args;

    rb_out_args = rb_ary_new();
    n_args = g_callable_info_get_n_args(callable_info);
    for (i = 0; i < n_args; i++) {
        RBGIArgMetadata *metadata;
        GIArgument *argument = NULL;
        VALUE rb_argument;

        metadata = g_ptr_array_index(args_metadata, i);
        if (metadata->array_length_p) {
            continue;
        }

        switch (metadata->direction) {
          case GI_DIRECTION_IN:
            break;
          case GI_DIRECTION_OUT:
            argument = &g_array_index(out_args, GIArgument,
                                      metadata->out_arg_index);
            break;
          case GI_DIRECTION_INOUT:
            argument = &g_array_index(in_args, GIArgument,
                                      metadata->in_arg_index);
            break;
          default:
            g_assert_not_reached();
            break;
        }

        if (!argument) {
            continue;
        }

        if (metadata->array_p) {
            GIArgument *length_argument = NULL;
            GIArgInfo *length_arg_info = NULL;

            if (metadata->array_length_in_arg_index != -1) {
                RBGIArgMetadata *length_metadata;
                length_metadata =
                    g_ptr_array_index(args_metadata,
                                      metadata->array_length_arg_index);
                if (length_metadata->direction == GI_DIRECTION_OUT) {
                    length_argument =
                        &g_array_index(in_args, GIArgument,
                                       length_metadata->out_arg_index);
                } else {
                    length_argument =
                        &g_array_index(in_args, GIArgument,
                                       length_metadata->in_arg_index);
                }
                length_arg_info = &(length_metadata->arg_info);
            }
            rb_argument = GI_OUT_ARRAY_ARGUMENT2RVAL(argument,
                                                     length_argument,
                                                     &(metadata->arg_info),
                                                     length_arg_info);
        } else {
            rb_argument = GI_OUT_ARGUMENT2RVAL(argument, &(metadata->arg_info));
        }
        rb_ary_push(rb_out_args, rb_argument);
    }

    if (RARRAY_LEN(rb_out_args) == 0) {
        return Qnil;
    } else {
        return rb_out_args;
    }
}

static void
arguments_init(GArray **in_args, GArray **out_args, GPtrArray **args_metadata)
{
    *in_args = g_array_new(FALSE, FALSE, sizeof(GIArgument));
    *out_args = g_array_new(FALSE, FALSE, sizeof(GIArgument));
    *args_metadata = g_ptr_array_new_with_free_func(arg_metadata_free);
}

static void
arguments_free(GArray *in_args, GArray *out_args, GPtrArray *args_metadata)
{
    guint i;

    for (i = 0; i < args_metadata->len; i++) {
        RBGIArgMetadata *metadata;
        gint in_arg_index;

        metadata = g_ptr_array_index(args_metadata, i);
        if (metadata->direction == GI_DIRECTION_IN ||
            metadata->direction == GI_DIRECTION_INOUT) {
            in_arg_index = metadata->in_arg_index;
            if (in_arg_index != -1) {
                GIArgument *argument;
                argument = &(g_array_index(in_args, GIArgument, in_arg_index));
                rb_gi_in_argument_free(argument, &(metadata->arg_info));
            }
        } else {
            GIArgument *argument;
            argument = &(g_array_index(out_args, GIArgument,
                                       metadata->out_arg_index));
            rb_gi_out_argument_fin(argument, &(metadata->arg_info));
        }
    }

    g_array_unref(in_args);
    g_array_unref(out_args);
    g_ptr_array_unref(args_metadata);
}

typedef struct {
    GIFunctionInfo *info;
    GArray *in_args;
    GArray *out_args;
    GIArgument *return_value;
    GError **error;
    gboolean succeeded;
} InvokeData;

static void
rb_gi_function_info_invoke_raw_call(InvokeData *data)
{
    data->succeeded =
        g_function_info_invoke(data->info,
                               (GIArgument *)((void *)(data->in_args->data)),
                               data->in_args->len,
                               (GIArgument *)((void *)(data->out_args->data)),
                               data->out_args->len,
                               data->return_value,
                               data->error);
}

static RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_TYPE
rb_gi_function_info_invoke_raw_call_without_gvl_body(void *user_data)
{
    InvokeData *data = (InvokeData *)user_data;

    rb_gi_function_info_invoke_raw_call(data);

    return RB_THREAD_CALL_WITHOUT_GVL_FUNC_RETURN_VALUE;
}

static gboolean
gobject_based_p(GIBaseInfo *info)
{
    GIBaseInfo *container_info;
    GIRegisteredTypeInfo *registered_type_info;

    container_info = g_base_info_get_container(info);
    if (g_base_info_get_type(container_info) != GI_INFO_TYPE_STRUCT) {
        return TRUE;
    }

    registered_type_info = (GIRegisteredTypeInfo *)container_info;
    if (g_registered_type_info_get_type_init(registered_type_info)) {
        return TRUE;
    }

    return FALSE;
}

VALUE
rb_gi_function_info_invoke_raw(GIFunctionInfo *info, VALUE rb_options,
                               GIArgument *return_value)
{
    GICallableInfo *callable_info;
    GIArgument receiver;
    GArray *in_args, *out_args;
    GPtrArray *args_metadata;
    VALUE rb_out_args = Qnil;
    gboolean succeeded;
    GError *error = NULL;
    gboolean unlock_gvl = FALSE;
    VALUE rb_receiver, rb_arguments, rb_unlock_gvl;

    if (RB_TYPE_P(rb_options, RUBY_T_ARRAY)) {
        rb_receiver = Qnil;
        rb_arguments = rb_options;
        rb_unlock_gvl = Qnil;
    } else if (NIL_P(rb_options)) {
        rb_receiver = Qnil;
        rb_arguments = rb_ary_new();
        rb_unlock_gvl = Qnil;
    } else {
        rb_options = rbg_check_hash_type(rb_options);
        rbg_scan_options(rb_options,
                         "receiver", &rb_receiver,
                         "arguments", &rb_arguments,
                         "unlock_gvl", &rb_unlock_gvl,
                         NULL);
    }

    if (NIL_P(rb_receiver)) {
        receiver.v_pointer = NULL;
    } else {
        if (gobject_based_p((GIBaseInfo *)info)) {
            receiver.v_pointer = RVAL2GOBJ(rb_receiver);
        } else {
            receiver.v_pointer = DATA_PTR(rb_receiver);
        }
    }
    rb_arguments = rbg_to_array(rb_arguments);
    if (!NIL_P(rb_unlock_gvl) && RVAL2CBOOL(rb_unlock_gvl)) {
        unlock_gvl = TRUE;
    }

    callable_info = (GICallableInfo *)info;
    arguments_init(&in_args, &out_args, &args_metadata);
    if (receiver.v_pointer) {
        g_array_append_val(in_args, receiver);
    }
    arguments_from_ruby(callable_info, rb_arguments,
                        in_args, out_args, args_metadata);
    {
        InvokeData data;
        data.info = info;
        data.in_args = in_args;
        data.out_args = out_args;
        data.return_value = return_value;
        data.error = &error;
        if (unlock_gvl) {
            rb_thread_call_without_gvl(
                rb_gi_function_info_invoke_raw_call_without_gvl_body, &data,
                NULL, NULL);
        } else {
            rb_gi_function_info_invoke_raw_call(&data);
        }
        succeeded = data.succeeded;
    }

    if (succeeded) {
        rb_out_args = out_arguments_to_ruby(callable_info,
                                            in_args, out_args,
                                            args_metadata);
    }
    arguments_free(in_args, out_args, args_metadata);
    if (!succeeded) {
        RG_RAISE_ERROR(error);
    }

    if (!NIL_P(rb_out_args) && RARRAY_LEN(rb_out_args) == 1) {
        VALUE rb_out_arg;
        rb_out_arg = RARRAY_PTR(rb_out_args)[0];
        if (rb_obj_is_kind_of(rb_out_arg, rb_eException)) {
            rb_exc_raise(rb_out_arg);
        }
    }

    return rb_out_args;
}

static VALUE
rg_invoke(VALUE self, VALUE rb_options)
{
    GIFunctionInfo *info;
    GICallableInfo *callable_info;
    GIArgument return_value;
    VALUE rb_out_args;
    VALUE rb_return_value;

    info = SELF(self);
    /* TODO: use rb_protect() */
    rb_out_args = rb_gi_function_info_invoke_raw(info,
                                                 rb_options,
                                                 &return_value);

    callable_info = (GICallableInfo *)info;
    rb_return_value = GI_RETURN_ARGUMENT2RVAL(&return_value, callable_info);

    if (NIL_P(rb_out_args)) {
        return rb_return_value;
    } else {
        GITypeInfo return_value_info;
        g_callable_info_load_return_type(callable_info, &return_value_info);
        if (g_type_info_get_tag(&return_value_info) != GI_TYPE_TAG_VOID) {
            rb_ary_unshift(rb_out_args, rb_return_value);
        }
        if (RARRAY_LEN(rb_out_args) == 1) {
            return RARRAY_PTR(rb_out_args)[0];
        } else {
            return rb_out_args;
        }
    }
}

void
rb_gi_function_info_init(VALUE rb_mGI, VALUE rb_cGICallableInfo)
{
    rb_cGLibError = rb_const_get(mGLib, rb_intern("Error"));

    RG_TARGET_NAMESPACE =
	G_DEF_CLASS_WITH_PARENT(GI_TYPE_FUNCTION_INFO, "FunctionInfo", rb_mGI,
				rb_cGICallableInfo);

    rb_iv_set(RG_TARGET_NAMESPACE, callbacks_key, rb_hash_new());

    callback_finders = g_ptr_array_new();
    rb_gi_callback_register_finder(source_func_callback_finder);

    RG_DEF_METHOD(symbol, 0);
    RG_DEF_METHOD(flags, 0);
    RG_DEF_METHOD(property, 0);
    RG_DEF_METHOD(vfunc, 0);
    RG_DEF_METHOD(invoke, 1);

    G_DEF_CLASS(G_TYPE_I_FUNCTION_INFO_FLAGS, "FunctionInfoFlags", rb_mGI);

    rb_gi_method_info_init(rb_mGI, RG_TARGET_NAMESPACE);
    rb_gi_constructor_info_init(rb_mGI, RG_TARGET_NAMESPACE);
}
