Add a type-level encoding of qualified paths
diff --git a/macro/src/lib.rs b/macro/src/lib.rs
index b56f58e..bc8d19b 100644
--- a/macro/src/lib.rs
+++ b/macro/src/lib.rs
@@ -11,10 +11,11 @@
 
 mod expand;
 mod syntax;
+mod type_id;
 
 use crate::syntax::namespace::Namespace;
 use proc_macro::TokenStream;
-use syn::{parse_macro_input, ItemMod};
+use syn::{parse_macro_input, ItemMod, LitStr};
 
 /// `#[cxx::bridge] mod ffi { ... }`
 ///
@@ -44,3 +45,9 @@
         .unwrap_or_else(|err| err.to_compile_error())
         .into()
 }
+
+#[proc_macro]
+pub fn type_id(input: TokenStream) -> TokenStream {
+    let arg = parse_macro_input!(input as LitStr);
+    type_id::expand(arg).into()
+}
diff --git a/macro/src/type_id.rs b/macro/src/type_id.rs
new file mode 100644
index 0000000..15a5833
--- /dev/null
+++ b/macro/src/type_id.rs
@@ -0,0 +1,29 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote};
+use syn::LitStr;
+
+// "folly::File" => `(f, o, l, l, y, (), F, i, l, e)`
+pub fn expand(arg: LitStr) -> TokenStream {
+    let mut ids = Vec::new();
+
+    for word in arg.value().split("::") {
+        if !ids.is_empty() {
+            ids.push(quote!(()));
+        }
+        for ch in word.chars() {
+            ids.push(match ch {
+                'A'..='Z' | 'a'..='z' => {
+                    let t = format_ident!("{}", ch);
+                    quote!(::cxx::private::#t)
+                }
+                '0'..='9' | '_' => {
+                    let t = format_ident!("_{}", ch);
+                    quote!(::cxx::private::#t)
+                }
+                _ => quote!([(); #ch as _]),
+            });
+        }
+    }
+
+    quote! { (#(#ids,)*) }
+}