diff --git a/include/qemu/plug-proptypes.h b/include/qemu/plug-proptypes.h
new file mode 100644
index 0000000..2c7be0e
--- /dev/null
+++ b/include/qemu/plug-proptypes.h
@@ -0,0 +1,14 @@
+#ifndef PLUG_PROPTYPES_H
+#define PLUG_PROPTYPES_H
+
+#include "plug.h"
+
+typedef bool (PlugPropertyGetterBool)(Plug *plug);
+typedef void (PlugPropertySetterBool)(Plug *plug, bool value);
+
+void plug_add_property_bool(Plug *plug, const char *name,
+                            PlugPropertyGetterBool *getter,
+                            PlugPropertySetterBool *setter,
+                            int flags);
+
+#endif
diff --git a/include/qemu/plug.h b/include/qemu/plug.h
new file mode 100644
index 0000000..f6948a0
--- /dev/null
+++ b/include/qemu/plug.h
@@ -0,0 +1,73 @@
+#ifndef PLUG_H
+#define PLUG_H
+
+#include "qemu/type.h"
+#include "error.h"
+#include "qapi/qapi-visit-core.h"
+
+typedef struct Plug Plug;
+typedef struct PlugProperty PlugProperty;
+
+typedef enum PlugPropertyFlags
+{
+    PROP_F_NONE = 0,
+    PROP_F_READ = 1,
+    PROP_F_WRITE = 2,
+    PROP_F_READWRITE = (PROP_F_READ | PROP_F_WRITE),
+    PROP_F_LOCKED = 4,
+} PlugPropertyFlags;
+
+struct Plug
+{
+    TypeInstance parent;
+
+    /* private */
+    bool realized;
+    PlugProperty *first_prop;
+};
+
+typedef struct PlugClass {
+    TypeClass parent_class;
+
+    /* protected */
+    void (*realize)(Plug *plug);
+    void (*unrealize)(Plug *plug);
+} PlugClass;
+
+#define TYPE_PLUG "plug"
+#define PLUG(obj) TYPE_CHECK(Plug, obj, TYPE_PLUG)
+#define PLUG_GET_CLASS(obj) TYPE_GET_CLASS(PlugClass, obj, TYPE_PLUG)
+#define PLUG_CLASS(obj) TYPE_CLASS_CHECK(PlugClass, obj, TYPE_PLUG)
+
+typedef void (PlugPropertyAccessor)(Plug *plug, const char *name, Visitor *v, void *opaque, Error **errp);
+typedef void (PlugPropertyFinalize)(Plug *plug, const char *name, void *opaque);
+typedef void (PropertyEnumerator)(Plug *plug, const char *name, const char *typename, int flags, void *opaque);
+
+void plug_set_property(Plug *plug, const char *name, Visitor *v, Error **errp);
+
+void plug_get_property(Plug *plug, const char *name, Visitor *v, Error **errp);
+
+void plug_add_property_full(Plug *plug, const char *name,
+                            PlugPropertyAccessor *getter,
+                            PlugPropertyAccessor *setter,
+                            PlugPropertyFinalize *fini,
+                            void *opaque,
+                            const char *typename, int flags);
+
+void plug_foreach_property(Plug *plug, PropertyEnumerator *enumfn, void *opaque);
+
+void plug_lock_property(Plug *plug, const char *name);
+void plug_unlock_property(Plug *plug, const char *name);
+
+void plug_lock_all_properties(Plug *plug);
+void plug_unlock_all_properties(Plug *plug);
+
+void plug_set_realized(Plug *plug, bool realized);
+bool plug_get_realized(Plug *plug);
+
+void plug_realize_all(Plug *plug);
+void plug_unrealize_all(Plug *plug);
+
+#include "qemu/plug-proptypes.h"
+
+#endif
diff --git a/qerror.c b/qerror.c
index 69c1bc9..dbe119c 100644
--- a/qerror.c
+++ b/qerror.c
@@ -170,6 +170,10 @@ static const QErrorStringTable qerror_table[] = {
         .desc      = "Property '%(device).%(property)' not found",
     },
     {
+        .error_fmt = QERR_PROPERTY_READ_ONLY,
+        .desc      = "Property '%(device).%(property)' is read only",
+    },
+    {
         .error_fmt = QERR_PROPERTY_VALUE_BAD,
         .desc      = "Property '%(device).%(property)' doesn't take value '%(value)'",
     },
