[jnigen] Refactor C bindings generation (https://github.com/dart-lang/jnigen/issues/282)

diff --git a/pkgs/jnigen/lib/src/bindings/c_bindings.dart b/pkgs/jnigen/lib/src/bindings/c_bindings.dart
deleted file mode 100644
index 4f1cad5..0000000
--- a/pkgs/jnigen/lib/src/bindings/c_bindings.dart
+++ /dev/null
@@ -1,307 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import '../config/config.dart';
-import '../elements/elements.dart';
-import 'c_generator.dart';
-
-class CBindingGenerator {
-  static const classVarPrefix = '_c';
-  static const methodVarPrefix = '_m';
-  static const fieldVarPrefix = '_f';
-  static final indent = ' ' * 4;
-  static const jniResultType = 'JniResult';
-  static const ifError =
-      '(JniResult){.value = {.j = 0}, .exception = check_exception()}';
-
-  // These should be avoided in parameter names.
-  static const _cTypeKeywords = {
-    'short',
-    'char',
-    'int',
-    'long',
-    'float',
-    'double',
-  };
-
-  String _renameCParam(String paramName) =>
-      _cTypeKeywords.contains(paramName) ? '${paramName}0' : paramName;
-
-  CBindingGenerator(this.config);
-  Config config;
-
-  String generateBinding(ClassDecl c) => _class(c);
-
-  String _class(ClassDecl c) {
-    final s = StringBuffer();
-    final classNameInC = c.uniqueName;
-    // global variable in C that holds the reference to class
-    final classVar = '${classVarPrefix}_$classNameInC';
-    s.write('// ${c.binaryName}\n'
-        'jclass $classVar = NULL;\n\n');
-
-    for (var m in c.methods) {
-      s.write(_method(c, m));
-      s.writeln();
-    }
-
-    for (var f in c.fields) {
-      final fieldBinding = _field(c, f);
-      s.write(fieldBinding);
-      // Fields are skipped if they're static final. In that case
-      // do not write too much whitespace.
-      if (fieldBinding.isNotEmpty) s.writeln();
-    }
-    return s.toString();
-  }
-
-  String getCType(String binaryName) {
-    switch (binaryName) {
-      case "void":
-        return "void";
-      case "byte":
-        return "int8_t";
-      case "char":
-        return "uint16_t";
-      case "double":
-        return "double";
-      case "float":
-        return "float";
-      case "int":
-        return "int32_t";
-      case "long":
-        return "int64_t";
-      case "short":
-        return "int16_t";
-      case "boolean":
-        return "uint8_t";
-      default:
-        return "jobject";
-    }
-  }
-
-  String _method(ClassDecl c, Method m) {
-    final classNameInC = c.uniqueName;
-    final isACtor = m.isCtor;
-    final isStatic = m.isStatic;
-
-    final s = StringBuffer();
-    final cMethodName = m.accept(const CMethodName());
-    final classRef = '${classVarPrefix}_$classNameInC';
-    final methodID = '${methodVarPrefix}_$cMethodName';
-    final cMethodParams = _formalArgs(m);
-    final jniSignature = m.accept(const MethodSignature());
-    final ifStaticMethodID = isStatic ? 'static_' : '';
-
-    var javaReturnType = m.returnType.name;
-    if (isACtor) {
-      javaReturnType = c.binaryName;
-    }
-    final callType = _typeNameAtCallSite(m.returnType);
-    final callArgs = _callArgs(m, classRef, methodID);
-
-    var ifAssignResult = '';
-    if (javaReturnType != 'void') {
-      ifAssignResult = '${getCType(javaReturnType)} _result = ';
-    }
-
-    final ifStaticCall = isStatic ? 'Static' : '';
-    final envMethod =
-        isACtor ? 'NewObject' : 'Call$ifStaticCall${callType}Method';
-    final returnResultIfAny = _result(m);
-    s.write('''
-jmethodID $methodID = NULL;
-FFI_PLUGIN_EXPORT
-$jniResultType $cMethodName($cMethodParams) {
-    $_loadEnvCall
-    ${_loadClassCall(classRef, c.internalName)}
-    load_${ifStaticMethodID}method($classRef,
-      &$methodID, "${m.name}", "$jniSignature");
-    if ($methodID == NULL) return $ifError;
-    $ifAssignResult(*jniEnv)->$envMethod($callArgs);
-    $returnResultIfAny
-}\n''');
-    return s.toString();
-  }
-
-  String _field(ClassDecl c, Field f) {
-    final cClassName = c.uniqueName;
-    final isStatic = f.isStatic;
-
-    final fieldName = f.finalName;
-    final fieldNameInC = f.accept(const CFieldName());
-    final fieldVar = "${fieldVarPrefix}_$fieldNameInC";
-
-    // If the field is final and default is assigned, then no need to wrap
-    // this field. It should then be a constant in dart code.
-    if (isStatic && f.isFinal && f.defaultValue != null) {
-      return "";
-    }
-
-    final s = StringBuffer();
-
-    s.write('jfieldID $fieldVar = NULL;\n');
-
-    final classVar = '${classVarPrefix}_$cClassName';
-    void writeAccessor({bool isSetter = false}) {
-      const cReturnType = jniResultType;
-      final cMethodPrefix = isSetter ? 'set' : 'get';
-      final formalArgs = <String>[
-        if (!isStatic) 'jobject self_',
-        if (isSetter) '${getCType(f.type.name)} value',
-      ].join(', ');
-      final ifStaticField = isStatic ? 'static_' : '';
-      final ifStaticCall = isStatic ? 'Static' : '';
-      final callType = _typeNameAtCallSite(f.type);
-      final objectArgument = isStatic ? classVar : 'self_';
-
-      String accessorStatements;
-      if (isSetter) {
-        accessorStatements =
-            '$indent(*jniEnv)->Set$ifStaticCall${callType}Field(jniEnv, '
-            '$objectArgument, $fieldVar, value);\n'
-            '${indent}return $ifError;';
-      } else {
-        final getterExpr =
-            '(*jniEnv)->Get$ifStaticCall${callType}Field(jniEnv, '
-            '$objectArgument, $fieldVar)';
-        final cResultType = getCType(f.type.name);
-        final unionField = getJValueField(f.type);
-        final String returnExpr;
-        if (f.type.kind != Kind.primitive) {
-          returnExpr = 'to_global_ref_result(_result)';
-        } else {
-          returnExpr = '(JniResult){.value = '
-              '{.$unionField = _result}, .exception = check_exception()}';
-        }
-        accessorStatements = '$indent$cResultType _result = $getterExpr;\n'
-            '${indent}return $returnExpr;';
-      }
-
-      s.write('''
-FFI_PLUGIN_EXPORT
-$cReturnType ${cMethodPrefix}_$fieldNameInC($formalArgs) {
-    $_loadEnvCall
-    ${_loadClassCall(classVar, c.internalName)}
-    load_${ifStaticField}field($classVar, &$fieldVar, "$fieldName",
-      "${f.type.accept(const Descriptor())}");
-$accessorStatements
-}\n\n''');
-    }
-
-    writeAccessor(isSetter: false);
-    if (f.isFinal) {
-      return s.toString();
-    }
-    writeAccessor(isSetter: true);
-    return s.toString();
-  }
-
-  final String _loadEnvCall = '${indent}load_env();';
-
-  String _loadClassCall(String classVar, String internalName) {
-    return '${indent}load_class_global_ref(&$classVar, "$internalName");\n'
-        '${indent}if ($classVar == NULL) return $ifError;';
-  }
-
-  String _formalArgs(Method m) {
-    final args = <String>[];
-    if (!m.isCtor && !m.isStatic) {
-      // The underscore-suffixed name prevents accidental collision with
-      // parameter named self, if any.
-      args.add('jobject self_');
-    }
-
-    for (var param in m.params) {
-      final paramName = _renameCParam(param.name);
-      args.add('${getCType(param.type.name)} $paramName');
-    }
-
-    return args.join(", ");
-  }
-
-  String getJValueField(TypeUsage type) {
-    const primitives = {
-      'boolean': 'z',
-      'byte': 'b',
-      'short': 's',
-      'char': 'c',
-      'int': 'i',
-      'long': 'j',
-      'float': 'f',
-      'double': 'd',
-      'void': 'j', // in case of void return, just write 0 to largest field.
-    };
-    if (type.kind == Kind.primitive) {
-      return primitives[type.name]!;
-    }
-    return 'l';
-  }
-
-  // Returns arguments at call site, concatenated by `,`.
-  String _callArgs(Method m, String classVar, String methodVar) {
-    final args = ['jniEnv'];
-    if (!m.isCtor && !m.isStatic) {
-      args.add('self_');
-    } else {
-      args.add(classVar);
-    }
-    args.add(methodVar);
-    for (var param in m.params) {
-      final paramName = _renameCParam(param.name);
-      args.add(paramName);
-    }
-    return args.join(', ');
-  }
-
-  String _result(Method m) {
-    final cReturnType = getCType(m.returnType.name);
-    String valuePart;
-    String unionField;
-    if (cReturnType == 'jobject' || m.isCtor) {
-      return '${indent}return to_global_ref_result(_result);';
-    } else if (cReturnType == 'void') {
-      // in case of void return, just write 0 in result part of JniResult
-      unionField = 'j';
-      valuePart = '0';
-    } else {
-      unionField = getJValueField(m.returnType);
-      valuePart = '_result';
-    }
-    const exceptionPart = 'check_exception()';
-    return '${indent}return (JniResult){.value = {.$unionField = $valuePart}, '
-        '.exception = $exceptionPart};';
-  }
-
-  /// Returns capitalized java type name to be used as in call${type}Method
-  /// or get${type}Field etc..
-  String _typeNameAtCallSite(TypeUsage type) {
-    if (type.kind == Kind.primitive) {
-      return type.name.substring(0, 1).toUpperCase() + type.name.substring(1);
-    }
-    return "Object";
-  }
-}
-
-class CPreludes {
-  static const autoGeneratedNotice = '// Autogenerated by jnigen. '
-      'DO NOT EDIT!\n\n';
-  static const includes = '#include <stdint.h>\n'
-      '#include "jni.h"\n'
-      '#include "dartjni.h"\n'
-      '\n';
-  static const defines = 'thread_local JNIEnv *jniEnv;\n'
-      'JniContext *jni;\n\n'
-      'JniContext *(*context_getter)(void);\n'
-      'JNIEnv *(*env_getter)(void);\n'
-      '\n';
-  static const initializers = 'void setJniGetters(JniContext *(*cg)(void),\n'
-      '        JNIEnv *(*eg)(void)) {\n'
-      '    context_getter = cg;\n'
-      '    env_getter = eg;\n'
-      '}\n'
-      '\n';
-  static const prelude =
-      autoGeneratedNotice + includes + defines + initializers;
-}
diff --git a/pkgs/jnigen/lib/src/bindings/c_generator.dart b/pkgs/jnigen/lib/src/bindings/c_generator.dart
index 97b2604..ee30572 100644
--- a/pkgs/jnigen/lib/src/bindings/c_generator.dart
+++ b/pkgs/jnigen/lib/src/bindings/c_generator.dart
@@ -2,7 +2,13 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
+import 'dart:io';
+
+import '../config/config.dart';
 import '../elements/elements.dart';
+import '../logging/logging.dart';
+import '../util/find_package.dart';
+import '../util/string_util.dart';
 import 'visitor.dart';
 
 /// JVM representation of type signatures.
@@ -92,3 +98,350 @@
     return '${className}__$methodName';
   }
 }
