aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorthe lemons <citrons@mondecitronne.com>2022-12-04 04:14:22 -0600
committerthe lemons <citrons@mondecitronne.com>2022-12-04 04:14:22 -0600
commit8e4af6b2c8602bfb7fd086375d741345947494aa (patch)
tree99cfa055853df745c7bbb486b3dcb6bacf7b9d14
initial
-rw-r--r--Makefile16
-rw-r--r--README.md49
-rw-r--r--lmdb.c343
3 files changed, 408 insertions, 0 deletions
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4b4860a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,16 @@
+LUA_VER=5.3
+CC = cc
+LD = ld
+CFLAGS = -std=c99 -Wall -O2 -fPIC -I/usr/include/lua$(LUA_VER)
+LFLAGS = -shared -llmdb
+
+lmdb.so: lmdb.o
+ $(LD) $(LFLAGS) -o $@ $<
+
+lmdb.o: lmdb.c
+ $(CC) $(CFLAGS) -c $< -o $@
+
+.PHONY: clean
+clean:
+ rm -rf lmdb.o
+ rm -rf lmdb.so
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..71b0e62
--- /dev/null
+++ b/README.md
@@ -0,0 +1,49 @@
+# [LMDB](http://www.lmdb.tech/doc/index.html) bindings for lua
+license: MIT
+
+## errors
+operations that can fail will return `nil, error, errno` on failure.
+
+## functions
+### `lmdb.open(path, opt)`
+create a new environment handle and open the database at the `path`. returns the environment handle (`env`) on success.
+
+`opt` is an optional table of options:
+
+* `maxdbs`: number: the maximum number of databases in the environment. must be set if multiple databases will be used.
+* `rdonly`: boolean: open the environment in read-only mode.
+* `nosubdir`: boolean: the `path` is used for the database file itself and not its containing directory.
+* `nosync`: boolean: don't flush buffers to disk when committing.
+* `nolock`: boolean: turn off locking. the application must manage all concurrency.
+
+### `lmdb.version()`
+returns the LMDB version as `major, minor, patch`.
+
+### `env:txn_begin(rdonly)`
+create a new transaction in the environment. if `rdonly` is true, then the transaction is not to be used to perform write operation. returns the transaction handle (`txn`) on success.
+
+### `env:copy(path)`
+copy the environment to the specified `path`. return `true` on success.
+
+### `env:sync(force)`
+flush buffers to disk. if `force` is `true`, a synchronous flush is performed even if `nosync` is set. return `true` on success.
+
+### `env:close()`
+close the environment.
+
+### `txn:open(name, create)`
+open a database in the environment. if multiple databases are to be used in the environment, `name` is the name of the database to open. otherwise, it should not be supplied. if `create` is true, the database is created if it does not exist. returns a database handle on success.
+
+the database handle behaves as a table that may be indexed with string keys and contains string values, read from the database. any changes made will be saved to the database when the transaction is committed.
+
+## `txn:drop(name)`
+delete the database `name` from the environment. (or clear the database, if `name` is not supplied)
+
+### `txn:abort()`
+abandon all operations performed in the transaction.
+
+### `txn:commit()`
+commit all operations of the transaction into the database.
+
+### `txn:txn_begin(rdonly)`
+create a nested transaction in the parent transaction.
diff --git a/lmdb.c b/lmdb.c
new file mode 100644
index 0000000..1a2c27a
--- /dev/null
+++ b/lmdb.c
@@ -0,0 +1,343 @@
+#include <stdlib.h>
+#include <stdbool.h>
+#include <lmdb.h>
+#include <lua.h>
+#include <lauxlib.h>
+#include <assert.h>
+
+struct handle {
+ bool closed;
+ bool has_child;
+ void *obj;
+};
+
+#define reterr(e) \
+ do { \
+ int __res = (e); if (__res != 0) { \
+ lua_pushnil(L); \
+ lua_pushstring(L, mdb_strerror(__res)); \
+ lua_pushinteger(L, __res); \
+ return 3; \
+ } \
+ } while (0)
+
+static void check_valid(lua_State *L, struct handle *h) {
+ if (h->closed) luaL_error(L, "attempt to use expired handle");
+}
+
+static struct {const char *name; int flag;} env_flags[] = {
+ {"rdonly", MDB_RDONLY},
+ {"nosubdir", MDB_NOSUBDIR},
+ {"nosync", MDB_NOSYNC},
+ {"nometasync", MDB_NOMETASYNC},
+ {"nordahead", MDB_NORDAHEAD},
+ {"nolock", MDB_NOLOCK},
+ {0},
+};
+
+static int version(lua_State *L) {
+ int maj, min, patch;
+ mdb_version(&maj, &min, &patch);
+ lua_pushinteger(L, maj);
+ lua_pushinteger(L, min);
+ lua_pushinteger(L, patch);
+ return 3;
+}
+
+static int env_open(lua_State *L) {
+ lua_settop(L, 2);
+ luaL_argcheck(L, lua_isstring(L, 1), 1, "expected string");
+ luaL_argcheck(L,
+ lua_isnil(L, 2) || lua_istable(L, 2), 2, "expected table or nil");
+
+ int flags = 0;
+ int maxdbs = 0;
+ int maxdbs_supplied = 0;
+ if (!lua_isnil(L, 2)) {
+ for (int i = 0; env_flags[i].name != NULL; i++) {
+ lua_getfield(L, 2, env_flags[i].name);
+ if (lua_toboolean(L, -1)) flags |= env_flags[i].flag;
+ lua_pop(L, 1);
+ }
+ lua_getfield(L, 2, "maxdbs");
+ maxdbs = lua_tointegerx(L, -1, &maxdbs_supplied);
+ }
+
+ MDB_env *env;
+ reterr(mdb_env_create(&env));
+ if (maxdbs_supplied) mdb_env_set_maxdbs(env, maxdbs);
+ int result = mdb_env_open(env, lua_tostring(L, 1), flags, 0664);
+ if (result != 0) {
+ mdb_env_close(env);
+ reterr(result);
+ }
+
+ struct handle *ud = lua_newuserdata(L, sizeof(struct handle));
+ ud->closed = false;
+ ud->has_child = false;
+ ud->obj = env;
+ luaL_getmetatable(L, "lmdb.env");
+ lua_setmetatable(L, -2);
+ return 1;
+};
+
+static int env_close(lua_State *L) {
+ lua_settop(L, 1);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.env");
+ lua_pushboolean(L, true);
+ if (ud->closed) return 1;
+ mdb_env_close(ud->obj);
+ ud->closed = true;
+ return 1;
+}
+
+static int env_sync(lua_State *L) {
+ lua_settop(L, 2);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.env");
+ check_valid(L, ud);
+ reterr(mdb_env_sync(ud->obj, lua_toboolean(L, 2)));
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static int env_copy(lua_State *L) {
+ lua_settop(L, 2);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.env");
+ check_valid(L, ud);
+ luaL_argcheck(L, lua_isstring(L, 2), 2, "expected string");
+ reterr(mdb_env_copy(ud->obj, lua_tostring(L, 2)));
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static MDB_env *get_env(lua_State *L, int idx) {
+ lua_pushvalue(L, idx);
+ struct handle *ud = NULL;
+ while (lua_getuservalue(L, -1) == LUA_TUSERDATA) {
+ ud = lua_touserdata(L, -1);
+ if (ud->closed) return NULL;
+ check_valid(L, ud);
+ lua_remove(L, -2);
+ }
+ lua_pop(L, 2);
+ return ud->obj;
+}
+
+static MDB_env *check_env(lua_State *L, int idx) {
+ MDB_env *env = get_env(L, idx);
+ if (env == NULL) luaL_error(L, "attempt to use expired handle");
+ return env;
+}
+
+static int txn_begin(lua_State *L, bool nested) {
+ lua_settop(L, 2);
+ struct handle *ud = luaL_checkudata(L, 1, !nested ? "lmdb.env" : "lmdb.txn");
+ check_valid(L, ud);
+ if (ud->has_child)
+ luaL_error(L, "cannot create new transaction while another exists");
+
+ MDB_env *env;
+ MDB_txn *parent = NULL;
+ if (!nested) env = ud->obj;
+ else {
+ parent = ud->obj;
+ env = check_env(L, 1);
+ }
+
+ MDB_txn *txn;
+ reterr(mdb_txn_begin(
+ env, parent, lua_toboolean(L, 2) ? MDB_RDONLY : 0, &txn));
+
+ struct handle *tud = lua_newuserdata(L, sizeof(struct handle));
+ tud->closed = false;
+ tud->has_child = false;
+ tud->obj = txn;
+ luaL_getmetatable(L, "lmdb.txn");
+ lua_setmetatable(L, -2);
+ // store the supplied transaction/env as the parent
+ lua_pushvalue(L, 1);
+ lua_setuservalue(L, -2);
+
+ ud->has_child = true;
+ return 1;
+}
+
+static int env_txn_begin(lua_State *L) {
+ return txn_begin(L, false);
+}
+
+static int txn_txn_begin(lua_State *L) {
+ return txn_begin(L, true);
+}
+
+static int txn_abort(lua_State *L) {
+ lua_settop(L, 1);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.txn");
+ if (ud->closed || get_env(L, 1) == NULL) {
+ lua_pushboolean(L, true);
+ return 1;
+ }
+ mdb_txn_abort(ud->obj);
+ ud->closed = true;
+
+ lua_getuservalue(L, 1);
+ struct handle *pud = lua_touserdata(L, -1);
+ pud->has_child = false;
+
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static int txn_commit(lua_State *L) {
+ lua_settop(L, 1);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.txn");
+ check_valid(L, ud);
+ check_env(L, 1);
+ ud->closed = true;
+
+ lua_getuservalue(L, 1);
+ struct handle *pud = lua_touserdata(L, -1);
+ pud->has_child = false;
+
+ reterr(mdb_txn_commit(ud->obj));
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static int db_open(lua_State *L) {
+ lua_settop(L, 3);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.txn");
+ check_valid(L, ud);
+ check_env(L, 1);
+ luaL_argcheck(L,
+ lua_isnil(L, 2) || lua_isstring(L, 2), 2, "expected string or nil");
+
+ MDB_dbi dbi;
+ reterr(mdb_dbi_open(ud->obj,
+ lua_tostring(L, 2), lua_toboolean(L, 3) ? MDB_CREATE : 0, &dbi));
+ *(MDB_dbi *) lua_newuserdata(L, sizeof(MDB_dbi)) = dbi;
+ luaL_getmetatable(L, "lmdb.db");
+ lua_setmetatable(L, -2);
+ // transaction is parent of DB handle
+ lua_pushvalue(L, 1);
+ lua_setuservalue(L, -2);
+ return 1;
+}
+
+static MDB_val toval(lua_State *L, int idx) {
+ MDB_val val;
+ val.mv_data = lua_tolstring(L, idx, &val.mv_size);
+ return val;
+}
+
+static void fromval(lua_State *L, MDB_val val) {
+ lua_pushlstring(L, val.mv_data, val.mv_size);
+}
+
+static int db_get(lua_State *L) {
+ lua_settop(L, 2);
+ MDB_dbi dbi = *(MDB_dbi *) luaL_checkudata(L, 1, "lmdb.db");
+ check_env(L, 1);
+ luaL_argcheck(L, lua_isstring(L, 2), 2, "expected string");
+
+ lua_getuservalue(L, 1);
+ struct handle *tud = lua_touserdata(L, -1);
+ MDB_val key = toval(L, 2);
+ MDB_val data;
+ int result = mdb_get(tud->obj, dbi, &key, &data);
+ if (result == MDB_NOTFOUND) {
+ lua_pushnil(L);
+ return 1;
+ } else if (result != 0) luaL_error(L, "%s", mdb_strerror(result));
+ fromval(L, data);
+ return 1;
+}
+
+static int db_put(lua_State *L) {
+ lua_settop(L, 3);
+ MDB_dbi dbi = *(MDB_dbi *) luaL_checkudata(L, 1, "lmdb.db");
+ check_env(L, 1);
+ luaL_argcheck(L, lua_isstring(L, 2), 2, "expected string");
+ luaL_argcheck(
+ L, lua_isnil(L, 3) || lua_isstring(L, 3), 3, "expected string or nil");
+
+ lua_getuservalue(L, 1);
+ struct handle *tud = lua_touserdata(L, -1);
+ MDB_val key = toval(L, 2);
+ int result;
+ if (!lua_isnil(L, 3)) {
+ MDB_val data = toval(L, 3);
+ result = mdb_put(tud->obj, dbi, &key, &data, 0);
+ } else result = mdb_del(tud->obj, dbi, &key, NULL);
+ if (result !=0) luaL_error(L, "%s", mdb_strerror(result));
+ return 0;
+}
+
+static int db_drop(lua_State *L) {
+ lua_settop(L, 2);
+ struct handle *ud = luaL_checkudata(L, 1, "lmdb.txn");
+ check_valid(L, ud);
+ check_env(L, 1);
+ luaL_argcheck(L,
+ lua_isnil(L, 2) || lua_isstring(L, 2), 2, "expected string or nil");
+
+ MDB_dbi dbi;
+ reterr(mdb_dbi_open(ud->obj, lua_tostring(L, 2), 0, &dbi));
+ reterr(mdb_drop(ud->obj, dbi, !lua_isnil(L, 2)));
+ lua_pushboolean(L, true);
+ return 1;
+}
+
+static const struct luaL_Reg lmdb[] = {
+ {"open", env_open},
+ {"version", version},
+ {NULL, NULL},
+};
+
+static const struct luaL_Reg lmdb_env[] = {
+ {"txn_begin", env_txn_begin},
+ {"copy", env_copy},
+ {"sync", env_sync},
+ {"close", env_close},
+ {"abort", env_close},
+ {"__close", env_close},
+ {"__gc", env_close},
+ {NULL, NULL},
+};
+
+static const struct luaL_Reg lmdb_txn[] = {
+ {"open", db_open},
+ {"abort", txn_abort},
+ {"commit", txn_commit},
+ {"txn_begin", txn_txn_begin},
+ {"drop", db_drop},
+ {"__close", txn_commit},
+ {"__gc", txn_abort},
+ {NULL, NULL},
+};
+
+static const struct luaL_Reg lmdb_db[] = {
+ {"__index", db_get},
+ {"__newindex", db_put},
+ {NULL, NULL},
+};
+
+static void mt(lua_State *L, const char *name, const struct luaL_Reg l[]) {
+ luaL_newmetatable(L, name);
+ luaL_setfuncs(L, l, 0);
+ lua_getfield(L, -1, "__index");
+ if (lua_isnil(L, -1)){
+ lua_pop(L, 1);
+ lua_setfield(L, -1, "__index");
+ }
+ lua_pop(L, 2);
+}
+
+int luaopen_lmdb(lua_State *L) {
+ mt(L, "lmdb.env", lmdb_env);
+ mt(L, "lmdb.txn", lmdb_txn);
+ mt(L, "lmdb.db", lmdb_db);
+
+ luaL_newlib(L, lmdb);
+ return 1;
+}