| // Copyright (c) 2020, 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:ffi'; |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:ffi/ffi.dart'; |
| import 'package:ffigen/src/code_generator.dart'; |
| import 'package:ffigen/src/header_parser/data.dart'; |
| import 'package:ffigen/src/header_parser/includer.dart'; |
| import 'package:ffigen/src/strings.dart' as strings; |
| import 'package:logging/logging.dart'; |
| import 'package:path/path.dart' as p; |
| |
| import '../clang_bindings/clang_bindings.dart' as clang_types; |
| import '../utils.dart'; |
| |
| final _logger = Logger('ffigen.header_parser.macro_parser'); |
| |
| Pointer< |
| NativeFunction< |
| Int32 Function( |
| clang_types.CXCursor, clang_types.CXCursor, Pointer<Void>)>>? |
| _macroVariablevisitorPtr; |
| |
| /// Adds a macro definition to be parsed later. |
| void saveMacroDefinition(clang_types.CXCursor cursor) { |
| final macroUsr = cursor.usr(); |
| final originalMacroName = cursor.spelling(); |
| if (clang.clang_Cursor_isMacroBuiltin(cursor) == 0 && |
| clang.clang_Cursor_isMacroFunctionLike(cursor) == 0 && |
| shouldIncludeMacro(macroUsr, originalMacroName)) { |
| // Parse macro only if it's not builtin or function-like. |
| _logger.fine( |
| "++++ Saved Macro '$originalMacroName' for later : ${cursor.completeStringRepr()}"); |
| final prefixedName = config.macroDecl.renameUsingConfig(originalMacroName); |
| bindingsIndex.addMacroToSeen(macroUsr, prefixedName); |
| _saveMacro(prefixedName, macroUsr, originalMacroName); |
| } |
| } |
| |
| /// Saves a macro to be parsed later. |
| /// |
| /// Macros are parsed later in [parseSavedMacros()]. |
| void _saveMacro(String name, String usr, String originalName) { |
| savedMacros[name] = Macro(usr, originalName); |
| } |
| |
| List<Constant>? _bindings; |
| |
| /// Macros cannot be parsed directly, so we create a new `.hpp` file in which |
| /// they are assigned to a variable after which their value can be determined |
| /// by evaluating the value of the variable. |
| List<Constant>? parseSavedMacros() { |
| _bindings = []; |
| |
| if (savedMacros.keys.isEmpty) { |
| return _bindings; |
| } |
| |
| // Create a file for parsing macros; |
| final file = createFileForMacros(); |
| |
| final index = clang.clang_createIndex(0, 0); |
| Pointer<Pointer<Utf8>> clangCmdArgs = nullptr; |
| var cmdLen = 0; |
| |
| final compilerOpts = config.compilerOpts; |
| clangCmdArgs = createDynamicStringArray(compilerOpts); |
| |
| cmdLen = config.compilerOpts.length; |
| final tu = clang.clang_parseTranslationUnit( |
| index, |
| file.path.toNativeUtf8().cast(), |
| clangCmdArgs.cast(), |
| cmdLen, |
| nullptr, |
| 0, |
| clang_types.CXTranslationUnit_Flags.CXTranslationUnit_KeepGoing, |
| ); |
| |
| if (tu == nullptr) { |
| _logger.severe('Unable to parse Macros.'); |
| } else { |
| logTuDiagnostics(tu, _logger, file.path, logLevel: Level.FINEST); |
| final rootCursor = clang.clang_getTranslationUnitCursor(tu); |
| |
| final resultCode = clang.clang_visitChildren( |
| rootCursor, |
| _macroVariablevisitorPtr ??= Pointer.fromFunction( |
| _macroVariablevisitor, exceptional_visitor_return), |
| nullptr, |
| ); |
| |
| visitChildrenResultChecker(resultCode); |
| } |
| |
| clang.clang_disposeTranslationUnit(tu); |
| clang.clang_disposeIndex(index); |
| // Delete the temp file created for macros. |
| file.deleteSync(); |
| |
| return _bindings; |
| } |
| |
| /// Child visitor invoked on translationUnitCursor for parsing macroVariables. |
| int _macroVariablevisitor(clang_types.CXCursor cursor, |
| clang_types.CXCursor parent, Pointer<Void> clientData) { |
| Constant? constant; |
| try { |
| if (isFromGeneratedFile(cursor) && |
| _macroVarNames.contains(cursor.spelling()) && |
| cursor.kind == clang_types.CXCursorKind.CXCursor_VarDecl) { |
| final e = clang.clang_Cursor_Evaluate(cursor); |
| final k = clang.clang_EvalResult_getKind(e); |
| _logger.fine('macroVariablevisitor: ${cursor.completeStringRepr()}'); |
| |
| /// Get macro name, the variable name starts with '<macro-name>_'. |
| final macroName = MacroVariableString.decode(cursor.spelling()); |
| switch (k) { |
| case clang_types.CXEvalResultKind.CXEval_Int: |
| constant = Constant( |
| usr: savedMacros[macroName]!.usr, |
| originalName: savedMacros[macroName]!.originalName, |
| name: macroName, |
| rawType: 'int', |
| rawValue: clang.clang_EvalResult_getAsLongLong(e).toString(), |
| ); |
| break; |
| case clang_types.CXEvalResultKind.CXEval_Float: |
| constant = Constant( |
| usr: savedMacros[macroName]!.usr, |
| originalName: savedMacros[macroName]!.originalName, |
| name: macroName, |
| rawType: 'double', |
| rawValue: |
| _writeDoubleAsString(clang.clang_EvalResult_getAsDouble(e)), |
| ); |
| break; |
| case clang_types.CXEvalResultKind.CXEval_StrLiteral: |
| final rawValue = _getWrittenRepresentation( |
| macroName, |
| clang.clang_EvalResult_getAsStr(e), |
| ); |
| constant = Constant( |
| usr: savedMacros[macroName]!.usr, |
| originalName: savedMacros[macroName]!.originalName, |
| name: macroName, |
| rawType: 'String', |
| rawValue: "'$rawValue'", |
| ); |
| break; |
| } |
| clang.clang_EvalResult_dispose(e); |
| |
| if (constant != null) { |
| _bindings!.add(constant); |
| } |
| } |
| } catch (e, s) { |
| _logger.severe(e); |
| _logger.severe(s); |
| rethrow; |
| } |
| return clang_types.CXChildVisitResult.CXChildVisit_Continue; |
| } |
| |
| /// Returns true if cursor is from generated file. |
| bool isFromGeneratedFile(clang_types.CXCursor cursor) { |
| final s = cursor.sourceFileName(); |
| return p.basename(s) == _generatedFileBaseName; |
| } |
| |
| /// Base name of generated file. |
| String? _generatedFileBaseName; |
| |
| /// Generated macro variable names. |
| /// |
| /// Used to determine if macro should be included in bindings or not. |
| late Set<String> _macroVarNames; |
| |
| /// Creates a temporary file for parsing macros in current directory. |
| File createFileForMacros() { |
| final fileNameBase = p.join(strings.tmpDir, 'temp_for_macros'); |
| final fileExt = 'hpp'; |
| |
| // Find a filename which doesn't already exist. |
| var file = File('$fileNameBase.$fileExt'); |
| var i = 0; |
| while (file.existsSync()) { |
| i++; |
| file = File('${fileNameBase}_$i.$fileExt'); |
| } |
| |
| // Create file. |
| file.createSync(); |
| |
| // Save generated name. |
| _generatedFileBaseName = p.basename(file.path); |
| |
| // Write file contents. |
| final sb = StringBuffer(); |
| for (final h in config.headers.entryPoints) { |
| final fullHeaderPath = File(h).absolute.path; |
| sb.writeln('#include "$fullHeaderPath"'); |
| } |
| |
| _macroVarNames = {}; |
| for (final prefixedMacroName in savedMacros.keys) { |
| // Write macro. |
| final macroVarName = MacroVariableString.encode(prefixedMacroName); |
| sb.writeln( |
| 'auto $macroVarName = ${savedMacros[prefixedMacroName]!.originalName};'); |
| // Add to _macroVarNames. |
| _macroVarNames.add(macroVarName); |
| } |
| final macroFileContent = sb.toString(); |
| // Log this generated file for debugging purpose. |
| // We use the finest log because this file may be very big. |
| _logger.finest('=====FILE FOR MACROS===='); |
| _logger.finest(macroFileContent); |
| _logger.finest('========================'); |
| |
| file.writeAsStringSync(macroFileContent); |
| return file; |
| } |
| |
| /// Deals with encoding/decoding name of the variable generated for a Macro. |
| class MacroVariableString { |
| static String encode(String s) { |
| return '_${s.length}_${s}_generated_macro_variable'; |
| } |
| |
| static String decode(String s) { |
| // Remove underscore. |
| s = s.substring(1); |
| final intReg = RegExp('[0-9]+'); |
| final lengthEnd = intReg.matchAsPrefix(s)!.end; |
| final len = int.parse(s.substring(0, lengthEnd)); |
| |
| // Name starts after an unerscore. |
| final nameStart = lengthEnd + 1; |
| return s.substring(nameStart, nameStart + len); |
| } |
| } |
| |
| /// Gets a written representation string of a C string. |
| /// |
| /// E.g- For a string "Hello\nWorld", The new line character is converted to \n. |
| /// Note: The string is considered to be Utf8, but is treated as Extended ASCII, |
| /// if the conversion fails. |
| String _getWrittenRepresentation(String macroName, Pointer<Char> strPtr) { |
| final sb = StringBuffer(); |
| try { |
| // Consider string to be Utf8 encoded by default. |
| sb.clear(); |
| // This throws a Format Exception if string isn't Utf8 so that we handle it |
| // in the catch block. |
| final result = strPtr.cast<Utf8>().toDartString(); |
| for (final s in result.runes) { |
| sb.write(_getWritableChar(s)); |
| } |
| } catch (e) { |
| // Handle string if it isn't Utf8. String is considered to be |
| // Extended ASCII in this case. |
| _logger.warning( |
| "Couldn't decode Macro string '$macroName' as Utf8, using ASCII instead."); |
| sb.clear(); |
| final length = strPtr.cast<Utf8>().length; |
| final charList = Uint8List.view( |
| strPtr.cast<Uint8>().asTypedList(length).buffer, 0, length); |
| |
| for (final char in charList) { |
| sb.write(_getWritableChar(char, utf8: false)); |
| } |
| } |
| |
| return sb.toString(); |
| } |
| |
| /// Creates a writable char from [char] code. |
| /// |
| /// E.g- `\` is converted to `\\`. |
| String _getWritableChar(int char, {bool utf8 = true}) { |
| /// Handle control characters. |
| if (char >= 0 && char < 32 || char == 127) { |
| /// Handle these - `\b \t \n \v \f \r` as special cases. |
| switch (char) { |
| case 8: // \b |
| return r'\b'; |
| case 9: // \t |
| return r'\t'; |
| case 10: // \n |
| return r'\n'; |
| case 11: // \v |
| return r'\v'; |
| case 12: // \f |
| return r'\f'; |
| case 13: // \r |
| return r'\r'; |
| default: |
| final h = char.toRadixString(16).toUpperCase().padLeft(2, '0'); |
| return '\\x$h'; |
| } |
| } |
| |
| /// Handle characters - `$ ' \` these need to be escaped when writing to file. |
| switch (char) { |
| case 36: // $ |
| return r'\$'; |
| case 39: // ' |
| return r"\'"; |
| case 92: // \ |
| return r'\\'; |
| } |
| |
| /// In case encoding is not Utf8, we know all characters will fall in [0..255] |
| /// Print range [128..255] as `\xHH`. |
| if (!utf8) { |
| final h = char.toRadixString(16).toUpperCase().padLeft(2, '0'); |
| return '\\x$h'; |
| } |
| |
| /// In all other cases, simply convert to string. |
| return String.fromCharCode(char); |
| } |
| |
| /// Converts a double to a string, handling cases like Infinity and NaN. |
| String _writeDoubleAsString(double d) { |
| if (d.isFinite) { |
| return d.toString(); |
| } else { |
| // The only Non-Finite numbers are Infinity, NegativeInfinity and NaN. |
| if (d.isInfinite) { |
| return d.isNegative |
| ? strings.doubleNegativeInfinity |
| : strings.doubleInfinity; |
| } |
| return strings.doubleNaN; |
| } |
| } |