+
+class CGenerator extends Visitor<Classes, Future<void>> {
+  static const _prelude = '''// Autogenerated by jnigen. DO NOT EDIT!
+
+#include <stdint.h>
+#include "jni.h"
+#include "dartjni.h"
+
+thread_local JNIEnv *jniEnv;
+JniContext *jni;
+
+JniContext *(*context_getter)(void);
+JNIEnv *(*env_getter)(void);
+
+void setJniGetters(JniContext *(*cg)(void),
+        JNIEnv *(*eg)(void)) {
+    context_getter = cg;
+    env_getter = eg;
+}
+
+''';
+
+  final Config config;
+
+  CGenerator(this.config);
+
+  Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
+      {String Function(String)? transform}) async {
+    final packagePath = await findPackageRoot(package);
+    if (packagePath != null) {
+      final sourceFile = File.fromUri(packagePath.resolve(relPath));
+      final targetFile = await File.fromUri(target).create(recursive: true);
+      var source = await sourceFile.readAsString();
+      if (transform != null) {
+        source = transform(source);
+      }
+      await targetFile.writeAsString(source);
+    } else {
+      log.warning('package $package not found! '
+          'skipped copying ${target.toFilePath()}');
+    }
+  }
+
+  @override
+  Future<void> visit(Classes node) async {
+    // Write C file and init file.
+    final cConfig = config.outputConfig.cConfig!;
+    final cRoot = cConfig.path;
+    final preamble = config.preamble;
+    log.info("Using c root = $cRoot");
+    final libraryName = cConfig.libraryName;
+    log.info('Creating dart init file ...');
+    // Create C file.
+    final subdir = cConfig.subdir ?? '.';
+    final cFileRelativePath = '$subdir/$libraryName.c';
+    final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
+        .create(recursive: true);
+    final cFileStream = cFile.openWrite();
+    // Write C Bindings.
+    if (preamble != null) {
+      cFileStream.writeln(preamble);
+    }
+    cFileStream.write(_prelude);
+    final classGenerator = _CClassGenerator(config, cFileStream);
+    for (final classDecl in node.decls.values) {
+      classDecl.accept(classGenerator);
+    }
+    await cFileStream.close();
+    log.info('Copying auxiliary files...');
+    for (final file in ['dartjni.h', '.clang-format']) {
+      await _copyFileFromPackage(
+          'jni', 'src/$file', cRoot.resolve('$subdir/$file'));
+    }
+    await _copyFileFromPackage(
+        'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'),
+        transform: (s) {
+      return s
+          .replaceAll('{{LIBRARY_NAME}}', libraryName)
+          .replaceAll('{{SUBDIR}}', subdir);
+    });
+    log.info('Running clang-format on C bindings');
+    try {
+      final clangFormat = Process.runSync('clang-format', ['-i', cFile.path]);
+      if (clangFormat.exitCode != 0) {
+        printError(clangFormat.stderr);
+        log.warning('clang-format exited with ${clangFormat.exitCode}');
+      }
+    } on ProcessException catch (e) {
+      log.warning('cannot run clang-format: $e');
+    }
+  }
+}
+
+const _classVarPrefix = '_c_';
+const _jniResultType = 'JniResult';
+const _loadEnvCall = 'load_env();';
+const _ifError =
+    '(JniResult){.value = {.j = 0}, .exception = check_exception()}';
+
+class _CClassGenerator extends Visitor<ClassDecl, void> {
+  final Config config;
+  final StringSink s;
+
+  _CClassGenerator(this.config, this.s);
+
+  @override
+  void visit(ClassDecl node) {
+    final classNameInC = node.uniqueName;
+    final classVar = '$_classVarPrefix$classNameInC';
+    // Global variable in C that holds the reference to class.
+    s.write('''// ${node.binaryName}
+jclass $classVar = NULL;
+
+''');
+
+    final methodGenerator = _CMethodGenerator(config, s);
+    for (final method in node.methods) {
+      method.accept(methodGenerator);
+    }
+
+    final fieldGenerator = _CFieldGenerator(config, s);
+    for (final field in node.fields) {
+      field.accept(fieldGenerator);
+    }
+  }
+}
+
+class _CLoadClassGenerator extends Visitor<ClassDecl, String> {
+  _CLoadClassGenerator();
+
+  @override
+  String visit(ClassDecl node) {
+    final classVar = '$_classVarPrefix${node.uniqueName}';
+    return '''    load_class_global_ref(&$classVar, "${node.internalName}");
+    if ($classVar == NULL) return $_ifError;''';
+  }
+}
+
+class _CMethodGenerator extends Visitor<Method, void> {
+  static const _methodVarPrefix = '_m_';
+
+  final Config config;
+  final StringSink s;
+
+  _CMethodGenerator(this.config, this.s);
+
+  @override
+  void visit(Method node) {
+    final classNameInC = node.classDecl.uniqueName;
+
+    final cMethodName = node.accept(const CMethodName());
+    final classRef = '$_classVarPrefix$classNameInC';
+    final methodId = '$_methodVarPrefix$cMethodName';
+    final cMethodParams = [
+      if (!node.isCtor && !node.isStatic) 'jobject self_',
+      ...node.params.accept(const _CParamGenerator(addReturnType: true)),
+    ].join(',');
+    final jniSignature = node.accept(const MethodSignature());
+    final ifStaticMethodID = node.isStatic ? 'static_' : '';
+
+    var javaReturnType = node.returnType.type;
+    if (node.isCtor) {
+      javaReturnType = DeclaredType(
+        binaryName: node.classDecl.binaryName,
+        simpleName: node.classDecl.simpleName,
+      );
+    }
+    final callType = node.returnType.accept(const _CTypeCallSite());
+    final callArgs = [
+      'jniEnv',
+      if (!node.isCtor && !node.isStatic) 'self_' else classRef,
+      methodId,
+      ...node.params.accept(const _CParamGenerator(addReturnType: false))
+    ].join(', ');
+
+    var ifAssignResult = '';
+    if (javaReturnType.name != 'void') {
+      ifAssignResult =
+          '${javaReturnType.accept(const _CReturnType())} _result = ';
+    }
+
+    final ifStaticCall = node.isStatic ? 'Static' : '';
+    final envMethod =
+        node.isCtor ? 'NewObject' : 'Call$ifStaticCall${callType}Method';
+    final returnResultIfAny = javaReturnType.accept(const _CResult());
+    s.write('''
+jmethodID $methodId = NULL;
+FFI_PLUGIN_EXPORT
+$_jniResultType $cMethodName($cMethodParams) {
+    $_loadEnvCall
+    ${node.classDecl.accept(_CLoadClassGenerator())}
+    load_${ifStaticMethodID}method($classRef,
+      &$methodId, "${node.name}", "$jniSignature");
+    if ($methodId == NULL) return $_ifError;
+    $ifAssignResult(*jniEnv)->$envMethod($callArgs);
+    $returnResultIfAny
+}
+
+''');
+  }
+}
+
+class _CFieldGenerator extends Visitor<Field, void> {
+  static const _fieldVarPrefix = '_f_';
+
+  final Config config;
+  final StringSink s;
+
+  _CFieldGenerator(this.config, this.s);
+
+  @override
+  void visit(Field node) {
+    final cClassName = node.classDecl.uniqueName;
+
+    final fieldName = node.finalName;
+    final fieldNameInC = node.accept(const CFieldName());
+    final fieldVar = "$_fieldVarPrefix$fieldNameInC";
+
+    // If the field is final and default is assigned, then no need to wrap
+    // this field. It should then be a constant in dart code.
+    if (node.isStatic && node.isFinal && node.defaultValue != null) {
+      return;
+    }
+
+    s.write('jfieldID $fieldVar = NULL;\n');
+
+    final classVar = '$_classVarPrefix$cClassName';
+    void writeAccessor({bool isSetter = false}) {
+      const cReturnType = _jniResultType;
+      final cMethodPrefix = isSetter ? 'set' : 'get';
+      final formalArgs = [
+        if (!node.isStatic) 'jobject self_',
+        if (isSetter) '${node.type.accept(const _CReturnType())} value',
+      ].join(', ');
+      final ifStaticField = node.isStatic ? 'static_' : '';
+      final ifStaticCall = node.isStatic ? 'Static' : '';
+      final callType = node.type.accept(const _CTypeCallSite());
+      final objectArgument = node.isStatic ? classVar : 'self_';
+
+      String accessorStatements;
+      if (isSetter) {
+        accessorStatements =
+            '    (*jniEnv)->Set$ifStaticCall${callType}Field(jniEnv, '
+            '$objectArgument, $fieldVar, value);\n'
+            '    return $_ifError;';
+      } else {
+        final getterExpr =
+            '(*jniEnv)->Get$ifStaticCall${callType}Field(jniEnv, '
+            '$objectArgument, $fieldVar)';
+        final cResultType = node.type.accept(const _CReturnType());
+        final result = node.type.accept(const _CResult());
+        accessorStatements = '''    $cResultType _result = $getterExpr;
+    $result''';
+      }
+      s.write('''
+FFI_PLUGIN_EXPORT
+$cReturnType ${cMethodPrefix}_$fieldNameInC($formalArgs) {
+    $_loadEnvCall
+    ${node.classDecl.accept(_CLoadClassGenerator())}
+    load_${ifStaticField}field($classVar, &$fieldVar, "$fieldName",
+      "${node.type.accept(const Descriptor())}");
+$accessorStatements
+}
+
+''');
+    }
+
+    writeAccessor(isSetter: false);
+    if (node.isFinal) {
+      return;
+    }
+    writeAccessor(isSetter: true);
+  }
+}
+
+class _CParamGenerator extends Visitor<Param, String> {
+  /// These should be avoided in parameter names.
+  static const _cTypeKeywords = {
+    'short',
+    'char',
+    'int',
+    'long',
+    'float',
+    'double',
+  };
+
+  const _CParamGenerator({required this.addReturnType});
+
+  final bool addReturnType;
+
+  @override
+  String visit(Param node) {
+    final paramName =
+        _cTypeKeywords.contains(node.name) ? '${node.name}0' : node.name;
+    final type = node.type.accept(const _CReturnType());
+    if (addReturnType) return '$type $paramName';
+    return paramName;
+  }
+}
+
+class _CReturnType extends TypeVisitor<String> {
+  const _CReturnType();
+
+  @override
+  String visitNonPrimitiveType(ReferredType node) {
+    return 'jobject';
+  }
+
+  @override
+  String visitPrimitiveType(PrimitiveType node) {
+    return node.cType;
+  }
+}
+
+class _CTypeCallSite extends TypeVisitor<String> {
+  const _CTypeCallSite();
+
+  @override
+  String visitNonPrimitiveType(ReferredType node) {
+    return 'Object';
+  }
+
+  @override
+  String visitPrimitiveType(PrimitiveType node) {
+    return node.name.capitalize();
+  }
+}
+
+class _CResult extends TypeVisitor<String> {
+  const _CResult();
+
+  @override
+  String visitNonPrimitiveType(ReferredType node) {
+    return 'return to_global_ref_result(_result);';
+  }
+
+  @override
+  String visitPrimitiveType(PrimitiveType node) {
+    if (node.name == 'void') {
+      return 'return $_ifError;';
+    }
+    // The union field is the same as the type's signature, but in lowercase.
+    final unionField = node.signature.toLowerCase();
+    return 'return (JniResult){.value = {.$unionField = _result}, '
+        '.exception = check_exception()};';
+  }
+}
diff --git a/pkgs/jnigen/lib/src/bindings/dart_generator.dart b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
index 326069a..1cd3c2b 100644
--- a/pkgs/jnigen/lib/src/bindings/dart_generator.dart
+++ b/pkgs/jnigen/lib/src/bindings/dart_generator.dart
@@ -9,7 +9,7 @@
 import '../config/config.dart';
 import '../elements/elements.dart';
 import '../logging/logging.dart';
