Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[pigeon] Separates message call code generation into separate methods…
Browse files Browse the repository at this point in the history
… in the KotlinGenerator (flutter#5891)

Separates message call code generation into separate methods in the KotlinGenerator for flutter#134777. The ProxyApi generator uses similar code to the HostApi and FlutterApi, so this makes the code reusable.

From suggestion: flutter/packages#5544 (comment)
bparrishMines authored Jan 17, 2024
1 parent 669fd25 commit f2c1be0
Showing 4 changed files with 294 additions and 200 deletions.
4 changes: 4 additions & 0 deletions packages/pigeon/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 16.0.3

* [kotlin] Separates message call code generation into separate methods.

## 16.0.2

* [dart] Separates message call code generation into separate methods.
2 changes: 1 addition & 1 deletion packages/pigeon/lib/generator_tools.dart
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import 'ast.dart';
/// The current version of pigeon.
///
/// This must match the version in pubspec.yaml.
const String pigeonVersion = '16.0.2';
const String pigeonVersion = '16.0.3';

/// Read all the content from [stdin] to a String.
String readStdin() {
486 changes: 288 additions & 198 deletions packages/pigeon/lib/kotlin_generator.dart
Original file line number Diff line number Diff line change
@@ -359,77 +359,17 @@ class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
});

