/* * Copyright (C) 2021, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "rust_writer.h" #include #include #include #include #include "Collation.h" #include "utils.h" // Note that we prepend _ to variable names to avoid using Rust language keyword. // E.g., a variable named "type" would not compile. namespace android { namespace stats_log_api_gen { const char* rust_type_name(java_type_t type, bool lifetime) { switch (type) { case JAVA_TYPE_BOOLEAN: return "bool"; case JAVA_TYPE_INT: case JAVA_TYPE_ENUM: return "i32"; case JAVA_TYPE_LONG: return "i64"; case JAVA_TYPE_FLOAT: return "f32"; case JAVA_TYPE_DOUBLE: return "f64"; case JAVA_TYPE_STRING: if (lifetime) { return "&'a str"; } else { return "&str"; } case JAVA_TYPE_BYTE_ARRAY: if (lifetime) { return "&'a [u8]"; } else { return "&[u8]"; } default: return "UNKNOWN"; } } static string make_camel_case_name(const string& str) { string result; const int N = str.size(); bool justSawUnderscore = false; for (int i = 0; i < N; i++) { const char c = str[i]; if (c == '_') { justSawUnderscore = true; // Don't add the underscore to our result } else if (i == 0 || justSawUnderscore) { result += toupper(c); justSawUnderscore = false; } else { result += tolower(c); } } return result; } static string make_snake_case_name(const string& str) { string result; const int N = str.size(); for (int i = 0; i < N; i++) { const char c = str[i]; if (isupper(c)) { if (i > 0) { result += "_"; } result += tolower(c); } else { result += c; } } return result; } static string get_variable_name(const string& str) { // From https://doc.rust-lang.org/reference/identifiers.html. if (str == "crate" || str == "self" || str == "super" || str == "Self") { return make_snake_case_name(str) + "_"; } else { return "r#" + make_snake_case_name(str); } } static void write_rust_method_signature(FILE* out, const char* namePrefix, const AtomDecl& atomDecl, const AtomDecl& attributionDecl, bool isCode, bool isNonChained, const char* headerCrate) { // To make the generated code pretty, add newlines between arguments. const char* separator = (isCode ? "\n" : " "); if (isCode) { fprintf(out, " pub fn %s(", namePrefix); } else { fprintf(out, " %s::%s(", atomDecl.name.c_str(), namePrefix); } if (isCode) { fprintf(out, "\n"); } if (atomDecl.atomType == ATOM_TYPE_PULLED) { if (isCode) { fprintf(out, " "); } fprintf(out, "pulled_data_: &mut AStatsEventList,%s", separator); } for (int i = 0; i < atomDecl.fields.size(); i++) { const AtomField& atomField = atomDecl.fields[i]; const java_type_t& type = atomField.javaType; if (type == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (int j = 0; j < attributionDecl.fields.size(); j++) { const AtomField& chainField = attributionDecl.fields[j]; if (isCode) { fprintf(out, " "); } fprintf(out, "%s_chain_: &[%s],%s", chainField.name.c_str(), rust_type_name(chainField.javaType, false), separator); } } else { if (isCode) { fprintf(out, " "); } // Other arguments can have the same name as these non-chained ones, // so append something. if (isNonChained && i < 2) { fprintf(out, "%s_non_chained_", atomField.name.c_str()); } else { fprintf(out, "%s", get_variable_name(atomField.name).c_str()); } if (type == JAVA_TYPE_ENUM) { fprintf(out, ": %s,%s", make_camel_case_name(atomField.name).c_str(), separator); } else { fprintf(out, ": %s,%s", rust_type_name(type, false), separator); } } } fprintf(out, " ) -> %s::StatsResult", headerCrate); if (isCode) { fprintf(out, " {"); } fprintf(out, "\n"); } static bool write_rust_usage(FILE* out, const string& method_name, const shared_ptr& atom, const AtomDecl& attributionDecl, bool isNonChained, const char* headerCrate) { fprintf(out, " // Definition: "); write_rust_method_signature(out, method_name.c_str(), *atom, attributionDecl, false, isNonChained, headerCrate); return true; } static void write_rust_atom_constants(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, const char* headerCrate) { fprintf(out, "// Constants for atom codes.\n"); fprintf(out, "#[derive(Clone, Copy)]\n"); fprintf(out, "pub enum Atoms {\n"); std::map atom_code_to_non_chained_decl_map; build_non_chained_decl_map(atoms, &atom_code_to_non_chained_decl_map); for (const shared_ptr& atomDecl : atoms.decls) { const string constant = make_camel_case_name(atomDecl->name); fprintf(out, "\n"); fprintf(out, " // %s %s\n", atomDecl->message.c_str(), atomDecl->name.c_str()); const bool isSupported = write_rust_usage(out, "// stats_write", atomDecl, attributionDecl, false, headerCrate); if (!isSupported) { continue; } auto non_chained_decl = atom_code_to_non_chained_decl_map.find(atomDecl->code); if (non_chained_decl != atom_code_to_non_chained_decl_map.end()) { write_rust_usage(out, "stats_write_non_chained", *non_chained_decl->second, attributionDecl, true, headerCrate); } fprintf(out, " %s = %d,\n", constant.c_str(), atomDecl->code); } fprintf(out, "\n"); fprintf(out, "}\n"); fprintf(out, "\n"); } static void write_rust_atom_constant_values(FILE* out, const shared_ptr& atomDecl) { bool hasConstants = false; for (const AtomField& field : atomDecl->fields) { if (field.javaType == JAVA_TYPE_ENUM) { fprintf(out, " #[repr(i32)]\n"); fprintf(out, " #[derive(Clone, Copy, Eq, PartialEq)]\n"); fprintf(out, " pub enum %s {\n", make_camel_case_name(field.name).c_str()); for (map::const_iterator value = field.enumValues.begin(); value != field.enumValues.end(); value++) { fprintf(out, " %s = %d,\n", make_camel_case_name(value->second).c_str(), value->first); } fprintf(out, " }\n"); hasConstants = true; } } if (hasConstants) { fprintf(out, "\n"); } } static void write_rust_annotation_constants(FILE* out) { fprintf(out, "// Annotation constants.\n"); // The annotation names are all prefixed with AnnotationId. // Ideally that would go into the enum name instead, // but I don't want to modify the actual name strings in case they change in the future. fprintf(out, "#[allow(unused, clippy::enum_variant_names)]\n"); fprintf(out, "#[repr(u8)]\n"); fprintf(out, "enum Annotations {\n"); const map& ANNOTATION_ID_CONSTANTS = get_annotation_id_constants(ANNOTATION_CONSTANT_NAME_PREFIX); for (const auto& [id, annotation] : ANNOTATION_ID_CONSTANTS) { fprintf(out, " %s = %hhu,\n", make_camel_case_name(annotation.name).c_str(), id); } fprintf(out, "}\n\n"); } // This is mostly copied from the version in native_writer with some minor changes. // Note that argIndex is 1 for the first argument. static void write_annotations(FILE* out, int argIndex, const AtomDecl& atomDecl, const string& methodPrefix, const string& methodSuffix) { const map& ANNOTATION_ID_CONSTANTS = get_annotation_id_constants(ANNOTATION_CONSTANT_NAME_PREFIX); auto annotationsIt = atomDecl.fieldNumberToAnnotations.find(argIndex); if (annotationsIt == atomDecl.fieldNumberToAnnotations.end()) { return; } int resetState = -1; int defaultState = -1; for (const shared_ptr& annotation : annotationsIt->second) { const string& annotationConstant = ANNOTATION_ID_CONSTANTS.at(annotation->annotationId).name; switch (annotation->type) { case ANNOTATION_TYPE_INT: if (ANNOTATION_ID_TRIGGER_STATE_RESET == annotation->annotationId) { resetState = annotation->value.intValue; } else if (ANNOTATION_ID_DEFAULT_STATE == annotation->annotationId) { defaultState = annotation->value.intValue; } else { fprintf(out, " %saddInt32Annotation(%scrate::Annotations::%s as u8, " "%d);\n", methodPrefix.c_str(), methodSuffix.c_str(), make_camel_case_name(annotationConstant).c_str(), annotation->value.intValue); } break; case ANNOTATION_TYPE_BOOL: fprintf(out, " %saddBoolAnnotation(%scrate::Annotations::%s as u8, %s);\n", methodPrefix.c_str(), methodSuffix.c_str(), make_camel_case_name(annotationConstant).c_str(), annotation->value.boolValue ? "true" : "false"); break; default: break; } } if (defaultState != -1 && resetState != -1) { const string& annotationConstant = ANNOTATION_ID_CONSTANTS.at(ANNOTATION_ID_TRIGGER_STATE_RESET).name; const AtomField& field = atomDecl.fields[argIndex - 1]; if (field.javaType == JAVA_TYPE_ENUM) { fprintf(out, " if %s as i32 == %d {\n", get_variable_name(field.name).c_str(), resetState); } else { fprintf(out, " if %s == %d {\n", get_variable_name(field.name).c_str(), resetState); } fprintf(out, " %saddInt32Annotation(%scrate::Annotations::%s as u8, %d);\n", methodPrefix.c_str(), methodSuffix.c_str(), make_camel_case_name(annotationConstant).c_str(), defaultState); fprintf(out, " }\n"); } } static int write_rust_method_body(FILE* out, const AtomDecl& atomDecl, const AtomDecl& attributionDecl, const int minApiLevel, const char* headerCrate) { fprintf(out, " unsafe {\n"); if (minApiLevel == API_Q) { fprintf(stderr, "TODO: Do we need to handle this case?"); return 1; } if (atomDecl.atomType == ATOM_TYPE_PUSHED) { fprintf(out, " let __event = AStatsEvent_obtain();\n"); fprintf(out, " let __dropper = crate::AStatsEventDropper(__event);\n"); } else { fprintf(out, " let __event = AStatsEventList_addStatsEvent(pulled_data_);\n"); } fprintf(out, " AStatsEvent_setAtomId(__event, %s::Atoms::%s as u32);\n", headerCrate, make_camel_case_name(atomDecl.name).c_str()); write_annotations(out, ATOM_ID_FIELD_NUMBER, atomDecl, "AStatsEvent_", "__event, "); for (int i = 0; i < atomDecl.fields.size(); i++) { const AtomField& atomField = atomDecl.fields[i]; const string& name = get_variable_name(atomField.name); const java_type_t& type = atomField.javaType; switch (type) { case JAVA_TYPE_ATTRIBUTION_CHAIN: { const char* uidName = attributionDecl.fields.front().name.c_str(); const char* tagName = attributionDecl.fields.back().name.c_str(); fprintf(out, " let uids = %s_chain_.iter().map(|n| (*n).try_into())" ".collect::, _>>()?;\n" " let str_arr = %s_chain_.iter().map(|s| " "std::ffi::CString::new(*s))" ".collect::, _>>()?;\n" " let ptr_arr = str_arr.iter().map(|s| s.as_ptr())" ".collect::>();\n" " AStatsEvent_writeAttributionChain(__event, uids.as_ptr(),\n" " ptr_arr.as_ptr(), %s_chain_.len().try_into()?);\n", uidName, tagName, uidName); break; } case JAVA_TYPE_BYTE_ARRAY: fprintf(out, " AStatsEvent_writeByteArray(__event, " "%s.as_ptr(), %s.len());\n", name.c_str(), name.c_str()); break; case JAVA_TYPE_BOOLEAN: fprintf(out, " AStatsEvent_writeBool(__event, %s);\n", name.c_str()); break; case JAVA_TYPE_ENUM: fprintf(out, " AStatsEvent_writeInt32(__event, %s as i32);\n", name.c_str()); break; case JAVA_TYPE_INT: fprintf(out, " AStatsEvent_writeInt32(__event, %s);\n", name.c_str()); break; case JAVA_TYPE_FLOAT: fprintf(out, " AStatsEvent_writeFloat(__event, %s);\n", name.c_str()); break; case JAVA_TYPE_LONG: fprintf(out, " AStatsEvent_writeInt64(__event, %s);\n", name.c_str()); break; case JAVA_TYPE_STRING: fprintf(out, " let str = std::ffi::CString::new(%s)?;\n", name.c_str()); fprintf(out, " AStatsEvent_writeString(__event, str.as_ptr());\n"); break; default: // Unsupported types: OBJECT, DOUBLE fprintf(stderr, "Encountered unsupported type: %d.", type); return 1; } // write_annotations expects the first argument to have an index of 1. write_annotations(out, i + 1, atomDecl, "AStatsEvent_", "__event, "); } if (atomDecl.atomType == ATOM_TYPE_PUSHED) { fprintf(out, " let __ret = AStatsEvent_write(__event);\n"); fprintf(out, " if __ret >= 0 { %s::StatsResult::Ok(()) }" " else { Err(%s::StatsError::Return(__ret)) }\n", headerCrate, headerCrate); } else { fprintf(out, " AStatsEvent_build(__event);\n"); fprintf(out, " %s::StatsResult::Ok(())\n", headerCrate); } fprintf(out, " }\n"); return 0; } static int write_rust_stats_write_method(FILE* out, const shared_ptr& atomDecl, const AtomDecl& attributionDecl, const int minApiLevel, const char* headerCrate) { if (atomDecl->atomType == ATOM_TYPE_PUSHED) { write_rust_method_signature(out, "stats_write", *atomDecl, attributionDecl, true, false, headerCrate); } else { write_rust_method_signature(out, "add_astats_event", *atomDecl, attributionDecl, true, false, headerCrate); } const int ret = write_rust_method_body(out, *atomDecl, attributionDecl, minApiLevel, headerCrate); if (ret != 0) { return ret; } fprintf(out, " }\n\n"); return 0; } static void write_rust_stats_write_non_chained_method(FILE* out, const shared_ptr& atomDecl, const AtomDecl& attributionDecl, const char* headerCrate) { write_rust_method_signature(out, "stats_write_non_chained", *atomDecl, attributionDecl, true, true, headerCrate); fprintf(out, " stats_write("); for (int i = 0; i < atomDecl->fields.size(); i++) { if (i != 0) { fprintf(out, ", "); } const AtomField& atomField = atomDecl->fields[i]; const java_type_t& type = atomField.javaType; if (i < 2) { // The first two args are attribution chains. fprintf(out, "&[%s_non_chained_]", atomField.name.c_str()); } else if (type == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (int j = 0; j < attributionDecl.fields.size(); j++) { const AtomField& chainField = attributionDecl.fields[j]; if (i != 0 || j != 0) { fprintf(out, ", "); } fprintf(out, "&[%s_chain_]", chainField.name.c_str()); } } else { fprintf(out, "%s", get_variable_name(atomField.name).c_str()); } } fprintf(out, ")\n"); fprintf(out, " }\n\n"); } static bool needs_lifetime(const shared_ptr& atomDecl) { for (const AtomField& atomField : atomDecl->fields) { const java_type_t& type = atomField.javaType; if (type == JAVA_TYPE_ATTRIBUTION_CHAIN || type == JAVA_TYPE_STRING || type == JAVA_TYPE_BYTE_ARRAY) { return true; } } return false; } static void write_rust_struct(FILE* out, const shared_ptr& atomDecl, const AtomDecl& attributionDecl, const char* headerCrate) { // Write the struct. const bool lifetime = needs_lifetime(atomDecl); if (lifetime) { fprintf(out, " pub struct %s<'a> {\n", make_camel_case_name(atomDecl->name).c_str()); } else { fprintf(out, " pub struct %s {\n", make_camel_case_name(atomDecl->name).c_str()); } for (const AtomField& atomField : atomDecl->fields) { const java_type_t& type = atomField.javaType; if (type == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (const AtomField& chainField : attributionDecl.fields) { fprintf(out, " pub %s_chain_: &'a [%s],\n", chainField.name.c_str(), rust_type_name(chainField.javaType, true)); } } else { fprintf(out, " pub %s:", get_variable_name(atomField.name).c_str()); if (type == JAVA_TYPE_ENUM) { fprintf(out, " %s,\n", make_camel_case_name(atomField.name).c_str()); } else { fprintf(out, " %s,\n", rust_type_name(type, true)); } } } fprintf(out, " }\n"); // Write the impl const bool isPush = atomDecl->atomType == ATOM_TYPE_PUSHED; if (isPush) { if (lifetime) { fprintf(out, " impl<'a> %s<'a> {\n", make_camel_case_name(atomDecl->name).c_str()); } else { fprintf(out, " impl %s {\n", make_camel_case_name(atomDecl->name).c_str()); } } else { if (lifetime) { fprintf(out, " impl<'a> %s::Stat for %s<'a> {\n", headerCrate, make_camel_case_name(atomDecl->name).c_str()); } else { fprintf(out, " impl %s::Stat for %s {\n", headerCrate, make_camel_case_name(atomDecl->name).c_str()); } } fprintf(out, " #[inline(always)]\n"); if (isPush) { fprintf(out, " pub fn stats_write(&self)" " -> %s::StatsResult {\n", headerCrate); fprintf(out, " stats_write("); } else { fprintf(out, " fn add_astats_event(&self, pulled_data: &mut AStatsEventList)" " -> %s::StatsResult {\n", headerCrate); fprintf(out, " add_astats_event(pulled_data, "); } for (const AtomField& atomField : atomDecl->fields) { const java_type_t& type = atomField.javaType; if (type == JAVA_TYPE_ATTRIBUTION_CHAIN) { for (const AtomField& chainField : attributionDecl.fields) { fprintf(out, "self.%s_chain_, ", chainField.name.c_str()); } } else { fprintf(out, "self.%s, ", get_variable_name(atomField.name).c_str()); } } fprintf(out, ")\n"); fprintf(out, " }\n"); fprintf(out, " }\n\n"); } static int write_rust_stats_write_atoms(FILE* out, const AtomDeclSet& atomDeclSet, const AtomDecl& attributionDecl, const AtomDeclSet& nonChainedAtomDeclSet, const int minApiLevel, const char* headerCrate) { for (const auto& atomDecl : atomDeclSet) { // TODO(b/216543320): support repeated fields in Rust if (std::find_if(atomDecl->fields.begin(), atomDecl->fields.end(), [](const AtomField& atomField) { return is_repeated_field(atomField.javaType); }) != atomDecl->fields.end()) { continue; } fprintf(out, "pub mod %s {\n", atomDecl->name.c_str()); fprintf(out, " use statspull_bindgen::*;\n"); fprintf(out, " #[allow(unused)]\n"); fprintf(out, " use std::convert::TryInto;\n"); fprintf(out, "\n"); write_rust_atom_constant_values(out, atomDecl); write_rust_struct(out, atomDecl, attributionDecl, headerCrate); const int ret = write_rust_stats_write_method(out, atomDecl, attributionDecl, minApiLevel, headerCrate); if (ret != 0) { return ret; } auto nonChained = nonChainedAtomDeclSet.find(atomDecl); if (nonChained != nonChainedAtomDeclSet.end()) { write_rust_stats_write_non_chained_method(out, *nonChained, attributionDecl, headerCrate); } fprintf(out, "}\n"); } return 0; } void write_stats_log_rust_header(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, const char* headerCrate) { // Print prelude fprintf(out, "// This file is autogenerated.\n"); fprintf(out, "\n"); fprintf(out, "#[derive(thiserror::Error, Debug)]\n"); fprintf(out, "pub enum StatsError {\n"); fprintf(out, " #[error(\"Return error {0:?}\")]\n"); fprintf(out, " Return(i32),\n"); fprintf(out, " #[error(transparent)]\n"); fprintf(out, " NullChar(#[from] std::ffi::NulError),\n"); fprintf(out, " #[error(transparent)]\n"); fprintf(out, " Conversion(#[from] std::num::TryFromIntError),\n"); fprintf(out, "}\n"); fprintf(out, "\n"); fprintf(out, "pub type StatsResult = std::result::Result<(), StatsError>;\n"); fprintf(out, "\n"); fprintf(out, "pub trait Stat {\n"); fprintf(out, " fn add_astats_event(&self," " pulled_data: &mut statspull_bindgen::AStatsEventList)" " -> StatsResult;\n"); fprintf(out, "}\n"); fprintf(out, "\n"); write_rust_atom_constants(out, atoms, attributionDecl, headerCrate); } int write_stats_log_rust(FILE* out, const Atoms& atoms, const AtomDecl& attributionDecl, const int minApiLevel, const char* headerCrate) { // Print prelude fprintf(out, "// This file is autogenerated.\n"); fprintf(out, "\n"); fprintf(out, "struct AStatsEventDropper(*mut statspull_bindgen::AStatsEvent);\n"); fprintf(out, "\n"); fprintf(out, "impl Drop for AStatsEventDropper {\n"); fprintf(out, " fn drop(&mut self) {\n"); fprintf(out, " unsafe { statspull_bindgen::AStatsEvent_release(self.0) }\n"); fprintf(out, " }\n"); fprintf(out, "}\n"); fprintf(out, "\n"); write_rust_annotation_constants(out); const int errorCount = write_rust_stats_write_atoms( out, atoms.decls, attributionDecl, atoms.non_chained_decls, minApiLevel, headerCrate); return errorCount; } } // namespace stats_log_api_gen } // namespace android