Commit 83212576 authored by Bastien Nocera's avatar Bastien Nocera
Browse files

lib: First pass at library

parent 7db6ffd2
Note: Anatole is under heavy development, and doesn't have all the features
listed actually usable. APIs might change, but the goal is having all those
features for the first public release.
# Anatole
Anatole is a library to help with embedding Lua scripts into GLib/GObject
programs and libraries. The main `AnatoleEngine` object will hide most of
Lua's C embedding quirks, presenting an API more in keeping with other
GLib APIs, such as GDBus with the use of GVariant as the data format,
and GTask as the async function support.
The library is also setup to be used as a git submodule, with the build system
integrated through the meson build system.
Note that this is not a way to access GObject-based libraries through Lua using
gobject-introspection, there's already a [project called lgi for that](https://github.com/pavouk/lgi).
## Requirements
- GLib
- Lua
Depending on the embedding application or library's needs, you could also need:
- libarchive for unzipping support
- json-glib for json decoding
- gperf for html entities encoding and decoding
- libsoup for HTTP(S) client support
## Features
project('anatole', 'c',
version: '0.0.0.0.0.0.0.1',
default_options: [
'buildtype=debugoptimized',
'c_std=gnu99',
'warning_level=1'
],
license: 'LGPL 2.1',
meson_version: '>= 0.37.0')
libdir = get_option('libdir')
glib2_required = '2.44'
grilo_required = '0.3.6'
glib2_required_info = '>= @0@'.format(glib2_required)
grilo_required_info = '>= @0@'.format(grilo_required)
# pkgconf = configuration_data()
# pkgconf.set('VERSION', plugin_version)
# pkgconfig_file = 'grilo-plugins-@0@.@1@'.format(grl_major, grl_minor)
# configure_file(input: pkgconfig_file + '.pc.in',
# output: pkgconfig_file + '.pc',
# configuration: pkgconf,
# install_dir: '@0@/pkgconfig'.format(get_option('libdir')))
gio_dep = dependency('gio-2.0', required: false)
lua_dep = dependency('lua', version: '>= 5.3.0', required: false)
if not lua_dep.found()
lua_dep = dependency('lua5.3', version: '>= 5.3.0', required: false)
if not lua_dep.found()
lua_dep = dependency('lua-5.3', version: '>= 5.3.0', required: true)
endif
endif
cdata = configuration_data()
# cdata.set('GRL_MAJOR', grl_major)
# cdata.set('GRL_MINOR', grl_minor)
cdata.set_quoted('GETTEXT_PACKAGE', meson.project_name())
# cdata.set_quoted('LOCALEDIR', localedir)
# cdata.set_quoted('VERSION', plugin_version)
gnome = import('gnome')
subdir('src')
//#include "config.h"
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
#include <gio/gio.h>
#include "anatole-error.h"
#include "anatole-engine.h"
struct _AnatoleEngine {
GObject parent_instance;
char *namespace;
GHashTable *functions;
/* Lua */
lua_State *L;
luaL_Reg *library_fn;
luaL_Reg *fn;
/* State */
gboolean script_loaded;
gboolean add_inspect;
};
typedef struct {
AnatoleNativeFunc native_func;
char *gvariant_format;
} FunctionData;
G_DEFINE_TYPE(AnatoleEngine, anatole_engine, G_TYPE_OBJECT);
enum {
PROP_0,
PROP_NAMESPACE
};
#define LUA_ENV_TABLE "_G"
#define LUA_LIBRARY_INSPECT "inspect"
#define LUA_LIBRARY_INSPECT_URI "resource:///org/gnome/anatole/library/inspect.lua"
/* From https://www.lua.org/pil/24.2.3.html */
static void
stackDump (lua_State *L)
{
int i;
int top = lua_gettop(L);
for (i = 1; i <= top; i++) { /* repeat for each level */
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING: /* strings */
printf("`%s'", lua_tostring(L, i));
break;
case LUA_TBOOLEAN: /* booleans */
printf(lua_toboolean(L, i) ? "true" : "false");
break;
case LUA_TNUMBER: /* numbers */
printf("%g", lua_tonumber(L, i));
break;
default: /* other values */
printf("%s", lua_typename(L, t));
break;
}
printf(" "); /* put a separator */
}
printf("\n"); /* end the listing */
}
static void
anatole_engine_load_safe_libs (lua_State *L)
{
/* http://www.lua.org/manual/5.3/manual.html#luaL_requiref
* http://www.lua.org/source/5.3/linit.c.html */
static const luaL_Reg loadedlibs[] = {
{"_G", luaopen_base},
/* {LUA_LOADLIBNAME, luaopen_package}, */
/* {LUA_COLIBNAME, luaopen_coroutine}, */
{LUA_TABLIBNAME, luaopen_table},
/* {LUA_IOLIBNAME, luaopen_io}, */
/* {LUA_OSLIBNAME, luaopen_os}, */
{LUA_STRLIBNAME, luaopen_string},
{LUA_MATHLIBNAME, luaopen_math},
{LUA_UTF8LIBNAME, luaopen_utf8},
{LUA_DBLIBNAME, luaopen_debug},
{NULL, NULL}
};
const luaL_Reg *lib;
for (lib = loadedlibs; lib->func; lib++) {
luaL_requiref(L, lib->name, lib->func, 1);
lua_pop(L, 1);
}
}
static void
anatole_engine_set_property (GObject *object,
guint property_id,
const GValue *value,
GParamSpec * pspec)
{
AnatoleEngine *engine = ANATOLE_ENGINE (object);
switch (property_id) {
case PROP_NAMESPACE:
engine->namespace = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
anatole_engine_get_property (GObject *object,
guint property_id,
GValue *value,
GParamSpec *pspec)
{
AnatoleEngine *engine = ANATOLE_ENGINE (object);
switch (property_id) {
case PROP_NAMESPACE:
g_value_set_string (value, engine->namespace);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
}
}
static void
function_data_free (FunctionData *func)
{
g_free (func->gvariant_format);
g_free (func);
}
static void
anatole_engine_init (AnatoleEngine *engine)
{
engine->L = luaL_newstate ();
/* Standard Lua libraries */
anatole_engine_load_safe_libs (engine->L);
engine->functions = g_hash_table_new_full (g_str_hash,
g_str_equal,
g_free,
(GDestroyNotify) function_data_free);
}
AnatoleEngine *
anatole_engine_new (const char *namespace)
{
g_return_val_if_fail (namespace != NULL, NULL);
return ANATOLE_ENGINE (g_object_new (ANATOLE_TYPE_ENGINE,
"namespace", namespace,
NULL));
}
gboolean
anatole_engine_add_function (AnatoleEngine *engine,
const char *function_name,
AnatoleNativeFunc native_func,
const char *gvariant_format)
{
FunctionData *data;
g_return_val_if_fail (engine != NULL, FALSE);
g_return_val_if_fail (function_name != NULL, FALSE);
g_return_val_if_fail (native_func != NULL, FALSE);
//FIXME is this the right check?
g_return_val_if_fail (g_variant_is_signature (gvariant_format), FALSE);
g_return_val_if_fail (g_hash_table_lookup (engine->functions, function_name) == NULL, FALSE);
data = g_new0 (FunctionData, 1);
data->native_func = native_func;
data->gvariant_format = g_strdup (gvariant_format);
g_hash_table_insert (engine->functions,
g_strdup (function_name),
data);
return TRUE;
}
/** Load library included as GResource and run it with lua_pcall.
* Caller should handle the stack aftwards.
**/
static gboolean
load_gresource_library (lua_State *L,
const gchar *uri)
{
GFile *file;
gchar *data;
gsize size;
gboolean ret = TRUE;
GError *error = NULL;
file = g_file_new_for_uri (uri);
if (!g_file_load_contents (file, NULL, &data, &size, NULL, &error)) {
g_error ("Failed to load '%s': %s", uri, error->message);
g_object_unref (file);
return FALSE;
}
g_object_unref (file);
if (luaL_dostring (L, data)) {
g_error ("Failed to load '%s': %s", uri, lua_tostring (L, -1));
ret = FALSE;
}
g_free (data);
return ret;
}
gboolean
anatole_engine_add_function_inspect (AnatoleEngine *engine)
{
g_return_val_if_fail (engine != NULL, FALSE);
g_return_val_if_fail (ANATOLE_ENGINE (engine), FALSE);
engine->add_inspect = TRUE;
return TRUE;
}
static void
add_inspect (lua_State *L)
{
/* Load inspect.lua and save object in global environment table */
lua_getglobal (L, LUA_ENV_TABLE);
if (load_gresource_library (L, LUA_LIBRARY_INSPECT_URI) &&
lua_istable (L, -1)) {
/* Top of the stack is inspect table from inspect.lua */
lua_getfield (L, -1, "inspect");
/* Top of the stack is inspect.inspect */
lua_setfield (L, -4, LUA_LIBRARY_INSPECT);
/* <namespace>.inspect points to inspect.inspect */
/* Save inspect table in LUA_ENV_TABLE */
lua_setfield (L, -2, "lua-inspect");
} else {
g_assert_not_reached ();
}
lua_pop (L, 1);
}
static int
anatole_engine_marshall (lua_State *L)
{
const char *func_name;
func_name = lua_tostring(L, lua_upvalueindex(1));
g_message ("func name: %s", func_name);
int a, b;
a = lua_tointeger (L, 1);
b = lua_tointeger (L, 2);
lua_pushinteger (L, a + b);
//FIXME marshall to gvariant, and launch something
return 1;
}
static int
luaopen_funcs (lua_State *L)
{
GList *func_names, *l;
AnatoleEngine *engine;
lua_getglobal (L, "engine-pointer");
engine = lua_touserdata(L, -1);
//lua_pop(L, 1);
lua_pushstring (L, engine->namespace);
lua_newtable (L);
func_names = g_hash_table_get_keys (engine->functions);
for (l = func_names; l != NULL; l = l->next) {
const char *func_name = l->data;
// stackDump(L);
//lua_getglobal(L, engine->namespace);
/* Add the function name as an upvalue for our unique callback */
lua_pushstring(L, func_name);
lua_pushcclosure (L, anatole_engine_marshall, 1);
//lua_pushcfunction(L, anatole_engine_marshall);
lua_setfield(L, -2, func_name);
//lua_setglobal(L, func_name);
}
g_list_free (func_names);
#if 0
if (array->len > 0) {
g_message ("add about to be reg'ed");
engine->fn = (luaL_Reg *) g_array_free (array, FALSE);
//FIXME
luaL_newlib (L, engine->fn);
}
#endif
if (engine->add_inspect)
add_inspect (engine->L);
return 1;
}
gboolean
anatole_engine_add_function_finish (AnatoleEngine *engine,
GError **error)
{
g_return_val_if_fail (engine != NULL, FALSE);
g_return_val_if_fail (ANATOLE_ENGINE (engine), FALSE);
/* Namespace library */
lua_pushlightuserdata (engine->L, engine);
lua_setglobal (engine->L, "engine-pointer");
luaL_requiref (engine->L, engine->namespace, luaopen_funcs, TRUE);
lua_pop (engine->L, 1);
//FIXME remove the engine-pointer?
//FIXME make stuff read-only
return TRUE;
}
gboolean
anatole_engine_init_script (AnatoleEngine *engine,
const char *path,
GError **error)
{
int ret;
g_return_val_if_fail (engine != NULL, FALSE);
g_return_val_if_fail (ANATOLE_ENGINE (engine), FALSE);
//FIXME load resources
/* Load the plugin */
ret = luaL_loadfile (engine->L, path);
if (ret != LUA_OK) {
g_set_error (error, ANATOLE_ERROR, ANATOLE_ERROR_FAILED,
"Failed to load '%s': %s", path, lua_tostring (engine->L, -1));
return FALSE;
}
ret = lua_pcall (engine->L, 0, 0, 0);
if (ret != LUA_OK) {
g_set_error (error, ANATOLE_ERROR, ANATOLE_ERROR_FAILED,
"Failed to run '%s': %s", path, lua_tostring (engine->L, -1));
return FALSE;
}
engine->script_loaded = TRUE;
return TRUE;
}
gboolean
anatole_engine_get_global_table (AnatoleEngine *engine,
const char *table_name,
GVariant **variant)
{
GVariant *ret = NULL;
g_return_val_if_fail (engine != NULL, FALSE);
g_return_val_if_fail (ANATOLE_ENGINE (engine), FALSE);
g_return_val_if_fail (engine->script_loaded, FALSE);
//FIXME actually fill in ret
*variant = ret;
return TRUE;
}
static void
anatole_engine_finalize (GObject *object)
{
AnatoleEngine *engine = ANATOLE_ENGINE (object);
g_clear_pointer (&engine->L, lua_close);
g_hash_table_destroy (engine->functions);
g_clear_pointer (&engine->fn, g_free);
G_OBJECT_CLASS (anatole_engine_parent_class)->finalize (object);
}
static void
anatole_engine_class_init (AnatoleEngineClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
gobject_class->finalize = anatole_engine_finalize;
gobject_class->set_property = anatole_engine_set_property;
gobject_class->get_property = anatole_engine_get_property;
g_object_class_install_property (gobject_class, PROP_NAMESPACE,
g_param_spec_string ("namespace", "Namespace", "Namespace prefix for functions",
NULL,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}
#include <glib-object.h>
#define ANATOLE_TYPE_ENGINE anatole_engine_get_type()
G_DECLARE_FINAL_TYPE(AnatoleEngine, anatole_engine, ANATOLE, ENGINE, GObject);
typedef GVariant * (* AnatoleNativeFunc) (GVariant *arguments);
GType anatole_engine_get_type (void) G_GNUC_CONST;
AnatoleEngine *anatole_engine_new (const char *namespace);
gboolean anatole_engine_init_script (AnatoleEngine *engine,
const char *path,
GError **error);
gboolean anatole_engine_add_function (AnatoleEngine *engine,
const char *function_name,
AnatoleNativeFunc native_func,
const char *gvariant_format);
gboolean anatole_engine_add_function_inspect (AnatoleEngine *engine);
gboolean anatole_engine_add_function_finish (AnatoleEngine *engine,
GError **error);
gboolean anatole_engine_get_global_table (AnatoleEngine *engine,
const char *table_name,
GVariant **variant);
#define ANATOLE_ERROR g_quark_from_static_string("anatole-error")
typedef enum {
ANATOLE_ERROR_FAILED
} AnatoleError;
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/gnome/anatole">
<file compressed="false">library/inspect.lua</file>
</gresource>
</gresources>
#include <anatole-error.h>
#include <anatole-engine.h>
local inspect ={
_VERSION = 'inspect.lua 3.0.0',
_URL = 'http://github.com/kikito/inspect.lua',
_DESCRIPTION = 'human-readable representations of tables',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
inspect.KEY = setmetatable({}, {__tostring = function() return 'inspect.KEY' end})
inspect.METATABLE = setmetatable({}, {__tostring = function() return 'inspect.METATABLE' end})
-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
if str:match('"') and not str:match("'") then
return "'" .. str .. "'"
end
return '"' .. str:gsub('"', '\\"') .. '"'
end
local controlCharsTranslation = {
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
}
local function escape(str)
local result = str:gsub("\\", "\\\\"):gsub("(%c)", controlCharsTranslation)
return result
end
local function isIdentifier(str)
return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
end
local function isSequenceKey(k, length)
return type(k) == 'number'
and 1 <= k
and k <= length
and math.floor(k) == k
end
local defaultTypeOrders = {
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
['function'] = 5, ['userdata'] = 6, ['thread'] = 7
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
-- strings and numbers are sorted numerically/alphabetically
if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
-- Two default types are compared according to the defaultTypeOrders table
if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
elseif dta then return true -- default types before custom ones
elseif dtb then return false -- custom types after default ones
end
-- custom types are sorted out alphabetically
return ta < tb
end
local function getNonSequentialKeys(t)
local keys, length = {}, #t
for k,_ in pairs(t) do
if not isSequenceKey(k, length) then table.insert(keys, k) end