-import '../writers/bindings_writer.dart';
+import '../util/string_util.dart';
 import 'c_generator.dart';
 import 'resolver.dart';
 import 'visitor.dart';
@@ -47,15 +47,6 @@
     '  /// The returned object must be deleted after use, '
     'by calling the `delete` method.';
 
-extension on String {
-  String capitalize() {
-    return '${this[0].toUpperCase()}${substring(1)}';
-  }
-
-  /// Reverses an ASCII string.
-  String get reversed => split('').reversed.join();
-}
-
 extension on Iterable<String> {
   /// Similar to [join] but adds the [separator] to the end as well.
   String delimited([String separator = '']) {
@@ -173,6 +164,19 @@
   static const preImportBoilerplate =
       autoGeneratedNotice + defaultLintSuppressions + defaultImports;
 
+  /// Run dart format command on [path].
+  Future<void> _runDartFormat(String path) async {
+    log.info('Running dart format...');
+    final formatRes = await Process.run('dart', ['format', path]);
+    // if negative exit code, likely due to an interrupt.
+    if (formatRes.exitCode > 0) {
+      log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
+          'This usually means there\'s a syntax error in bindings.\n'
+          'Please look at the generated files and report a bug: \n'
+          'https://github.com/dart-lang/jnigen/issues/new\n');
+    }
+  }
+
   @override
   Future<void> visit(Classes node) async {
     final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
@@ -192,9 +196,11 @@
         s.writeln(cInitCode);
       }
       final classGenerator = _ClassGenerator(config, s);
-      node.decls.values.accept(classGenerator).toList();
+      for (final classDecl in node.decls.values) {
+        classDecl.accept(classGenerator);
+      }
       await s.close();
-      await runDartFormat(file.path);
+      await _runDartFormat(file.path);
       return;
     }
     final files = <String, List<ClassDecl>>{};
