diff --git a/build.sh b/build.sh
index 211112d..ef7739f 100755
--- a/build.sh
+++ b/build.sh
@@ -83,7 +83,7 @@ Darwin) # macOS
 	UXNEMU_LDFLAGS="$(brew --prefix)/lib/libSDL2.a $(sdl2-config --cflags --static-libs | sed -e 's/-lSDL2 //')"
 	;;
 Linux|*)
-	UXNEMU_LDFLAGS="-L/usr/local/lib $(sdl2-config --cflags --libs)"
+	UXNEMU_LDFLAGS="-L/usr/local/lib $(sdl2-config --cflags --libs) -ldl"
 	;;
 esac
 
diff --git a/src/uxn-fast.c b/src/uxn-fast.c
index 24d7625..13544aa 100644
--- a/src/uxn-fast.c
+++ b/src/uxn-fast.c
@@ -37,14 +37,13 @@ WITH REGARD TO THIS SOFTWARE.
 #define DEO(a, b) { u->dev[a] = b; if((deo_masks[(a) >> 4] >> ((a) & 0xf)) & 0x1) uxn_deo(u, a); }
 #define DEI(a, b) { PUT(a, ((dei_masks[(b) >> 4] >> ((b) & 0xf)) & 0x1) ? uxn_dei(u, b) : u->dev[b])  }
 
-static 
 Uint16 deo_masks[] = {
 	0x6a08, 0x0300, 0xc028, 0x8000, 0x8000, 0x8000, 0x8000, 0x0000,
 	0x0000, 0x0000, 0xa260, 0xa260, 0x0000, 0x0000, 0x0000, 0x0000 };
-static
+
 Uint16 dei_masks[] = {
 	0x0000, 0x0000, 0x0014, 0x0014, 0x0014, 0x0014, 0x0014, 0x0000,
-	0x0000, 0x0000, 0x0000, 0x0000, 0x07fd, 0x0000, 0x0000, 0x0000 };
+	0x0000, 0x0000, 0x0000, 0x0000, 0x07fd, 0x0000, 0x0000, 0xffff };
 
 int
 uxn_eval(Uxn *u, Uint16 pc)
diff --git a/src/uxn.h b/src/uxn.h
index ac3fce3..8acbf00 100644
--- a/src/uxn.h
+++ b/src/uxn.h
@@ -45,3 +45,17 @@ extern int uxn_halt(Uxn *u, Uint8 instr, Uint8 err, Uint16 addr);
 
 int uxn_boot(Uxn *u, Uint8 *ram);
 int uxn_eval(Uxn *u, Uint16 pc);
+
+/* device plugins */
+typedef struct Plugin {
+	void *handle;
+	int (*start)();
+	void (*stop)();
+	Uint16 deo_mask;
+	Uint16 dei_mask;
+	Uint8 (*dei)(Uxn *u, Uint8 addr);
+	void (*deo)(Uxn *u, Uint8 addr);
+} Plugin;
+
+extern Uint16 deo_masks[16];
+extern Uint16 dei_masks[16];
diff --git a/src/uxnemu.c b/src/uxnemu.c
index 9ef36bb..cc6ab2e 100644
--- a/src/uxnemu.c
+++ b/src/uxnemu.c
@@ -22,6 +22,8 @@
 #pragma GCC diagnostic pop
 #pragma clang diagnostic pop
 
+#include <dlfcn.h>
+
 /*
 Copyright (c) 2021-2023 Devine Lu Linvega, Andrew Alderwick
 
@@ -108,6 +110,13 @@ audio_deo(int instance, Uint8 *d, Uint8 port, Uxn *u)
 	}
 }
 
+Plugin *plugins[] = {
+	NULL, NULL, NULL, NULL,
+	NULL, NULL, NULL, NULL,
+	NULL, NULL, NULL, NULL,
+	NULL, NULL, NULL, NULL
+};
+
 Uint8
 uxn_dei(Uxn *u, Uint8 addr)
 {
@@ -119,6 +128,7 @@ uxn_dei(Uxn *u, Uint8 addr)
 	case 0x50: return audio_dei(2, &u->dev[d], p);
 	case 0x60: return audio_dei(3, &u->dev[d], p);
 	case 0xc0: return datetime_dei(u, addr);
+	default:	if (plugins[d>>4]) u->dev[addr] = plugins[d>>4]->dei(u, addr);
 	}
 	return u->dev[addr];
 }
@@ -141,6 +151,7 @@ uxn_deo(Uxn *u, Uint8 addr)
 	case 0x60: audio_deo(3, &u->dev[d], p, u); break;
 	case 0xa0: file_deo(0, u->ram, &u->dev[d], p); break;
 	case 0xb0: file_deo(1, u->ram, &u->dev[d], p); break;
+	default: if (plugins[d>>4]) plugins[d>>4]->deo(u, addr);
 	}
 }
 
@@ -259,6 +270,46 @@ init(void)
 
 /* Boot */
 
+static int
+load_plugin(char *path, Uint8 device, Plugin *plugin)
+{
+	/* handle to dynamically-loaded object */
+	void *handle;
+	char *errmsg;
+
+    handle = dlopen ("/home/d6/w/uxn/plugin.so", RTLD_LAZY);
+    if (!handle) return error("Device", dlerror());
+	plugin->handle = handle;
+
+	plugin->start = dlsym(handle, "start");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	plugin->stop = dlsym(handle, "stop");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	plugin->dei_mask = *(Uint8*)dlsym(handle, "dei_mask");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	plugin->deo_mask = *(Uint8*)dlsym(handle, "deo_mask");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	plugin->dei = dlsym(handle, "dei");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	plugin->deo = dlsym(handle, "deo");
+    if ((errmsg = dlerror()) != NULL) return error("Device", errmsg);
+
+	/* set up plugin in device 0xf */
+	int res = plugin->start();
+	if (res) {
+		deo_masks[device] = plugin->deo_mask;
+		dei_masks[device] = plugin->dei_mask;
+		plugins[device] = plugin;
+	}
+	return res;
+}
+
+
 static int
 start(Uxn *u, char *rom)
 {
@@ -474,6 +525,17 @@ main(int argc, char **argv)
 	/* set default zoom */
 	if(SDL_GetCurrentDisplayMode(0, &DM) == 0)
 		set_zoom(DM.w / 1280);
+
+	Plugin plugin;
+	if(!load_plugin(NULL, 0xf, &plugin))
+		return error("start", "loading plugin failed.");
+
+	/* set up plugin in device 0xf */
+/*	plugin.init();
+	deo_masks[0xf] = plugin.deo_mask;
+	dei_masks[0xf] = plugin.dei_mask;
+	plugins[0xf] = &plugin; */
+
 	for(i = 1; i < argc; i++) {
 		/* get default zoom from flags */
 		if(strcmp(argv[i], "-s") == 0) {
@@ -491,9 +553,12 @@ main(int argc, char **argv)
 			console_input(&u, '\n');
 		}
 	}
+
 	if(!loaded && !start(&u, "launcher.rom"))
 		return error("usage", "uxnemu [-s scale] file.rom");
+
 	run(&u);
+	dlclose(plugin.handle);
 #ifdef _WIN32
 #pragma GCC diagnostic ignored "-Wint-to-pointer-cast"
 	TerminateThread((HANDLE)SDL_GetThreadID(stdin_thread), 0);
