diff --git a/crates/rune-macros/Cargo.toml b/crates/rune-macros/Cargo.toml
index f8df9df87..c12612b7f 100644
--- a/crates/rune-macros/Cargo.toml
+++ b/crates/rune-macros/Cargo.toml
@@ -15,7 +15,7 @@ categories = ["parser-implementations"]
 
 [dependencies]
 rune-core = { version = "=0.14.0", path = "../rune-core", features = ["std"] }
-syn = { version = "2.0.16", features = ["full"] }
+syn = { version = "2.0.16", features = ["full", "extra-traits"] }
 quote = "1.0.27"
 proc-macro2 = "1.0.56"
 
diff --git a/crates/rune-macros/src/function.rs b/crates/rune-macros/src/function.rs
index 135e77060..fa9328296 100644
--- a/crates/rune-macros/src/function.rs
+++ b/crates/rune-macros/src/function.rs
@@ -116,13 +116,13 @@ impl FunctionAttrs {
 }
 
 pub(crate) struct Function {
-    attributes: Vec<syn::Attribute>,
-    vis: syn::Visibility,
-    sig: syn::Signature,
-    remainder: TokenStream,
-    docs: syn::ExprArray,
-    arguments: syn::ExprArray,
-    takes_self: bool,
+    pub attributes: Vec<syn::Attribute>,
+    pub vis: syn::Visibility,
+    pub sig: syn::Signature,
+    pub remainder: TokenStream,
+    pub docs: syn::ExprArray,
+    pub arguments: syn::ExprArray,
+    pub takes_self: bool,
 }
 
 impl Function {
diff --git a/crates/rune-macros/src/item_impl.rs b/crates/rune-macros/src/item_impl.rs
new file mode 100644
index 000000000..a7005048d
--- /dev/null
+++ b/crates/rune-macros/src/item_impl.rs
@@ -0,0 +1,161 @@
+use proc_macro2::TokenStream;
+use quote::{quote, ToTokens};
+use syn::parse::ParseStream;
+use syn::punctuated::Punctuated;
+use syn::spanned::Spanned;
+use syn::Token;
+
+pub(crate) struct ItemImplAttrs {
+    /// Name of the function meta list function
+    list: syn::Ident,
+    /// Name of the function export function
+    exporter: Option<syn::Ident>,
+}
+
+impl Default for ItemImplAttrs {
+    fn default() -> Self {
+        Self {
+            list: syn::Ident::new("rune_api", proc_macro2::Span::call_site()),
+            exporter: None,
+        }
+    }
+}
+
+impl ItemImplAttrs {
+    const LIST_IDENT: &'static str = "list";
+    const EXPORTER_IDENT: &'static str = "exporter";
+
+    pub(crate) fn parse(input: ParseStream) -> syn::Result<Self> {
+        let mut attrs = Self::default();
+
+        while !input.is_empty() {
+            let ident = input.parse::<syn::Ident>()?;
+
+            match ident.to_string().as_str() {
+                Self::LIST_IDENT | Self::EXPORTER_IDENT => {
+                    input.parse::<Token![=]>()?;
+                    if ident == Self::LIST_IDENT {
+                        attrs.list = input.parse()?;
+                    } else {
+                        attrs.exporter = Some(input.parse()?);
+                    }
+                }
+                _ => return Err(syn::Error::new_spanned(ident, "Unsupported option")),
+            }
+
+            if input.parse::<Option<Token![,]>>()?.is_none() {
+                break;
+            }
+        }
+
+        Ok(attrs)
+    }
+}
+
+pub(crate) struct ItemImpl(pub syn::ItemImpl);
+
+impl ItemImpl {
+    pub(crate) fn expand(self, attrs: ItemImplAttrs) -> syn::Result<TokenStream> {
+        let Self(mut block) = self;
+
+        let mut export_list = Vec::new();
+        let export_attr: syn::Attribute = syn::parse_quote!(#[rune(export)]);
+
+        for item in block.items.iter_mut() {
+            if let syn::ImplItem::Fn(method) = item {
+                let attr_index = method
+                    .attrs
+                    .iter()
+                    .enumerate()
+                    .find_map(|(index, attr)| (*attr == export_attr).then_some(index));
+
+                if let Some(index) = attr_index {
+                    method.attrs.remove(index);
+
+                    let reparsed = syn::parse::Parser::parse2(
+                        crate::function::Function::parse,
+                        method.to_token_stream(),
+                    )?;
+
+                    let name = method.sig.ident.clone();
+                    let name_string = syn::LitStr::new(
+                        &reparsed.sig.ident.to_string(),
+                        reparsed.sig.ident.span(),
+                    );
+                    let path = syn::Path {
+                        leading_colon: None,
+                        segments: Punctuated::from_iter(
+                            [
+                                reparsed
+                                    .takes_self
+                                    .then(|| syn::PathSegment::from(<syn::Token![Self]>::default()))
+                                    .or_else(|| {
+                                        Some(syn::PathSegment::from(
+                                            syn::parse2::<syn::Ident>(
+                                                block.self_ty.to_token_stream(),
+                                            )
+                                            .unwrap(),
+                                        ))
+                                    }),
+                                Some(syn::PathSegment::from(name.clone())),
+                            ]
+                            .into_iter()
+                            .flatten(),
+                        ),
+                    };
+
+                    let docs = reparsed.docs;
+                    let arguments = reparsed.arguments;
+                    let meta_kind = syn::Ident::new(
+                        ["function", "instance"][reparsed.takes_self as usize],
+                        reparsed.sig.span(),
+                    );
+                    let build_with = if reparsed.takes_self {
+                        None
+                    } else {
+                        Some(quote!(.build()?))
+                    };
+
+                    let meta = quote! {
+                        rune::__private::FunctionMetaData {
+                            kind: rune::__private::FunctionMetaKind::#meta_kind(#name_string, #path)?#build_with,
+                            name: #name_string,
+                            deprecated: None,
+                            docs: &#docs[..],
+                            arguments: &#arguments[..],
+                        }
+                    };
+
+                    export_list.push(meta);
+                }
+            }
+        }
+
+        let name = attrs.list;
+
+        let export_count = export_list.len();
+        let list_function = quote! {
+            fn #name() -> ::rune::alloc::Result<[::rune::__private::FunctionMetaData; #export_count]> {
+                Ok([ #(#export_list),* ])
+            }
+        };
+        block.items.push(syn::parse2(list_function).unwrap());
+
+        if let Some(exporter_name) = attrs.exporter {
+            let exporter_function = quote! {
+                fn #exporter_name(mut module: ::rune::Module) -> ::rune::alloc::Result<Result<::rune::Module, ::rune::ContextError>> {
+                    for meta in Self::#name()? {
+                        if let Err(e) = module.function_from_meta(meta) {
+                            return Ok(Err(e));
+                        }
+                    }
+                    Ok(Ok(module))
+                }
+            };
+
+            block.items.push(syn::parse2(exporter_function).unwrap());
+        }
+
+        Ok(block.to_token_stream())
+    }
+}
diff --git a/crates/rune-macros/src/lib.rs b/crates/rune-macros/src/lib.rs
index adf5b22c9..5740cbdd9 100644
--- a/crates/rune-macros/src/lib.rs
+++ b/crates/rune-macros/src/lib.rs
@@ -37,6 +37,7 @@ mod hash;
 mod inst_display;
 mod instrument;
 mod internals;
+mod item_impl;
 mod macro_;
 mod module;
 mod opaque;
@@ -77,6 +78,54 @@ pub fn function(
     output.into()
 }
 
+/// Create a function to export all functions marked with the `#[rune(export)]` attribute within a module.
+///
+/// ### Example
+///
+/// ```rs
+/// #[derive(rune::Any)]
+/// struct MyStruct {
+///     field: u32,
+/// }
+///
+/// #[rune::item_impl(exporter = export_rune_api)]
+/// impl MyStruct {
+///     // Exported
+///     #[rune(export)]
+///     fn foo(&self) -> u32 {
+///         self.field + 1
+///     }
+///
+///     // Not exported
+///     fn bar(&self) -> u32 {
+///         self.field + 2
+///     }
+/// }
+///
+/// fn main() {
+///     let mut module = rune::Module::new();
+///     module.ty::<MyStruct>().unwrap();
+///     module = MyStruct::export_rune_api(module)
+///         .expect("Allocation error")
+///         .expect("Context error");
+/// }
+/// ```
+#[proc_macro_attribute]
+pub fn item_impl(
+    attrs: proc_macro::TokenStream,
+    item: proc_macro::TokenStream,
+) -> proc_macro::TokenStream {
+    let attrs = syn::parse_macro_input!(attrs with crate::item_impl::ItemImplAttrs::parse);
+    let item = crate::item_impl::ItemImpl(syn::parse_macro_input!(item as syn::ItemImpl));
+
+    let output = match item.expand(attrs) {
+        Ok(output) => output,
+        Err(e) => return proc_macro::TokenStream::from(e.to_compile_error()),
+    };
+
+    output.into()
+}
+
 #[proc_macro_attribute]
 pub fn macro_(
     attrs: proc_macro::TokenStream,
diff --git a/crates/rune-macros/tests/derive.rs b/crates/rune-macros/tests/derive.rs
index 489fc8436..1d47defb1 100644
--- a/crates/rune-macros/tests/derive.rs
+++ b/crates/rune-macros/tests/derive.rs
@@ -1,3 +1,5 @@
+use std::fmt::Debug;
+
 use rune::T;
 use rune_macros::*;
 
@@ -17,3 +19,64 @@ fn generic_derive() {
         value: T,
     }
 }
+
+#[test]
+fn export_impl() {
+    #[derive(crate::Any)]
+    struct MyStruct(#[rune(get)] usize);
+
+    #[crate::item_impl(exporter = export_rune_api)]
+    impl MyStruct {
+        #[rune(export)]
+        pub fn foo(&self) -> usize {
+            self.0
+        }
+    }
+
+    #[crate::item_impl(list = rune_api_extension, exporter = export_rune_api_extension)]
+    impl MyStruct {
+        #[rune(export)]
+        pub fn bar(&self) -> usize {
+            self.0 + 1
+        }
+
+        #[rune(export)]
+        pub fn baz() -> usize {
+            42
+        }
+
+        pub fn rune_export(
+            mut module: rune::Module,
+        ) -> rune::alloc::Result<Result<rune::Module, rune::ContextError>> {
+            for func in Self::rune_api()?
+                .into_iter()
+                .chain(Self::rune_api_extension()?.into_iter())
+            {
+                if let Err(e) = module.function_from_meta(func) {
+                    return Ok(Err(e));
+                }
+            }
+
+            Ok(Ok(module))
+        }
+    }
+
+    let a = MyStruct(2);
+    assert_eq!(a.foo() + 1, a.bar());
+
+    fn test_fn<F, T, E>(f: F)
+    where
+        E: Debug,
+        F: Fn(rune::Module) -> Result<T, E>,
+    {
+        let mut m = rune::Module::new();
+        m.ty::<MyStruct>().unwrap();
+        f(m).unwrap();
+    }
+
+    test_fn(MyStruct::rune_export);
+    test_fn(MyStruct::export_rune_api);
+    test_fn(MyStruct::export_rune_api_extension);
+
+    assert_eq!(MyStruct::baz(), 42);
+}
diff --git a/crates/rune/src/module/module.rs b/crates/rune/src/module/module.rs
index 1f235b85e..19c881a7a 100644
--- a/crates/rune/src/module/module.rs
+++ b/crates/rune/src/module/module.rs
@@ -25,6 +25,8 @@ use crate::runtime::{
 };
 use crate::Hash;
 
+use super::FunctionMetaData;
+
 /// Function builder as returned by [`Module::function`].
 ///
 /// This allows for building a function regularly with
@@ -1242,7 +1244,14 @@ impl Module {
     #[inline]
     pub fn function_meta(&mut self, meta: FunctionMeta) -> Result<ItemFnMut<'_>, ContextError> {
         let meta = meta()?;
+        self.function_from_meta(meta)
+    }
 
+    /// Register a function handler through its metadata.
+    pub fn function_from_meta(
+        &mut self,
+        meta: FunctionMetaData,
+    ) -> Result<ItemFnMut<'_>, ContextError> {
         match meta.kind {
             FunctionMetaKind::Function(data) => {
                 let mut docs = Docs::EMPTY;