@@ -247,9 +253,10 @@
         currentClass: fileClassName,
         inputClassNames: node.decls.keys.toSet(),
       );
-      classesInFile
-          .accept(_ClassGenerator(config, s, resolver: resolver))
-          .toList();
+      final classGenerator = _ClassGenerator(config, s, resolver: resolver);
+      for (final classDecl in classesInFile) {
+        classDecl.accept(classGenerator);
+      }
       dartFileStream.writeAll(resolver.getImportStrings(), '\n');
       dartFileStream.writeln(s.toString());
       await dartFileStream.close();
@@ -265,7 +272,7 @@
           packages[package]!.map((cls) => 'export "$cls.dart";').join('\n');
       exportFile.writeAsStringSync(exports);
     }
-    await runDartFormat(root.toFilePath());
+    await _runDartFormat(root.toFilePath());
     log.info('Completed.');
   }
 }
diff --git a/pkgs/jnigen/lib/src/elements/elements.dart b/pkgs/jnigen/lib/src/elements/elements.dart
index 2e4a9f5..7793c1f 100644
--- a/pkgs/jnigen/lib/src/elements/elements.dart
+++ b/pkgs/jnigen/lib/src/elements/elements.dart
@@ -278,7 +278,7 @@
       signature: 'C',
       dartType: 'int',
       jniType: 'jchar',
