diff --git a/meson.build b/meson.build
index c30bb290c5..b6b8330b97 100644
--- a/meson.build
+++ b/meson.build
@@ -1147,7 +1147,9 @@ qapi_gen_depends = [ meson.source_root() / 'scripts/qapi/__init__.py',
                      meson.source_root() / 'scripts/qapi/types.py',
                      meson.source_root() / 'scripts/qapi/visit.py',
                      meson.source_root() / 'scripts/qapi/common.py',
-                     meson.source_root() / 'scripts/qapi-gen.py'
+                     meson.source_root() / 'scripts/qapi/rs.py',
+                     meson.source_root() / 'scripts/qapi/rs_sys.py',
+                     meson.source_root() / 'scripts/qapi-gen.py',
 ]
 
 tracetool = [
diff --git a/scripts/qapi-gen.py b/scripts/qapi-gen.py
index 541e8c1f55..5bfe9c8cd1 100644
--- a/scripts/qapi-gen.py
+++ b/scripts/qapi-gen.py
@@ -16,10 +16,13 @@ from qapi.schema import QAPIError, QAPISchema
 from qapi.types import gen_types
 from qapi.visit import gen_visit
 
+from qapi.rs_sys import gen_rs_systypes
 
 def main(argv):
     parser = argparse.ArgumentParser(
         description='Generate code from a QAPI schema')
+    parser.add_argument('-r', '--rust', action='store_true',
+                        help="generate Rust code")
     parser.add_argument('-b', '--builtins', action='store_true',
                         help="generate code for built-in types")
     parser.add_argument('-o', '--output-dir', action='store', default='',
@@ -45,11 +48,14 @@ def main(argv):
         print(err, file=sys.stderr)
         exit(1)
 
-    gen_types(schema, args.output_dir, args.prefix, args.builtins)
-    gen_visit(schema, args.output_dir, args.prefix, args.builtins)
-    gen_commands(schema, args.output_dir, args.prefix)
-    gen_events(schema, args.output_dir, args.prefix)
-    gen_introspect(schema, args.output_dir, args.prefix, args.unmask)
+    if args.rust:
+        gen_rs_systypes(schema, args.output_dir, args.prefix, args.builtins)
+    else:
+        gen_types(schema, args.output_dir, args.prefix, args.builtins)
+        gen_visit(schema, args.output_dir, args.prefix, args.builtins)
+        gen_commands(schema, args.output_dir, args.prefix)
+        gen_events(schema, args.output_dir, args.prefix)
+        gen_introspect(schema, args.output_dir, args.prefix, args.unmask)
 
 
 if __name__ == '__main__':
diff --git a/scripts/qapi/rs.py b/scripts/qapi/rs.py
new file mode 100644
index 0000000000..daa946580b
--- /dev/null
+++ b/scripts/qapi/rs.py
@@ -0,0 +1,126 @@
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+QAPI Rust generator
+"""
+
+import os
+import subprocess
+
+from qapi.common import *
+from qapi.gen import QAPIGen, QAPISchemaVisitor
+
+
+rs_name_trans = str.maketrans('.-', '__')
+
+# Map @name to a valid Rust identifier.
+# If @protect, avoid returning certain ticklish identifiers (like
+# keywords) by prepending raw identifier prefix 'r#'.
+def rs_name(name, protect=True):
+    name = name.translate(rs_name_trans)
+    if name[0].isnumeric():
+        name = '_' + name
+    if not protect:
+        return name
+    # based from the list:
+    # https://doc.rust-lang.org/reference/keywords.html
+    if name in ('Self', 'abstract', 'as', 'async',
+                'await','become', 'box', 'break',
+                'const', 'continue', 'crate', 'do',
+                'dyn', 'else', 'enum', 'extern',
+                'false', 'final', 'fn', 'for',
+                'if', 'impl', 'in', 'let',
+                'loop', 'macro', 'match', 'mod',
+                'move', 'mut', 'override', 'priv',
+                'pub', 'ref', 'return', 'self',
+                'static', 'struct', 'super', 'trait',
+                'true', 'try', 'type', 'typeof',
+                'union', 'unsafe', 'unsized', 'use',
+                'virtual', 'where', 'while', 'yield',
+                ):
+        name = 'r#' + name
+    return name
+
+
+def rs_ctype_parse(c_type):
+    is_pointer = False
+    if c_type.endswith(pointer_suffix):
+        is_pointer = True
+        c_type = c_type.rstrip(pointer_suffix).strip()
+    is_list = c_type.endswith('List')
+    is_const = False
+    if c_type.startswith('const '):
+        is_const = True
+        c_type = c_type[6:]
+
+    return (is_pointer, is_const, is_list, c_type)
+
+
+def rs_systype(c_type, sys_ns='qapi_sys::', list_as_newp=False):
+    (is_pointer, is_const, is_list, c_type) = rs_ctype_parse(c_type)
+
+    to_rs = {
+        'char': 'libc::c_char',
+        'int8_t': 'i8',
+        'uint8_t': 'u8',
+        'int16_t': 'i16',
+        'uint16_t': 'u16',
+        'int32_t': 'i32',
+        'uint32_t': 'u32',
+        'int64_t': 'libc::c_longlong',
+        'uint64_t': 'libc::c_ulonglong',
+        'double': 'libc::c_double',
+        'bool': 'bool',
+    }
+
+    rs = ''
+    if is_const and is_pointer:
+        rs += '*const '
+    elif is_pointer:
+        rs += '*mut '
+    if c_type in to_rs:
+        rs += to_rs[c_type]
+    else:
+        rs += sys_ns + c_type
+
+    if is_list and list_as_newp:
+        rs = 'NewPtr<{}>'.format(rs)
+
+    return rs
+
+
+def to_camel_case(value):
+    if value[0] == '_':
+        return value
+    raw_id = False
+    if value.startswith('r#'):
+        raw_id = True
+        value = value[2:]
+    value = ''.join(word.title() for word in filter(None, re.split("[-_]+", value)))
+    if raw_id:
+        return 'r#' + value
+    else:
+        return value
+
+
+class QAPIGenRs(QAPIGen):
+
+    def __init__(self, fname):
+        super().__init__(fname)
+
+
+class QAPISchemaRsVisitor(QAPISchemaVisitor):
+
+    def __init__(self, prefix, what):
+        self._prefix = prefix
+        self._what = what
+        self._gen = QAPIGenRs(self._prefix + self._what + '.rs')
+
+    def write(self, output_dir):
+        self._gen.write(output_dir)
+
+        pathname = os.path.join(output_dir, self._gen.fname)
+        try:
+            subprocess.check_call(['rustfmt', pathname])
+        except FileNotFoundError:
+            pass
diff --git a/scripts/qapi/rs_sys.py b/scripts/qapi/rs_sys.py
new file mode 100644
index 0000000000..551a910c57
--- /dev/null
+++ b/scripts/qapi/rs_sys.py
@@ -0,0 +1,254 @@
+# This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+QAPI Rust sys/ffi generator
+"""
+
+from qapi.common import *
+from qapi.rs import *
+from qapi.schema import QAPISchemaEnumMember, QAPISchemaObjectType
+
+
+objects_seen = set()
+
+
+def gen_rs_sys_enum(name, ifcond, members, prefix=None):
+    if ifcond:
+        raise NotImplementedError("ifcond are not implemented")
+    # append automatically generated _max value
+    enum_members = members + [QAPISchemaEnumMember('_MAX', None)]
+
+    ret = mcgen('''
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+#[repr(C)]
+pub enum %(rs_name)s {
+''',
+    rs_name=rs_name(name))
+
+    for m in enum_members:
+        if m.ifcond:
+            raise NotImplementedError("ifcond are not implemented")
+        ret += mcgen('''
+    %(c_enum)s,
+''',
+                     c_enum=to_camel_case(rs_name(m.name, False)))
+    ret += mcgen('''
+}
+''')
+    return ret
+
+
+def gen_rs_sys_struct_members(members):
+    ret = ''
+    for memb in members:
+        if memb.ifcond:
+            raise NotImplementedError("ifcond are not implemented")
+        if memb.optional:
+            ret += mcgen('''
+    pub has_%(rs_name)s: bool,
+''',
+                         rs_name=rs_name(memb.name, protect=False))
+        ret += mcgen('''
+    pub %(rs_name)s: %(rs_systype)s,
+''',
+                     rs_systype=rs_systype(memb.type.c_type(), ''), rs_name=rs_name(memb.name))
+    return ret
+
+
+def gen_rs_sys_free(ty):
+    return mcgen('''
+
+extern "C" {
+    pub fn qapi_free_%(ty)s(obj: *mut %(ty)s);
+}
+''', ty=ty)
+
+
+def gen_rs_sys_variants(name, variants):
+    ret = mcgen('''
+
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub union %(rs_name)s { /* union tag is @%(tag_name)s */
+''',
+                tag_name=rs_name(variants.tag_member.name),
+                rs_name=name)
+
+    for var in variants.variants:
+        if var.ifcond:
+            raise NotImplementedError("ifcond are not implemented")
+        if var.type.name == 'q_empty':
+            continue
+        ret += mcgen('''
+    pub %(rs_name)s: %(rs_systype)s,
+''',
+                     rs_systype=rs_systype(var.type.c_unboxed_type(), ''),
+                     rs_name=rs_name(var.name))
+
+    ret += mcgen('''
+}
+
+impl ::std::fmt::Debug for %(rs_name)s {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        f.debug_struct(&format!("%(rs_name)s @ {:?}", self as *const _))
+            .finish()
+    }
+}
+''', rs_name=name)
+
+    return ret
+
+
+def gen_rs_sys_object(name, ifcond, base, members, variants):
+    if ifcond:
+        raise NotImplementedError("ifcond are not implemented")
+    if name in objects_seen:
+        return ''
+
+    ret = ''
+    objects_seen.add(name)
+    unionty = name + 'Union'
+    if variants:
+        for v in variants.variants:
+            if v.ifcond:
+                raise NotImplementedError("ifcond are not implemented")
+            if isinstance(v.type, QAPISchemaObjectType):
+                ret += gen_rs_sys_object(v.type.name, v.type.ifcond, v.type.base,
+                                         v.type.local_members, v.type.variants)
+        ret += gen_rs_sys_variants(unionty, variants)
+
+    ret += gen_rs_sys_free(rs_name(name))
+    ret += mcgen('''
+
+#[repr(C)]
+#[derive(Copy, Clone, Debug)]
+pub struct %(rs_name)s {
+''',
+                 rs_name=rs_name(name))
+
+    if base:
+        if not base.is_implicit():
+            ret += mcgen('''
+    // Members inherited:
+''')
+        ret += gen_rs_sys_struct_members(base.members)
+        if not base.is_implicit():
+            ret += mcgen('''
+    // Own members:
+''')
+
+    ret += gen_rs_sys_struct_members(members)
+    if variants:
+        ret += mcgen('''
+        pub u: %(unionty)s
+''', unionty=unionty)
+    ret += mcgen('''
+}
+''')
+    return ret
+
+
+def gen_rs_sys_variant(name, ifcond, variants):
+    if ifcond:
+        raise NotImplementedError("ifcond are not implemented")
+    if name in objects_seen:
+        return ''
+
+    objects_seen.add(name)
+
+    vs = ''
+    for var in variants.variants:
+        if var.type.name == 'q_empty':
+            continue
+        vs += mcgen('''
+    pub %(mem_name)s: %(rs_systype)s,
+''',
+                     rs_systype=rs_systype(var.type.c_unboxed_type(), ''),
+                     mem_name=rs_name(var.name))
+
+    return mcgen('''
+
+#[repr(C)]
+#[derive(Copy,Clone)]
+pub union %(rs_name)sUnion {
+    %(variants)s
+}
+
+impl ::std::fmt::Debug for %(rs_name)sUnion {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        f.debug_struct(&format!("%(rs_name)sUnion @ {:?}", self as *const _))
+            .finish()
+    }
+}
+
+#[repr(C)]
+#[derive(Copy,Clone,Debug)]
+pub struct %(rs_name)s {
+    pub ty: QType,
+    pub u: %(rs_name)sUnion,
+}
+''',
+                 rs_name=rs_name(name), variants=vs)
+
+
+def gen_rs_sys_array(name, ifcond, element_type):
+    if ifcond:
+        raise NotImplementedError("ifcond are not implemented")
+    ret = mcgen('''
+
+#[repr(C)]
+#[derive(Copy,Clone)]
+pub struct %(rs_name)s {
+    pub next: *mut %(rs_name)s,
+    pub value: %(rs_systype)s,
+}
+
+impl ::std::fmt::Debug for %(rs_name)s {
+    fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
+        f.debug_struct(&format!("%(rs_name)s @ {:?}", self as *const _))
+            .finish()
+    }
+}
+''',
+                 rs_name=rs_name(name), rs_systype=rs_systype(element_type.c_type(), ''))
+    ret += gen_rs_sys_free(rs_name(name))
+    return ret
+
+
+class QAPISchemaGenRsSysTypeVisitor(QAPISchemaRsVisitor):
+
+    def __init__(self, prefix):
+        super().__init__(prefix, 'qapi-sys-types')
+
+    def visit_begin(self, schema):
+        # gen_object() is recursive, ensure it doesn't visit the empty type
+        objects_seen.add(schema.the_empty_object_type.name)
+        self._gen.preamble_add(
+            mcgen('''
+// generated by qapi-gen, DO NOT EDIT
+
+use common::sys::{QNull, QObject};
+
+'''))
+
+    def visit_enum_type(self, name, info, ifcond, features, members, prefix):
+        self._gen.add(gen_rs_sys_enum(name, ifcond, members, prefix))
+
+    def visit_array_type(self, name, info, ifcond, element_type):
+        self._gen.add(gen_rs_sys_array(name, ifcond, element_type))
+
+    def visit_object_type(self, name, info, ifcond, features,
+                          base, members, variants):
+        if name.startswith('q_'):
+            return
+        self._gen.add(gen_rs_sys_object(name, ifcond, base, members, variants))
+
+    def visit_alternate_type(self, name, info, ifcond, features, variants):
+        self._gen.add(gen_rs_sys_variant(name, ifcond, variants))
+
+
+def gen_rs_systypes(schema, output_dir, prefix, opt_builtins):
+    vis = QAPISchemaGenRsSysTypeVisitor(prefix)
+    schema.visit(vis)
+    vis.write(output_dir)
