diff --git a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift
index 6b49774f..5e0c5b1c 100644
--- a/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift
+++ b/Sources/JExtractSwift/Swift2JavaTranslator+FunctionLowering.swift
@@ -16,6 +16,9 @@ import JavaTypes
 import SwiftSyntax
 
 extension Swift2JavaTranslator {
+  /// Lower the given function declaration to a C-compatible entrypoint,
+  /// providing all of the mappings between the parameter and result types
+  /// of the original function and its `@_cdecl` counterpart.
   @_spi(Testing)
   public func lowerFunctionSignature(
     _ decl: FunctionDeclSyntax,
@@ -124,9 +127,29 @@ extension Swift2JavaTranslator {
     parameterName: String
   ) throws -> LoweredParameters {
     switch type {
-    case .function, .metatype, .optional:
+    case .function, .optional:
       throw LoweringError.unhandledType(type)
 
+    case .metatype(let instanceType):
+      return LoweredParameters(
+        cdeclToOriginal: .unsafeCastPointer(
+          .passDirectly(parameterName),
+          swiftType: instanceType
+        ),
+        cdeclParameters: [
+          SwiftParameter(
+            convention: .byValue,
+            parameterName: parameterName,
+            type: .nominal(
+              SwiftNominalType(
+                nominalTypeDecl: swiftStdlibTypes.unsafeRawPointerDecl
+              )
+            )
+          )
+        ],
+        javaFFMParameters: [.SwiftPointer]
+      )
+
     case .nominal(let nominal):
       // Types from the Swift standard library that we know about.
       if nominal.nominalTypeDecl.moduleName == "Swift",
@@ -145,8 +168,12 @@ extension Swift2JavaTranslator {
       let mutable = (convention == .inout)
       let loweringStep: LoweringStep
       switch nominal.nominalTypeDecl.kind {
-      case .actor, .class: loweringStep = .passDirectly(parameterName)
-      case .enum, .struct, .protocol: loweringStep = .passIndirectly(parameterName)
+      case .actor, .class:
+        loweringStep = 
+          .unsafeCastPointer(.passDirectly(parameterName), swiftType: type)
+      case .enum, .struct, .protocol:
+        loweringStep =
+          .passIndirectly(.pointee( .typedPointer(.passDirectly(parameterName), swiftType: type)))
       }
 
       return LoweredParameters(
@@ -173,7 +200,7 @@ extension Swift2JavaTranslator {
         try lowerParameter(element, convention: convention, parameterName: name)
       }
       return LoweredParameters(
-        cdeclToOriginal: .tuplify(parameterNames.map { .passDirectly($0) }),
+        cdeclToOriginal: .tuplify(loweredElements.map { $0.cdeclToOriginal }),
         cdeclParameters: loweredElements.flatMap { $0.cdeclParameters },
         javaFFMParameters: loweredElements.flatMap { $0.javaFFMParameters }
       )
@@ -258,10 +285,9 @@ extension Swift2JavaTranslator {
       cdeclToOriginal = .passDirectly(parameterName)
 
     case (true, false):
-      // FIXME: Generic arguments, ugh
-      cdeclToOriginal = .suffixed(
-        .passDirectly(parameterName),
-        ".assumingMemoryBound(to: \(nominal.genericArguments![0]).self)"
+      cdeclToOriginal = .typedPointer(
+        .passDirectly(parameterName + "_pointer"),
+        swiftType: nominal.genericArguments![0]
       )
 
     case (false, true):
@@ -275,9 +301,9 @@ extension Swift2JavaTranslator {
         type,
         arguments: [
           LabeledArgument(label: "start",
-                 argument: .suffixed(
+                 argument: .typedPointer(
                     .passDirectly(parameterName + "_pointer"),
-                                ".assumingMemoryBound(to: \(nominal.genericArguments![0]).self")),
+                    swiftType: nominal.genericArguments![0])),
           LabeledArgument(label: "count",
                  argument: .passDirectly(parameterName + "_count"))
         ]
@@ -338,30 +364,113 @@ struct LabeledArgument<Element> {
 
 extension LabeledArgument: Equatable where Element: Equatable { }
 
-/// How to lower the Swift parameter
+/// Describes the transformation needed to take the parameters of a thunk
+/// and map them to the corresponding parameter (or result value) of the
+/// original function.
 enum LoweringStep: Equatable {
+  /// A direct reference to a parameter of the thunk.
   case passDirectly(String)
-  case passIndirectly(String)
-  indirect case suffixed(LoweringStep, String)
+
+  /// Cast the pointer described by the lowering step to the given
+  /// Swift type using `unsafeBitCast(_:to:)`.
+  indirect case unsafeCastPointer(LoweringStep, swiftType: SwiftType)
+
+  /// Assume at the untyped pointer described by the lowering step to the
+  /// given type, using `assumingMemoryBound(to:).`
+  indirect case typedPointer(LoweringStep, swiftType: SwiftType)
+
+  /// The thing to which the pointer typed, which is the `pointee` property
+  /// of the `Unsafe(Mutable)Pointer` types in Swift.
+  indirect case pointee(LoweringStep)
+
+  /// Pass this value indirectly, via & for explicit `inout` parameters.
+  indirect case passIndirectly(LoweringStep)
+
+  /// Initialize a value of the given Swift type with the set of labeled
+  /// arguments.
   case initialize(SwiftType, arguments: [LabeledArgument<LoweringStep>])
+
+  /// Produce a tuple with the given elements.
+  ///
+  /// This is used for exploding Swift tuple arguments into multiple
+  /// elements, recursively. Note that this always produces unlabeled
+  /// tuples, which Swift will convert to the labeled tuple form.
   case tuplify([LoweringStep])
 }
 
 struct LoweredParameters: Equatable {
-  /// The steps needed to get from the @_cdecl parameter to the original function
+  /// The steps needed to get from the @_cdecl parameters to the original function
   /// parameter.
   var cdeclToOriginal: LoweringStep
 
   /// The lowering of the parameters at the C level in Swift.
   var cdeclParameters: [SwiftParameter]
 
-  /// The lowerung of the parmaeters at the C level as expressed for Java's
+  /// The lowering of the parameters at the C level as expressed for Java's
   /// foreign function and memory interface.
   ///
   /// The elements in this array match up with those of 'cdeclParameters'.
   var javaFFMParameters: [ForeignValueLayout]
 }
 
+extension LoweredParameters {
+  /// Produce an expression that computes the argument for this parameter
+  /// when calling the original function from the cdecl entrypoint.
+  func cdeclToOriginalArgumentExpr(isSelf: Bool)-> ExprSyntax {
+    cdeclToOriginal.asExprSyntax(isSelf: isSelf)
+  }
+}
+
+extension LoweringStep {
+  func asExprSyntax(isSelf: Bool) -> ExprSyntax {
+    switch self {
+    case .passDirectly(let rawArgument):
+      return "\(raw: rawArgument)"
+
+    case .unsafeCastPointer(let step, swiftType: let swiftType):
+      let untypedExpr = step.asExprSyntax(isSelf: false)
+      return "unsafeBitCast(\(untypedExpr), to: \(swiftType.metatypeReferenceExprSyntax))"
+
+    case .typedPointer(let step, swiftType: let type):
+      let untypedExpr = step.asExprSyntax(isSelf: isSelf)
+      return "\(untypedExpr).assumingMemoryBound(to: \(type.metatypeReferenceExprSyntax))"
+
+    case .pointee(let step):
+      let untypedExpr = step.asExprSyntax(isSelf: isSelf)
+      return "\(untypedExpr).pointee"
+
+    case .passIndirectly(let step):
+      let innerExpr = step.asExprSyntax(isSelf: false)
+      return isSelf ? innerExpr : "&\(innerExpr)"
+
+    case .initialize(let type, arguments: let arguments):
+      let renderedArguments: [String] = arguments.map { labeledArgument in
+        let renderedArg = labeledArgument.argument.asExprSyntax(isSelf: false)
+        if let argmentLabel = labeledArgument.label {
+          return "\(argmentLabel): \(renderedArg.description)"
+        } else {
+          return renderedArg.description
+        }
+      }
+
+      // FIXME: Should be able to use structured initializers here instead
+      // of splatting out text.
+      let renderedArgumentList = renderedArguments.joined(separator: ", ")
+      return "\(raw: type.description)(\(raw: renderedArgumentList))"
+
+    case .tuplify(let elements):
+      let renderedElements: [String] = elements.map { element in
+        element.asExprSyntax(isSelf: false).description
+      }
+
+      // FIXME: Should be able to use structured initializers here instead
+      // of splatting out text.
+      let renderedElementList = renderedElements.joined(separator: ", ")
+      return "(\(raw: renderedElementList))"
+    }
+  }
+}
+
 enum LoweringError: Error {
   case inoutNotSupported(SwiftType)
   case unhandledType(SwiftType)
@@ -375,3 +484,74 @@ public struct LoweredFunctionSignature: Equatable {
   var parameters: [LoweredParameters]
   var result: LoweredParameters
 }
+
+extension LoweredFunctionSignature {
+  /// Produce the `@_cdecl` thunk for this lowered function signature that will
+  /// call into the original function.
+  @_spi(Testing)
+  public func cdeclThunk(cName: String, inputFunction: FunctionDeclSyntax) -> FunctionDeclSyntax {
+    var loweredCDecl = cdecl.createFunctionDecl(cName)
+
+    // Add the @_cdecl attribute.
+    let cdeclAttribute: AttributeSyntax = "@_cdecl(\(literal: cName))\n"
+    loweredCDecl.attributes.append(.attribute(cdeclAttribute))
+
+    // Create the body.
+
+    // Lower "self", if there is one.
+    let parametersToLower: ArraySlice<LoweredParameters>
+    let cdeclToOriginalSelf: ExprSyntax?
+    if original.selfParameter != nil {
+      cdeclToOriginalSelf = parameters[0].cdeclToOriginalArgumentExpr(isSelf: true)
+      parametersToLower = parameters[1...]
+    } else {
+      cdeclToOriginalSelf = nil
+      parametersToLower = parameters[...]
+    }
+
+    // Lower the remaining arguments.
+    // FIXME: Should be able to use structured initializers here instead
+    // of splatting out text.
+    let cdeclToOriginalArguments = zip(parametersToLower, original.parameters).map { lowering, originalParam in
+      let cdeclToOriginalArg = lowering.cdeclToOriginalArgumentExpr(isSelf: false)
+      if let argumentLabel = originalParam.argumentLabel {
+        return "\(argumentLabel): \(cdeclToOriginalArg.description)"
+      } else {
+        return cdeclToOriginalArg.description
+      }
+    }
+
+    // Form the call expression.
+    var callExpression: ExprSyntax = "\(inputFunction.name)(\(raw: cdeclToOriginalArguments.joined(separator: ", ")))"
+    if let cdeclToOriginalSelf {
+      callExpression = "\(cdeclToOriginalSelf).\(callExpression)"
+    }
+
+    // Handle the return.
+    if cdecl.result.type.isVoid && original.result.type.isVoid {
+      // Nothing to return.
+      loweredCDecl.body = """
+        {
+          \(callExpression)
+        }
+        """
+    } else if cdecl.result.type.isVoid {
+      // Indirect return. This is a regular return in Swift that turns
+      // into a
+      loweredCDecl.body = """
+        {
+          \(result.cdeclToOriginalArgumentExpr(isSelf: true)) = \(callExpression)
+        }
+        """
+    } else {
+      // Direct return.
+      loweredCDecl.body = """
+        {
+          return \(callExpression)
+        }
+        """
+    }
+
+    return loweredCDecl
+  }
+}
diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift
index 187c18a6..c2b626f2 100644
--- a/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift
+++ b/Sources/JExtractSwift/SwiftTypes/SwiftFunctionSignature.swift
@@ -19,6 +19,7 @@ import SwiftSyntaxBuilder
 /// parameters and return type.
 @_spi(Testing)
 public struct SwiftFunctionSignature: Equatable {
+  // FIXME: isStaticOrClass probably shouldn't be here?
   var isStaticOrClass: Bool
   var selfParameter: SwiftParameter?
   var parameters: [SwiftParameter]
@@ -30,9 +31,16 @@ extension SwiftFunctionSignature {
   /// signature.
   package func createFunctionDecl(_ name: String) -> FunctionDeclSyntax {
     let parametersStr = parameters.map(\.description).joined(separator: ", ")
-    let resultStr = result.type.description
+
+    let resultWithArrow: String
+    if result.type.isVoid {
+      resultWithArrow = ""
+    } else {
+      resultWithArrow = " -> \(result.type.description)"
+    }
+
     let decl: DeclSyntax = """
-      func \(raw: name)(\(raw: parametersStr)) -> \(raw: resultStr) {
+      func \(raw: name)(\(raw: parametersStr))\(raw: resultWithArrow) {
         // implementation
       }
       """
@@ -53,7 +61,7 @@ extension SwiftFunctionSignature {
       var isConsuming = false
       var isStaticOrClass = false
       for modifier in node.modifiers {
-        switch modifier.name {
+        switch modifier.name.tokenKind {
         case .keyword(.mutating): isMutating = true
         case .keyword(.static), .keyword(.class): isStaticOrClass = true
         case .keyword(.consuming): isConsuming = true
diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift
index b7bc28fb..35b7a0e1 100644
--- a/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift
+++ b/Sources/JExtractSwift/SwiftTypes/SwiftParameter.swift
@@ -58,23 +58,34 @@ enum SwiftParameterConvention: Equatable {
 
 extension SwiftParameter {
   init(_ node: FunctionParameterSyntax, symbolTable: SwiftSymbolTable) throws {
-    // Determine the convention. The default is by-value, but modifiers can alter
-    // this.
+    // Determine the convention. The default is by-value, but there are
+    // specifiers on the type for other conventions (like `inout`).
+    var type = node.type
     var convention = SwiftParameterConvention.byValue
-    for modifier in node.modifiers {
-      switch modifier.name {
-      case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
-        convention = .consuming
-      case .keyword(.inout):
-        convention = .inout
-      default:
-        break
+    if let attributedType = type.as(AttributedTypeSyntax.self) {
+      for specifier in attributedType.specifiers {
+        guard case .simpleTypeSpecifier(let simple) = specifier else {
+          continue
+        }
+
+        switch simple.specifier.tokenKind {
+        case .keyword(.consuming), .keyword(.__consuming), .keyword(.__owned):
+          convention = .consuming
+        case .keyword(.inout):
+          convention = .inout
+        default:
+          break
+        }
       }
+
+      // Ignore anything else in the attributed type.
+      // FIXME: We might want to check for these and ignore them.
+      type = attributedType.baseType
     }
     self.convention = convention
 
     // Determine the type.
-    self.type = try SwiftType(node.type, symbolTable: symbolTable)
+    self.type = try SwiftType(type, symbolTable: symbolTable)
 
     // FIXME: swift-syntax itself should have these utilities based on identifiers.
     if let secondName = node.secondName {
diff --git a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift
index 4eef607a..d83d5e63 100644
--- a/Sources/JExtractSwift/SwiftTypes/SwiftType.swift
+++ b/Sources/JExtractSwift/SwiftTypes/SwiftType.swift
@@ -33,15 +33,34 @@ enum SwiftType: Equatable {
   var asNominalTypeDeclaration: SwiftNominalTypeDeclaration? {
     asNominalType?.nominalTypeDecl
   }
+
+  /// Whether this is the "Void" type, which is actually an empty
+  /// tuple.
+  var isVoid: Bool {
+    return self == .tuple([])
+  }
 }
 
 extension SwiftType: CustomStringConvertible {
+  /// Whether forming a postfix type or expression to this Swift type
+  /// requires parentheses.
+  private var postfixRequiresParentheses: Bool {
+    switch self {
+    case .function: true
+    case .metatype, .nominal, .optional, .tuple: false
+    }
+  }
+
   var description: String {
     switch self {
     case .nominal(let nominal): return nominal.description
     case .function(let functionType): return functionType.description
     case .metatype(let instanceType):
-      return "(\(instanceType.description)).Type"
+      var instanceTypeStr = instanceType.description
+      if instanceType.postfixRequiresParentheses {
+        instanceTypeStr = "(\(instanceTypeStr))"
+      }
+      return "\(instanceTypeStr).Type"
     case .optional(let wrappedType):
       return "\(wrappedType.description)?"
     case .tuple(let elements):
@@ -213,4 +232,14 @@ extension SwiftType {
       )
     )
   }
+
+  /// Produce an expression that creates the metatype for this type in
+  /// Swift source code.
+  var metatypeReferenceExprSyntax: ExprSyntax {
+    let type: ExprSyntax = "\(raw: description)"
+    if postfixRequiresParentheses {
+      return "(\(type)).self"
+    }
+    return "\(type).self"
+  }
 }
diff --git a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
index ca101133..b5304b71 100644
--- a/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
+++ b/Tests/JExtractSwiftTests/Asserts/LoweringAssertions.swift
@@ -46,7 +46,8 @@ func assertLoweredFunction(
     inputFunction,
     enclosingType: enclosingType
   )
-  let loweredCDecl = loweredFunction.cdecl.createFunctionDecl(inputFunction.name.text)
+  let loweredCDecl = loweredFunction.cdeclThunk(cName: "c_\(inputFunction.name.text)", inputFunction: inputFunction)
+
   #expect(
     loweredCDecl.description == expectedCDecl.description,
     sourceLocation: Testing.SourceLocation(
diff --git a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift
index fd909565..08187914 100644
--- a/Tests/JExtractSwiftTests/FunctionLoweringTests.swift
+++ b/Tests/JExtractSwiftTests/FunctionLoweringTests.swift
@@ -17,6 +17,7 @@ import SwiftSyntax
 import SwiftSyntaxBuilder
 import Testing
 
+@Suite("Swift function lowering tests")
 final class FunctionLoweringTests {
   @Test("Lowering buffer pointers")
   func loweringBufferPointers() throws {
@@ -24,8 +25,9 @@ final class FunctionLoweringTests {
       func f(x: Int, y: Swift.Float, z: UnsafeBufferPointer<Bool>) { }
       """,
       expectedCDecl: """
-      func f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) -> () {
-        // implementation
+      @_cdecl("c_f")
+      func c_f(_ x: Int, _ y: Float, _ z_pointer: UnsafeRawPointer, _ z_count: Int) {
+        f(x: x, y: y, z: UnsafeBufferPointer<Bool>(start: z_pointer.assumingMemoryBound(to: Bool.self), count: z_count))
       }
       """
     )
@@ -34,16 +36,33 @@ final class FunctionLoweringTests {
   @Test("Lowering tuples")
   func loweringTuples() throws {
     try assertLoweredFunction("""
-      func f(t: (Int, (Float, Double)), z: UnsafePointer<Int>) { }
+      func f(t: (Int, (Float, Double)), z: UnsafePointer<Int>) -> Int { }
       """,
       expectedCDecl: """
-      func f(_ t_0: Int, _ t_1_0: Float, _ t_1_1: Double, _ z_pointer: UnsafeRawPointer) -> () {
-        // implementation
+      @_cdecl("c_f")
+      func c_f(_ t_0: Int, _ t_1_0: Float, _ t_1_1: Double, _ z_pointer: UnsafeRawPointer) -> Int {
+        return f(t: (t_0, (t_1_0, t_1_1)), z: z_pointer.assumingMemoryBound(to: Int.self))
       }
       """
     )
   }
 
+  @Test("Lowering functions involving inout")
+  func loweringInoutParameters() throws {
+    try assertLoweredFunction("""
+      func shift(point: inout Point, by delta: (Double, Double)) { }
+      """,
+      sourceFile: """
+      struct Point { }
+      """,
+      expectedCDecl: """
+      @_cdecl("c_shift")
+      func c_shift(_ point: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) {
+        shift(point: &point.assumingMemoryBound(to: Point.self).pointee, by: (delta_0, delta_1))
+      }
+      """
+    )
+  }
   @Test("Lowering methods")
   func loweringMethods() throws {
     try assertLoweredFunction("""
@@ -54,21 +73,59 @@ final class FunctionLoweringTests {
       """,
       enclosingType: "Point",
       expectedCDecl: """
-      func shifted(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer) -> () {
-        // implementation
+      @_cdecl("c_shifted")
+      func c_shifted(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double, _ _result: UnsafeMutableRawPointer) {
+        _result.assumingMemoryBound(to: Point.self).pointee = self.assumingMemoryBound(to: Point.self).pointee.shifted(by: (delta_0, delta_1))
+      }
+      """
+    )
+  }
+
+  @Test("Lowering mutating methods")
+  func loweringMutatingMethods() throws {
+    try assertLoweredFunction("""
+      mutating func shift(by delta: (Double, Double)) { }
+      """,
+      sourceFile: """
+      struct Point { }
+      """,
+      enclosingType: "Point",
+      expectedCDecl: """
+      @_cdecl("c_shift")
+      func c_shift(_ self: UnsafeMutableRawPointer, _ delta_0: Double, _ delta_1: Double) {
+        self.assumingMemoryBound(to: Point.self).pointee.shift(by: (delta_0, delta_1))
+      }
+      """
+    )
+  }
+
+  @Test("Lowering instance methods of classes")
+  func loweringInstanceMethodsOfClass() throws {
+    try assertLoweredFunction("""
+      func shift(by delta: (Double, Double)) { }
+      """,
+      sourceFile: """
+      class Point { }
+      """,
+      enclosingType: "Point",
+      expectedCDecl: """
+      @_cdecl("c_shift")
+      func c_shift(_ self: UnsafeRawPointer, _ delta_0: Double, _ delta_1: Double) {
+        unsafeBitCast(self, to: Point.self).shift(by: (delta_0, delta_1))
       }
       """
     )
   }
 
-  @Test("Lowering metatypes", .disabled("Metatypes are not yet lowered"))
+  @Test("Lowering metatypes")
   func lowerMetatype() throws {
     try assertLoweredFunction("""
       func f(t: Int.Type) { }
       """,
       expectedCDecl: """
-      func f(t: RawPointerType) -> () {
-        // implementation
+      @_cdecl("c_f")
+      func c_f(_ t: UnsafeRawPointer) {
+        f(t: unsafeBitCast(t, to: Int.self))
       }
       """
      )