final String errorClassName = _getErrorClassName(generatorOptions);
for (final Method func in api.methods) {
final String returnType = func.returnType.isVoid
? 'Unit'
: _nullsafeKotlinTypeForDartType(func.returnType);
String sendArgument;

addDocumentationComments(
indent, func.documentationComments, _docCommentSpec);

if (func.parameters.isEmpty) {
indent.write(
'fun ${func.name}(callback: (Result<$returnType>) -> Unit) ');
sendArgument = 'null';
} else {
final Iterable<String> argTypes = func.parameters
.map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
final Iterable<String> argNames =
indexMap(func.parameters, _getSafeArgumentName);
final Iterable<String> enumSafeArgNames = indexMap(
func.parameters,
(int count, NamedType type) =>
_getEnumSafeArgumentExpression(count, type));
sendArgument = 'listOf(${enumSafeArgNames.join(', ')})';
final String argsSignature = map2(argTypes, argNames,
(String type, String name) => '$name: $type').join(', ');
indent.write(
'fun ${func.name}($argsSignature, callback: (Result<$returnType>) -> Unit) ');
}
indent.addScoped('{', '}', () {
const String channel = 'channel';
indent.writeln(
'val channelName = "${makeChannelName(api, func, dartPackageName)}"');
indent.writeln(
'val $channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)');
indent.writeScoped('$channel.send($sendArgument) {', '}', () {
indent.writeScoped('if (it is List<*>) {', '} ', () {
indent.writeScoped('if (it.size > 1) {', '} ', () {
indent.writeln(
'callback(Result.failure($errorClassName(it[0] as String, it[1] as String, it[2] as String?)))');
}, addTrailingNewline: false);
if (!func.returnType.isNullable && !func.returnType.isVoid) {
indent.addScoped('else if (it[0] == null) {', '} ', () {
indent.writeln(
'callback(Result.failure($errorClassName("null-error", "Flutter api returned null value for non-null return value.", "")))');
}, addTrailingNewline: false);
}
indent.addScoped('else {', '}', () {
if (func.returnType.isVoid) {
indent.writeln('callback(Result.success(Unit))');
} else {
const String output = 'output';
// Nullable enums require special handling.
if (func.returnType.isEnum && func.returnType.isNullable) {
indent.writeScoped(
'val $output = (it[0] as Int?)?.let {', '}', () {
indent.writeln('${func.returnType.baseName}.ofRaw(it)');
});
} else {
indent.writeln(
'val $output = ${_cast(indent, 'it[0]', type: func.returnType)}');
}
indent.writeln('callback(Result.success($output))');
}
});
}, addTrailingNewline: false);
indent.addScoped('else {', '} ', () {
indent.writeln(
'callback(Result.failure(createConnectionError(channelName)))');
});
});
});
for (final Method method in api.methods) {
_writeFlutterMethod(
indent,
name: method.name,
parameters: method.parameters,
returnType: method.returnType,
channelName: makeChannelName(api, method, dartPackageName),
documentationComments: method.documentationComments,
errorClassName: errorClassName,
dartPackageName: dartPackageName,
);
}
});
}
@@ -469,36 +409,14 @@ class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
indent.write('interface $apiName ');
indent.addScoped('{', '}', () {
for (final Method method in api.methods) {
final List<String> argSignature = <String>[];
if (method.parameters.isNotEmpty) {
final Iterable<String> argTypes = method.parameters
.map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
final Iterable<String> argNames =
method.parameters.map((NamedType e) => e.name);
argSignature.addAll(
map2(argTypes, argNames, (String argType, String argName) {
return '$argName: $argType';
}));
}

final String returnType = method.returnType.isVoid
? ''
: _nullsafeKotlinTypeForDartType(method.returnType);

final String resultType =
method.returnType.isVoid ? 'Unit' : returnType;
addDocumentationComments(
indent, method.documentationComments, _docCommentSpec);

if (method.isAsynchronous) {
argSignature.add('callback: (Result<$resultType>) -> Unit');
indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
} else if (method.returnType.isVoid) {
indent.writeln('fun ${method.name}(${argSignature.join(', ')})');
} else {
indent.writeln(
'fun ${method.name}(${argSignature.join(', ')}): $returnType');
}
_writeMethodDeclaration(
indent,
name: method.name,
documentationComments: method.documentationComments,
returnType: method.returnType,
parameters: method.parameters,
isAsynchronous: method.isAsynchronous,
);
}

indent.newln();
@@ -520,103 +438,16 @@ class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
'fun setUp(binaryMessenger: BinaryMessenger, api: $apiName?) ');
indent.addScoped('{', '}', () {
for (final Method method in api.methods) {
indent.write('run ');
indent.addScoped('{', '}', () {
String? taskQueue;
if (method.taskQueueType != TaskQueueType.serial) {
taskQueue = 'taskQueue';
indent.writeln(
'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
}

final String channelName =
makeChannelName(api, method, dartPackageName);

indent.write(
'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');

if (taskQueue != null) {
indent.addln(', $taskQueue)');
} else {
indent.addln(')');
}

indent.write('if (api != null) ');
indent.addScoped('{', '}', () {
final String messageVarName =
method.parameters.isNotEmpty ? 'message' : '_';

indent.write('channel.setMessageHandler ');
indent.addScoped('{ $messageVarName, reply ->', '}', () {
final List<String> methodArguments = <String>[];
if (method.parameters.isNotEmpty) {
indent.writeln('val args = message as List<Any?>');
enumerate(method.parameters, (int index, NamedType arg) {
final String argName = _getSafeArgumentName(index, arg);
final String argIndex = 'args[$index]';
indent.writeln(
'val $argName = ${_castForceUnwrap(argIndex, arg.type, indent)}');
methodArguments.add(argName);
});
}
final String call =
'api.${method.name}(${methodArguments.join(', ')})';

if (method.isAsynchronous) {
indent.write('$call ');
final String resultType = method.returnType.isVoid
? 'Unit'
: _nullsafeKotlinTypeForDartType(method.returnType);
indent.addScoped('{ result: Result<$resultType> ->', '}',
() {
indent.writeln('val error = result.exceptionOrNull()');
indent.writeScoped('if (error != null) {', '}', () {
indent.writeln('reply.reply(wrapError(error))');
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
final String enumTagNullablePrefix =
method.returnType.isNullable ? '?' : '!!';
final String enumTag = method.returnType.isEnum
? '$enumTagNullablePrefix.raw'
: '';
if (method.returnType.isVoid) {
indent.writeln('reply.reply(wrapResult(null))');
} else {
indent.writeln('val data = result.getOrNull()');
indent
.writeln('reply.reply(wrapResult(data$enumTag))');
}
});
});
} else {
indent.writeln('var wrapped: List<Any?>');
indent.write('try ');
indent.addScoped('{', '}', () {
if (method.returnType.isVoid) {
indent.writeln(call);
indent.writeln('wrapped = listOf<Any?>(null)');
} else {
String enumTag = '';
if (method.returnType.isEnum) {
final String safeUnwrap =
method.returnType.isNullable ? '?' : '';
enumTag = '$safeUnwrap.raw';
}
indent.writeln('wrapped = listOf<Any?>($call$enumTag)');
}
}, addTrailingNewline: false);
indent.add(' catch (exception: Throwable) ');
indent.addScoped('{', '}', () {
indent.writeln('wrapped = wrapError(exception)');
});
indent.writeln('reply.reply(wrapped)');
}
});
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
indent.writeln('channel.setMessageHandler(null)');
});
});
_writeHostMethodMessageHandler(
indent,
api: api,
name: method.name,
channelName: makeChannelName(api, method, dartPackageName),
taskQueueType: method.taskQueueType,
parameters: method.parameters,
returnType: method.returnType,
isAsynchronous: method.isAsynchronous,
);
}
});
});
@@ -756,6 +587,265 @@ class KotlinGenerator extends StructuredGenerator<KotlinOptions> {
}
_writeErrorClass(generatorOptions, indent);
}