diff --git a/qerror.h b/qerror.h
index 8058456..eaabe6c 100644
--- a/qerror.h
+++ b/qerror.h
@@ -145,6 +145,9 @@ QError *qobject_to_qerror(const QObject *obj);
 #define QERR_PROPERTY_NOT_FOUND \
     "{ 'class': 'PropertyNotFound', 'data': { 'device': %s, 'property': %s } }"
 
+#define QERR_PROPERTY_READ_ONLY \
+    "{ 'class': 'PropertyReadOnly', 'data': { 'device': %s, 'property': %s } }"
+
 #define QERR_PROPERTY_VALUE_BAD \
     "{ 'class': 'PropertyValueBad', 'data': { 'device': %s, 'property': %s, 'value': %s } }"
 
diff --git a/qom/Makefile b/qom/Makefile
index 838054f..8a438d5 100644
--- a/qom/Makefile
+++ b/qom/Makefile
@@ -1 +1 @@
-qom-obj-$(CONFIG_QOM) += type.o
+qom-obj-$(CONFIG_QOM) += type.o string-visitor.o plug.o plug-proptypes.o
diff --git a/qom/plug-proptypes.c b/qom/plug-proptypes.c
new file mode 100644
index 0000000..e9152a7
--- /dev/null
+++ b/qom/plug-proptypes.c
@@ -0,0 +1,52 @@
+/** FIXME: move to generated code **/
+
+#include "qemu/plug-proptypes.h"
+
+typedef struct FunctionPointer
+{
+    void (*getter)(void);
+    void (*setter)(void);
+} FunctionPointer;
+
+static void plug_get_property__bool(Plug *plug, const char *name, Visitor *v, void *opaque, Error **errp)
+{
+    FunctionPointer *fp = opaque;
+    PlugPropertyGetterBool *getter = (PlugPropertyGetterBool *)fp->getter;
+    bool value;
+
+    value = getter(plug);
+    visit_type_bool(v, &value, name, errp);
+}
+
+static void plug_set_property__bool(Plug *plug, const char *name, Visitor *v, void *opaque, Error **errp)
+{
+    FunctionPointer *fp = opaque;
+    PlugPropertySetterBool *setter = (PlugPropertySetterBool *)fp->setter;
+    bool value = false;
+    Error *local_err = NULL;
+
+    visit_type_bool(v, &value, name, &local_err);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        return;
+    }
+
+    setter(plug, value);
+}
+
+void plug_add_property_bool(Plug *plug, const char *name,
+                            PlugPropertyGetterBool *getter,
+                            PlugPropertySetterBool *setter,
+                            int flags)
+{
+    FunctionPointer *fp = qemu_mallocz(sizeof(*fp));
+
+    fp->getter = (void (*)(void))getter;
+    fp->setter = (void (*)(void))setter;
+
+    plug_add_property_full(plug, name,
+                           plug_get_property__bool,
+                           plug_set_property__bool,
+                           plug_del_property__fp,
+                           fp, "bool", flags);
+}
diff --git a/qom/plug.c b/qom/plug.c
new file mode 100644
index 0000000..4a5f2fb
--- /dev/null
+++ b/qom/plug.c
@@ -0,0 +1,225 @@
+#include "qemu/plug.h"
+#include "qerror.h"
+#include "qom/string-visitor.h"
+
+#define MAX_NAME (128 + 1)
+#define MAX_TYPENAME (32 + 1)
+
+struct PlugProperty
+{
+    char name[MAX_NAME];
+    char typename[MAX_TYPENAME];
+    
+    PlugPropertyAccessor *getter;
+    PlugPropertyAccessor *setter;
+    PlugPropertyFinalize *finalize;
+    void *opaque;
+
+    int flags;
+
+    PlugProperty *next;
+};
+
+void plug_add_property_full(Plug *plug, const char *name,
+                            PlugPropertyAccessor *getter,
+                            PlugPropertyAccessor *setter,
+                            PlugPropertyFinalize *fini,
+                            void *opaque, const char *typename, int flags)
+{
+    PlugProperty *prop = qemu_mallocz(sizeof(*prop));
+
+    snprintf(prop->name, sizeof(prop->name), "%s", name);
+    snprintf(prop->typename, sizeof(prop->typename), "%s", typename);
+
+    prop->getter = getter;
+    prop->setter = setter;
+    prop->finalize = fini;
+    prop->opaque = opaque;
+
+    prop->flags = flags;
+
+    prop->next = plug->first_prop;
+    plug->first_prop = prop;
+}
+
+static PlugProperty *plug_find_property(Plug *plug, const char *name)
+{
+    PlugProperty *prop;
+
+    for (prop = plug->first_prop; prop; prop = prop->next) {
+        if (strcmp(prop->name, name) == 0) {
+            return prop;
+        }
+    }
+
+    return NULL;
+}
+
+void plug_set_property(Plug *plug, const char *name, Visitor *v, Error **errp)
+{
+    PlugProperty *prop = plug_find_property(plug, name);
+
+    if (!prop) {
+        error_set(errp, QERR_PROPERTY_NOT_FOUND, type_get_id(TYPE_INSTANCE(plug)), name);
+        return;
+    }
+
+    if (!(prop->flags & PROP_F_WRITE) || (prop->flags & PROP_F_LOCKED)) {
+        error_set(errp, QERR_PROPERTY_READ_ONLY, type_get_id(TYPE_INSTANCE(plug)), name);
+        return;
+    }
+
+    prop->setter(plug, name, v, prop->opaque, errp);
+}
+
+void plug_get_property(Plug *plug, const char *name, Visitor *v, Error **errp)
+{
+    PlugProperty *prop = plug_find_property(plug, name);
+
+    if (!prop) {
+        error_set(errp, QERR_PROPERTY_NOT_FOUND, type_get_id(TYPE_INSTANCE(plug)), name);
+        printf("property not found\n");
+        return;
+    }
+
+    if (!(prop->flags & PROP_F_READ)) {
+        error_set(errp, QERR_PROPERTY_READ_ONLY, type_get_id(TYPE_INSTANCE(plug)), name);
+        printf("property read only\n");
+        return;
+    }
+
+    prop->getter(plug, name, v, prop->opaque, errp);
+}
+
+void plug_lock_property(Plug *plug, const char *name)
+{
+    PlugProperty *prop = plug_find_property(plug, name);
+
+    assert(prop != NULL);
+
+    prop->flags |= PROP_F_LOCKED;
+}
+
+void plug_unlock_property(Plug *plug, const char *name)
+{
+    PlugProperty *prop = plug_find_property(plug, name);
+
+    assert(prop != NULL);
+
+    prop->flags &= ~PROP_F_LOCKED;
+}
+
+void plug_lock_all_properties(Plug *plug)
+{
+    PlugProperty *prop;
+
+    for (prop = plug->first_prop; prop; prop = prop->next) {
+        prop->flags |= PROP_F_LOCKED;
+    }
+}
+
+void plug_unlock_all_properties(Plug *plug)
+{
+    PlugProperty *prop;
+
+    for (prop = plug->first_prop; prop; prop = prop->next) {
+        prop->flags &= ~PROP_F_LOCKED;
+    }
+}
+
+void plug_foreach_property(Plug *plug, PropertyEnumerator *enumfn, void *opaque)
+{
+    PlugProperty *prop;
+
+    for (prop = plug->first_prop; prop; prop = prop->next) {
+        enumfn(plug, prop->name, prop->typename, prop->flags, opaque);
+    }
+}
+
+void plug_set_realized(Plug *plug, bool realized)
+{
+    PlugClass *class = PLUG_GET_CLASS(plug);
+    bool old_value = plug->realized;
+
+    plug->realized = realized;
+
+    if (plug->realized && !old_value) {
+        if (class->realize) {
+            class->realize(plug);
+        }
+    } else if (!plug->realized && old_value) {
+        if (class->unrealize) {
+            class->unrealize(plug);
+        }
+    }
+}
+
+bool plug_get_realized(Plug *plug)
+{
+    return plug->realized;
+}
+
+void plug_realize_all(Plug *plug)
+{
+    /* This doesn't loop infinitely because the callbacks are only called when
+     * the state changes. */
+    plug_set_realized(plug, true);
+    plug_lock_all_properties(plug);
+}
+
+void plug_unrealize_all(Plug *plug)
+{
+    /* This doesn't loop infinitely because the callbacks are only called when
+     * the state changes. */
+    plug_set_realized(plug, false);
+    plug_unlock_all_properties(plug);
+}
+
+static void plug_class_initfn(TypeClass *base_class)
+{
+    PlugClass *class = PLUG_CLASS(base_class);
+
+    class->realize = plug_realize_all;
+    class->unrealize = plug_unrealize_all;
+}
+
+static void plug_initfn(TypeInstance *inst)
+{
+    Plug *obj = PLUG(inst);
+
+    plug_add_property_bool(obj, "realized",
+                           plug_get_realized,
+                           plug_set_realized,
+                           PROP_F_READWRITE);
+}
+
+static void plug_finifn(TypeInstance *inst)
+{
+    Plug *plug = PLUG(inst);
+
+    while (plug->first_prop) {
+        PlugProperty *p = plug->first_prop;
+
+        plug->first_prop = plug->first_prop->next;
+        if (p->finalize) {
+            p->finalize(plug, p->name, p->opaque);
+        }
+        qemu_free(p);
+    }
+}
+
+static const TypeInfo plug_type_info = {
+    .name = TYPE_PLUG,
+    .instance_size = sizeof(Plug),
+    .class_size = sizeof(PlugClass),
+    .instance_init = plug_initfn,
+    .instance_finalize = plug_finifn,
+    .class_init = plug_class_initfn,
+};
+
+static void register_devices(void)
+{
+    type_register_static(&plug_type_info);
+}
+
+device_init(register_devices);
diff --git a/qom/string-visitor.c b/qom/string-visitor.c
new file mode 100644
index 0000000..55297e2
--- /dev/null
+++ b/qom/string-visitor.c
@@ -0,0 +1,41 @@
+#include "string-visitor.h"
+
+static void string_input_visitor_int(Visitor *v, int64_t *obj, const char *name, Error **errp)
+{
+    StringInputVisitor *sv = container_of(v, StringInputVisitor, parent);
+    *obj = atoi(sv->value);
+}
+
+static void string_input_visitor_str(Visitor *v, char **obj, const char *name, Error **errp)
+{
+    StringInputVisitor *sv = container_of(v, StringInputVisitor, parent);
+    *obj = qemu_strdup(sv->value);
+}
+
+void string_input_visitor_init(StringInputVisitor *sv, const char *value)
+{
+    memset(sv, 0, sizeof(*sv));
+    sv->parent.type_int = string_input_visitor_int;
+    sv->parent.type_str = string_input_visitor_str;
+    sv->value = value;
+}
+
+static void string_output_visitor_int(Visitor *v, int64_t *obj, const char *name, Error **errp)
+{
+    StringOutputVisitor *sv = container_of(v, StringOutputVisitor, parent);
+    snprintf(sv->value, sizeof(sv->value), "%" PRId64, *obj);
+}
+
+static void string_output_visitor_str(Visitor *v, char **obj, const char *name, Error **errp)
+{
+    StringOutputVisitor *sv = container_of(v, StringOutputVisitor, parent);
+
+    snprintf(sv->value, sizeof(sv->value), "%s", *obj);
+}
+
+void string_output_visitor_init(StringOutputVisitor *sv)
+{
+    memset(sv, 0, sizeof(*sv));
+    sv->parent.type_int = string_output_visitor_int;
+    sv->parent.type_str = string_output_visitor_str;
+}
diff --git a/qom/string-visitor.h b/qom/string-visitor.h
new file mode 100644
index 0000000..ddee3e5
--- /dev/null
+++ b/qom/string-visitor.h
@@ -0,0 +1,21 @@
+#ifndef STRING_VISITOR_H
+#define STRING_VISITOR_H
+
+#include "qapi/qapi-visit-core.h"
+
+typedef struct StringInputVisitor
+{
+    Visitor parent;
+    const char *value;
+} StringInputVisitor;
+
+typedef struct StringOutputVisitor
+{
+    Visitor parent;
+    char value[256];
+} StringOutputVisitor;
+
+void string_input_visitor_init(StringInputVisitor *sv, const char *value);
+void string_output_visitor_init(StringOutputVisitor *sv);
+
+#endif