-      cType: 'char',
+      cType: 'uint16_t',
       ffiType: 'Uint16',
     ),
     'int': PrimitiveType._(
diff --git a/pkgs/jnigen/lib/src/generate_bindings.dart b/pkgs/jnigen/lib/src/generate_bindings.dart
index d553657..ab184da 100644
--- a/pkgs/jnigen/lib/src/generate_bindings.dart
+++ b/pkgs/jnigen/lib/src/generate_bindings.dart
@@ -5,6 +5,7 @@
 import 'dart:io';
 import 'dart:convert';
 
+import 'bindings/c_generator.dart';
 import 'bindings/dart_generator.dart';
 import 'bindings/excluder.dart';
 import 'bindings/linker.dart';
@@ -13,7 +14,6 @@
 import 'summary/summary.dart';
 import 'config/config.dart';
 import 'tools/tools.dart';
-import 'writers/bindings_writer.dart';
 import 'logging/logging.dart';
 
 void collectOutputStream(Stream<List<int>> stream, StringBuffer buffer) =>
@@ -34,14 +34,14 @@
     log.fatal(e.message);
   }
 
-  final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
   classes
     ..accept(Excluder(config))
     ..accept(Linker(config))
     ..accept(Renamer(config));
 
+  final cBased = config.outputConfig.bindingsType == BindingsType.cBased;
   if (cBased) {
-    await writeCBindings(config, classes.decls.values.toList());
+    await classes.accept(CGenerator(config));
   }
 
   try {
diff --git a/pkgs/jnigen/lib/src/util/string_util.dart b/pkgs/jnigen/lib/src/util/string_util.dart
new file mode 100644
index 0000000..5a65bfb
--- /dev/null
+++ b/pkgs/jnigen/lib/src/util/string_util.dart
@@ -0,0 +1,13 @@
+// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+extension StringUtil on String {
+  /// Makes the first letter uppercase.
+  String capitalize() {
+    return '${this[0].toUpperCase()}${substring(1)}';
+  }
+
+  /// Reverses an ASCII string.
+  String get reversed => split('').reversed.join();
+}
diff --git a/pkgs/jnigen/lib/src/writers/bindings_writer.dart b/pkgs/jnigen/lib/src/writers/bindings_writer.dart
deleted file mode 100644
index 190a90c..0000000
--- a/pkgs/jnigen/lib/src/writers/bindings_writer.dart
+++ /dev/null
@@ -1,88 +0,0 @@
-// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:io';
-
-import '../bindings/c_bindings.dart';
-import '../config/config.dart';
-import '../elements/elements.dart';
-import '../logging/logging.dart';
-import '../util/find_package.dart';
-
-/// Run dart format command on [path].
-Future<void> runDartFormat(String path) async {
-  log.info('Running dart format...');
-  final formatRes = await Process.run('dart', ['format', path]);
-  // if negative exit code, likely due to an interrupt.
-  if (formatRes.exitCode > 0) {
-    log.fatal('Dart format completed with exit code ${formatRes.exitCode} '
-        'This usually means there\'s a syntax error in bindings.\n'
-        'Please look at the generated files and report a bug.');
-  }
-}
-
-Future<void> _copyFileFromPackage(String package, String relPath, Uri target,
-    {String Function(String)? transform}) async {
-  final packagePath = await findPackageRoot(package);
-  if (packagePath != null) {
-    final sourceFile = File.fromUri(packagePath.resolve(relPath));
-    final targetFile = await File.fromUri(target).create(recursive: true);
-    var source = await sourceFile.readAsString();
-    if (transform != null) {
-      source = transform(source);
-    }
-    await targetFile.writeAsString(source);
-  } else {
-    log.warning('package $package not found! '
-        'skipped copying ${target.toFilePath()}');
-  }
-}
-
-Future<void> writeCBindings(Config config, List<ClassDecl> classes) async {
-  // write C file and init file
-  final cConfig = config.outputConfig.cConfig!;
-  final cRoot = cConfig.path;
-  final preamble = config.preamble;
-  log.info("Using c root = $cRoot");
-  final libraryName = cConfig.libraryName;
-  log.info('Creating dart init file ...');
-  // Create C file
-  final subdir = cConfig.subdir ?? '.';
-  final cFileRelativePath = '$subdir/$libraryName.c';
-  final cFile = await File.fromUri(cRoot.resolve(cFileRelativePath))
-      .create(recursive: true);
-  final cFileStream = cFile.openWrite();
-  // Write C Bindings
-  if (preamble != null) {
-    cFileStream.writeln(preamble);
-  }
-  cFileStream.write(CPreludes.prelude);
-  final cgen = CBindingGenerator(config);
-  final cBindings = classes.map(cgen.generateBinding).toList();
-  log.info('writing c bindings to $cFile');
-  cBindings.forEach(cFileStream.write);
-  await cFileStream.close();
-  log.info('Copying auxiliary files...');
-  await _copyFileFromPackage(
-      'jni', 'src/dartjni.h', cRoot.resolve('$subdir/dartjni.h'));
-  await _copyFileFromPackage(
-      'jni', 'src/.clang-format', cRoot.resolve('$subdir/.clang-format'));
-  await _copyFileFromPackage(
-      'jnigen', 'cmake/CMakeLists.txt.tmpl', cRoot.resolve('CMakeLists.txt'),
-      transform: (s) {
-    return s
-        .replaceAll('{{LIBRARY_NAME}}', libraryName)
-        .replaceAll('{{SUBDIR}}', subdir);
-  });
-  log.info('Running clang-format on C bindings');
-  try {
-    final clangFormat = Process.runSync('clang-format', ['-i', cFile.path]);
-    if (clangFormat.exitCode != 0) {
-      printError(clangFormat.stderr);
-      log.warning('clang-format exited with ${clangFormat.exitCode}');
-    }
-  } on ProcessException catch (e) {
-    log.warning('cannot run clang-format: $e');
-  }
-}