static void _writeMethodDeclaration(
Indent indent, {
required String name,
required TypeDeclaration returnType,
required List<Parameter> parameters,
List<String> documentationComments = const <String>[],
int? minApiRequirement,
bool isAsynchronous = false,
bool isAbstract = false,
String Function(int index, NamedType type) getArgumentName =
_getArgumentName,
}) {
final List<String> argSignature = <String>[];
if (parameters.isNotEmpty) {
final Iterable<String> argTypes = parameters
.map((NamedType e) => _nullsafeKotlinTypeForDartType(e.type));
final Iterable<String> argNames = indexMap(parameters, getArgumentName);
argSignature.addAll(
map2(
argTypes,
argNames,
(String argType, String argName) {
return '$argName: $argType';
},
),
);
}

final String returnTypeString =
returnType.isVoid ? '' : _nullsafeKotlinTypeForDartType(returnType);

final String resultType = returnType.isVoid ? 'Unit' : returnTypeString;
addDocumentationComments(indent, documentationComments, _docCommentSpec);

if (minApiRequirement != null) {
indent.writeln(
'@androidx.annotation.RequiresApi(api = $minApiRequirement)',
);
}

final String abstractKeyword = isAbstract ? 'abstract ' : '';

if (isAsynchronous) {
argSignature.add('callback: (Result<$resultType>) -> Unit');
indent.writeln('${abstractKeyword}fun $name(${argSignature.join(', ')})');
} else if (returnType.isVoid) {
indent.writeln('${abstractKeyword}fun $name(${argSignature.join(', ')})');
} else {
indent.writeln(
'${abstractKeyword}fun $name(${argSignature.join(', ')}): $returnTypeString',
);
}
}

void _writeHostMethodMessageHandler(
Indent indent, {
required Api api,
required String name,
required String channelName,
required TaskQueueType taskQueueType,
required List<Parameter> parameters,
required TypeDeclaration returnType,
bool isAsynchronous = false,
String Function(List<String> safeArgNames, {required String apiVarName})?
onCreateCall,
}) {
indent.write('run ');
indent.addScoped('{', '}', () {
String? taskQueue;
if (taskQueueType != TaskQueueType.serial) {
taskQueue = 'taskQueue';
indent.writeln(
'val $taskQueue = binaryMessenger.makeBackgroundTaskQueue()');
}

indent.write(
'val channel = BasicMessageChannel<Any?>(binaryMessenger, "$channelName", codec');

if (taskQueue != null) {
indent.addln(', $taskQueue)');
} else {
indent.addln(')');
}

indent.write('if (api != null) ');
indent.addScoped('{', '}', () {
final String messageVarName = parameters.isNotEmpty ? 'message' : '_';

indent.write('channel.setMessageHandler ');
indent.addScoped('{ $messageVarName, reply ->', '}', () {
final List<String> methodArguments = <String>[];
if (parameters.isNotEmpty) {
indent.writeln('val args = message as List<Any?>');
enumerate(parameters, (int index, NamedType arg) {
final String argName = _getSafeArgumentName(index, arg);
final String argIndex = 'args[$index]';
indent.writeln(
'val $argName = ${_castForceUnwrap(argIndex, arg.type, indent)}');
methodArguments.add(argName);
});
}
final String call = onCreateCall != null
? onCreateCall(methodArguments, apiVarName: 'api')
: 'api.$name(${methodArguments.join(', ')})';

if (isAsynchronous) {
indent.write('$call ');
final String resultType = returnType.isVoid
? 'Unit'
: _nullsafeKotlinTypeForDartType(returnType);
indent.addScoped('{ result: Result<$resultType> ->', '}', () {
indent.writeln('val error = result.exceptionOrNull()');
indent.writeScoped('if (error != null) {', '}', () {
indent.writeln('reply.reply(wrapError(error))');
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
final String enumTagNullablePrefix =
returnType.isNullable ? '?' : '!!';
final String enumTag =
returnType.isEnum ? '$enumTagNullablePrefix.raw' : '';
if (returnType.isVoid) {
indent.writeln('reply.reply(wrapResult(null))');
} else {
indent.writeln('val data = result.getOrNull()');
indent.writeln('reply.reply(wrapResult(data$enumTag))');
}
});
});
} else {
indent.writeln('var wrapped: List<Any?>');
indent.write('try ');
indent.addScoped('{', '}', () {
if (returnType.isVoid) {
indent.writeln(call);
indent.writeln('wrapped = listOf<Any?>(null)');
} else {
String enumTag = '';
if (returnType.isEnum) {
final String safeUnwrap = returnType.isNullable ? '?' : '';
enumTag = '$safeUnwrap.raw';
}
indent.writeln('wrapped = listOf<Any?>($call$enumTag)');
}
}, addTrailingNewline: false);
indent.add(' catch (exception: Throwable) ');
indent.addScoped('{', '}', () {
indent.writeln('wrapped = wrapError(exception)');
});
indent.writeln('reply.reply(wrapped)');
}
});
}, addTrailingNewline: false);
indent.addScoped(' else {', '}', () {
indent.writeln('channel.setMessageHandler(null)');
});
});
}

