blob: 61931b8cf76bee07362712d870edb0372a92272e [file]
// 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 'package:jnigen/src/elements/elements.dart';
import 'package:jnigen/src/util/rename_conflict.dart';
import 'symbol_resolver.dart';
/// Base class of both C based and Dart-only binding generators. Implements
/// Many methods used commonly by both of them.
///
/// These methods are in a superclass since they usually require access
/// to resolver, or for consistency with similar methods which require
/// the resolver.
abstract class BindingsGenerator {
// Name for reference in base class.
static const selfPointer = 'reference';
static final indent = ' ' * 2;
// import prefixes
static const ffi = 'ffi.';
static const jni = 'jni.';
static const String voidPointer = '${ffi}Pointer<${ffi}Void>';
static const String ffiVoidType = '${ffi}Void';
static const String jobjectType = '${jni}JObjectPtr';
static const String jthrowableType = '${jni}JThrowablePtr';
static const String jniObjectType = '${jni}JObject';
static const String jniArrayType = '${jni}JArray';
static const String jniCallType = '${jni}JniCallType';
static const String jniTypeType = '${jni}JType';
static const String typeClassSuffix = 'Type';
static const String typeClassPrefix = '_\$';
static const String jniResultType = '${jni}JniResult';
/// Generate bindings string for given class declaration.
String generateBindings(ClassDecl decl, SymbolResolver resolver);
/// Get common initialization code for bindings.
///
/// if this method returns an empty String, no init file is created.
String getInitFileContents();
/// Get boilerplate to be pasted on a file before any imports.
///
/// [initFilePath], if provided, shall point to the _init.dart file where
/// initialization code is stored. If this is null, the method should provide
/// standalone boilerplate which doesn't need an init file.
///
/// This method should return an empty string if no such boilerplate is
/// required.
String getPreImportBoilerplate([String? initFilePath]);
/// Get boilerplate to be pasted on a file before any imports.
///
/// See [getPreImportBoilerplate] for an explanation of [initFilePath].
///
/// This method should return an empty String if no such boilerplate is
/// required.
String getPostImportBoilerplate([String? initFilePath]);
/// Returns the formal parameters list of the generated function.
///
/// This is the signature seen by the user.
String getFormalArgs(Method m, SymbolResolver resolver) {
final List<String> args = [];
for (var param in m.params) {
args.add(
'${getDartOuterType(param.type, resolver)} ${kwRename(param.name)}');
}
return args.join(', ');
}
/// Actual arguments passed to native call.
String actualArgs(Method m, {bool addSelf = true}) {
final List<String> args = [if (hasSelfParam(m) && addSelf) selfPointer];
for (var param in m.params) {
final paramName = kwRename(param.name);
args.add(toNativeArg(paramName, param.type));
}
return args.join(', ');
}
String dartSigForField(Field f,
{bool isSetter = false, required bool isFfiSig}) {
final conv = isFfiSig ? getDartFfiType : getDartInnerType;
final ref = f.modifiers.contains('static') ? '' : '$jobjectType, ';
if (isSetter) {
return '$jthrowableType Function($ref${conv(f.type)})';
}
return '$jniResultType Function($ref)';
}
String dartArrayExtension(ClassDecl decl) {
final name = decl.finalName;
return '\nextension \$${name}Array on $jniArrayType<$name> {\n'
'$indent$name operator [](int index) {\n'
'${indent * 2}return $name.fromRef(elementAt(index, ${jni}JniCallType.objectType).object);\n'
'$indent}\n\n'
'${indent}void operator []=(int index, $name value) {\n'
'${indent * 2}(this as $jniArrayType<$jniObjectType>)[index] = value;\n'
'$indent}\n'
'}\n';
}
String dartTypeClass(ClassDecl decl) {
final name = decl.finalName;
final signature = getSignature(decl.binaryName);
final typeClassName = '$typeClassPrefix$name$typeClassSuffix';
return '\nclass $typeClassName extends $jniTypeType<$name> {\n'
'${indent}const $typeClassName();\n\n'
'$indent@override\n'
'${indent}String get signature => r"$signature";\n'
'}\n';
}
String dartStaticTypeGetter(ClassDecl decl) {
final name = decl.finalName;
final typeClassName = '$typeClassPrefix$name$typeClassSuffix';
return '\n$indent/// The type which includes information such as the signature of this class.\n'
'${indent}static const $jniTypeType<$name> type = $typeClassName();\n';
}
String dartSigForMethod(Method m, {required bool isFfiSig}) {
final conv = isFfiSig ? getDartFfiType : getDartInnerType;
final argTypes = [if (hasSelfParam(m)) voidPointer];
for (var param in m.params) {
argTypes.add(conv(param.type));
}
return '$jniResultType Function (${argTypes.join(", ")})';
}
String _dartType(TypeUsage t, {SymbolResolver? resolver}) {
// if resolver == null, looking for inner fn type, type of fn reference
// else looking for outer fn type, that's what user of the library sees.
const primitives = {
'byte': 'int',
'short': 'int',
'char': 'int',
'int': 'int',
'long': 'int',
'float': 'double',
'double': 'double',
'void': 'void',
'boolean': 'bool',
};
const jniTypes = {
'byte': 'JByte',
'short': 'JShort',
'char': 'JChar',
'int': 'JInt',
'long': 'JLong',
'float': 'JFloat',
'double': 'JDouble',
'boolean': 'JBoolean',
};
switch (t.kind) {
case Kind.primitive:
if (t.name == 'boolean' && resolver == null) return 'int';
return primitives[(t.type as PrimitiveType).name]!;
case Kind.typeVariable:
case Kind.wildcard:
throw SkipException('Not supported: generics');
case Kind.array:
if (resolver != null) {
final innerType = (t.type as ArrayType).type;
final dartType = innerType.kind == Kind.primitive
? jni + jniTypes[(innerType.type as PrimitiveType).name]!
: _dartType(innerType, resolver: resolver);
return '$jniArrayType<$dartType>';
}
return voidPointer;
case Kind.declared:
if (resolver != null) {
return resolver.resolve((t.type as DeclaredType).binaryName) ??
jniObjectType;
}
return voidPointer;
}
}
/// Get corresponding Dart FFI type of Java type.
String getDartFfiType(TypeUsage t) {
const primitives = {
'byte': 'Int8',
'short': 'Int16',
'char': 'Uint16',
'int': 'Int32',
'long': 'Int64',
'float': 'Float',
'double': 'Double',
'void': 'Void',
'boolean': 'Uint8',
};
switch (t.kind) {
case Kind.primitive:
return ffi + primitives[(t.type as PrimitiveType).name]!;
case Kind.typeVariable:
case Kind.wildcard:
throw SkipException(
'Generic type parameters are not supported', t.name);
case Kind.array:
case Kind.declared:
return voidPointer;
}
}
String getDartInnerType(TypeUsage t) => _dartType(t);
String getDartOuterType(TypeUsage t, SymbolResolver resolver) =>
_dartType(t, resolver: resolver);
String getDartLiteral(dynamic value) {
if (value is String) {
// TODO(#31): escape string literal.
return '"$value"'.replaceAll('\\', '\\\\');
}
if (value is int || value is double || value is bool) {
return value.toString();
}
throw SkipException('Not a constant of a known type.');
}
String getJValueAccessor(TypeUsage type) {
const primitives = {
'boolean': 'boolean',
'byte': 'byte',
'short': 'short',
'char': 'char',
'int': 'integer',
'long': 'long',
'float': 'float',
'double': 'doubleFloat',
'void': 'check()',
};
if (isPrimitive(type)) {
return primitives[type.name]!;
}
return 'object';
}
String getOriginalFieldDecl(Field f) {
final declStmt = '${f.type.shorthand} ${f.name}';
return [...f.modifiers, declStmt].join(' ');
}
String getOriginalMethodHeader(Method m) {
final args = <String>[];
for (var p in m.params) {
args.add('${p.type.shorthand} ${p.name}');
}
final declStmt = '${m.returnType.shorthand} ${m.name}'
'(${args.join(', ')})';
return [...m.modifiers, declStmt].join(' ');
}
String toNativeArg(String name, TypeUsage type,
{bool convertBooleanToInt = true}) {
if (isPrimitive(type)) {
return (type.name == 'boolean' && convertBooleanToInt)
? '$name ? 1 : 0'
: name;
}
return '$name.$selfPointer';
}
String toDartResult(String expr, TypeUsage type, String dartType) {
if (isPrimitive(type)) {
return expr;
}
return '$dartType.fromRef($expr)';
}
static final deleteInstruction =
'$indent/// The returned object must be deleted after use, '
'by calling the `delete` method.\n';
String getCallType(TypeUsage returnType) {
if (isPrimitive(returnType)) {
return "$jniCallType.${returnType.name}Type";
}
return "$jniCallType.objectType";
}
}
String breakDocComment(JavaDocComment? javadoc, {String depth = ' '}) {
final link = RegExp('{@link ([^{}]+)}');
if (javadoc == null) return '';
final comment = javadoc.comment
.replaceAllMapped(link, (match) => match.group(1) ?? '')
.replaceAll('#', '\\#')
.replaceAll('<p>', '')
.replaceAll('</p>', '\n')
.replaceAll('<b>', '__')
.replaceAll('</b>', '__')
.replaceAll('<em>', '_')
.replaceAll('</em>', '_');
return '$depth///\n'
'$depth/// ${comment.replaceAll('\n', '\n$depth///')}\n';
}
/// class name canonicalized for C bindings, by replacing "." with "_" and
/// "$" with "__".
String getUniqueClassName(ClassDecl decl) {
if (!decl.isPreprocessed) {
throw StateError("class not preprocessed: ${decl.binaryName}");
}
return decl.uniqueName;
}
/// Returns the name of the class member as referred to by C bindings
String getMemberNameInC(ClassDecl decl, String name) =>
"${getUniqueClassName(decl)}__$name";
String getCType(String binaryName) {
switch (binaryName) {
case "void":
return "void";
case "byte":
return "int8_t";
case "char":
return "char";
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 getInternalName(String binaryName) {
switch (binaryName) {
case "void":
return "V";
case "byte":
return "B";
case "char":
return "C";
case "double":
return "D";
case "float":
return "F";
case "int":
return "I";
case "long":
return "J";
case "short":
return "S";
case "boolean":
return "Z";
default:
return binaryName.replaceAll(".", "/");
}
}
String getSignature(String binaryName) {
switch (binaryName) {
case "void":
return "V";
case "byte":
return "B";
case "char":
return "C";
case "double":
return "D";
case "float":
return "F";
case "int":
return "I";
case "long":
return "J";
case "short":
return "S";
case "boolean":
return "Z";
default:
return 'L${binaryName.replaceAll(".", "/")};';
}
}
String getDescriptor(TypeUsage usage, {bool escapeDollarSign = false}) {
switch (usage.kind) {
case Kind.declared:
return getSignature((usage.type as DeclaredType).binaryName);
case Kind.primitive:
return getSignature((usage.type as PrimitiveType).name);
case Kind.typeVariable:
// It should be possible to compute the erasure of a type
// in parser itself.
// TODO(#23): Use erasure of the type variable here.
// This is just a (wrong) placeholder
return "Ljava/lang/Object;";
case Kind.array:
final inner = getDescriptor((usage.type as ArrayType).type);
return "[$inner";
case Kind.wildcard:
final extendsBound = (usage.type as Wildcard).extendsBound;
if (extendsBound != null) {
return getDescriptor(extendsBound);
}
return 'Ljava/lang/Object;';
}
}
bool isPrimitive(TypeUsage t) => t.kind == Kind.primitive;
bool isVoid(TypeUsage t) => isPrimitive(t) && t.name == 'void';
bool isStaticField(Field f) => f.modifiers.contains('static');
bool isStaticMethod(Method m) => m.modifiers.contains('static');
bool isFinalField(Field f) => f.modifiers.contains('final');
bool isFinalMethod(Method m) => m.modifiers.contains('final');
bool isCtor(Method m) => m.name == '<init>';
// static methods & constructors do not have self param.
bool hasSelfParam(Method m) => !isStaticMethod(m) && !isCtor(m);
bool isObjectField(Field f) => !isPrimitive(f.type);
bool isObjectMethod(Method m) => !isPrimitive(m.returnType);
/// Returns class name as useful in dart.
///
/// Eg -> a.b.X.Y -> X_Y
String getSimplifiedClassName(String binaryName) =>
binaryName.split('.').last.replaceAll('\$', '_');
// Marker exception when a method or class cannot be translated
// The inner functions may not know how much context has to be skipped in case
// of an error or unknown element. They throw SkipException.
class SkipException implements Exception {
SkipException(this.message, [this.element]);
String message;
dynamic element;
@override
String toString() {
return '$message;';
}
}
String getTypeNameAtCallSite(TypeUsage t) {
if (isPrimitive(t)) {
return t.name.substring(0, 1).toUpperCase() + t.name.substring(1);
}
return "Object";
}
String getResultGetterName(TypeUsage returnType) {
final primitives = {
'boolean': 'boolean',
'byte': 'byte',
'short': 'short',
'char': 'char',
'int': 'integer',
'long': 'long',
'float': 'float',
'double': 'doubleFloat',
'void': 'check()',
};
return primitives[returnType.name] ?? 'object';
}
/// Returns the JNI signature of the method.
String getJniSignatureForMethod(Method m) {
final s = StringBuffer();
s.write('(');
for (var param in m.params) {
final type = getDescriptor(param.type);
s.write(type);
}
s.write(')');
final returnType = getDescriptor(m.returnType);
s.write(returnType);
return s.toString();
}