void _writeFlutterMethod(
Indent indent, {
required String name,
required List<Parameter> parameters,
required TypeDeclaration returnType,
required String channelName,
required String errorClassName,
required String dartPackageName,
List<String> documentationComments = const <String>[],
int? minApiRequirement,
void Function(
Indent indent, {
required List<Parameter> parameters,
required TypeDeclaration returnType,
required String channelName,
required String errorClassName,
}) onWriteBody = _writeFlutterMethodMessageCall,
}) {
_writeMethodDeclaration(
indent,
name: name,
returnType: returnType,
parameters: parameters,
documentationComments: documentationComments,
isAsynchronous: true,
minApiRequirement: minApiRequirement,
getArgumentName: _getSafeArgumentName,
);

indent.addScoped('{', '}', () {
onWriteBody(
indent,
parameters: parameters,
returnType: returnType,
channelName: channelName,
errorClassName: errorClassName,
);
});
}

static void _writeFlutterMethodMessageCall(
Indent indent, {
required List<Parameter> parameters,
required TypeDeclaration returnType,
required String channelName,
required String errorClassName,
}) {
String sendArgument;

if (parameters.isEmpty) {
sendArgument = 'null';
} else {
final Iterable<String> enumSafeArgNames = indexMap(
parameters,
(int count, NamedType type) =>
_getEnumSafeArgumentExpression(count, type));
sendArgument = 'listOf(${enumSafeArgNames.join(', ')})';
}

const String channel = 'channel';
indent.writeln('val channelName = "$channelName"');
indent.writeln(
'val $channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)');
indent.writeScoped('$channel.send($sendArgument) {', '}', () {
indent.writeScoped('if (it is List<*>) {', '} ', () {
indent.writeScoped('if (it.size > 1) {', '} ', () {
indent.writeln(
'callback(Result.failure($errorClassName(it[0] as String, it[1] as String, it[2] as String?)))');
}, addTrailingNewline: false);
if (!returnType.isNullable && !returnType.isVoid) {
indent.addScoped('else if (it[0] == null) {', '} ', () {
indent.writeln(
'callback(Result.failure($errorClassName("null-error", "Flutter api returned null value for non-null return value.", "")))');
}, addTrailingNewline: false);
}
indent.addScoped('else {', '}', () {
if (returnType.isVoid) {
indent.writeln('callback(Result.success(Unit))');
} else {
const String output = 'output';
// Nullable enums require special handling.
if (returnType.isEnum && returnType.isNullable) {
indent.writeScoped('val $output = (it[0] as Int?)?.let {', '}',
() {
indent.writeln('${returnType.baseName}.ofRaw(it)');
});
} else {
indent.writeln(
'val $output = ${_cast(indent, 'it[0]', type: returnType)}');
}
indent.writeln('callback(Result.success($output))');
}
});
}, addTrailingNewline: false);
indent.addScoped('else {', '} ', () {
indent.writeln(
'callback(Result.failure(createConnectionError(channelName)))');
});
});
}
}

HostDatatype _getHostDatatype(Root root, NamedType field) {
2 changes: 1 addition & 1 deletion packages/pigeon/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ name: pigeon
description: Code generator tool to make communication between Flutter and the host platform type-safe and easier.
repository: https://github.com/flutter/packages/tree/main/packages/pigeon
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+pigeon%22
version: 16.0.2 # This must match the version in lib/generator_tools.dart
version: 16.0.3 # This must match the version in lib/generator_tools.dart

environment:
sdk: ">=3.0.0 <4.0.0"

0 comments on commit f2c1be0

Please sign in to comment.