diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/base/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/base/build.gradle.kts b/base/build.gradle.kts new file mode 100644 index 000000000..e8a21c0a9 --- /dev/null +++ b/base/build.gradle.kts @@ -0,0 +1,32 @@ +plugins { + id("ndksamples.android.library") +} + +android { + namespace = "com.android.ndk.samples.base" + + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + } + } + + buildFeatures { + prefabPublishing = true + } + + prefab { + create("base") { + headers = "src/main/cpp/include" + } + } +} + +dependencies { + + implementation(libs.appcompat) + implementation(libs.material) + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/base/src/main/cpp/CMakeLists.txt b/base/src/main/cpp/CMakeLists.txt new file mode 100644 index 000000000..c70b4410d --- /dev/null +++ b/base/src/main/cpp/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.22.1) +project(Base LANGUAGES CXX) + +add_compile_options(-Wall -Werror -Wextra) + +add_library(base + STATIC + logging.cpp +) + +target_compile_features(base PRIVATE cxx_std_23) +target_compile_options(base PRIVATE -Wno-vla-cxx-extension) +target_include_directories(base PUBLIC include) +target_link_libraries(base PUBLIC log) diff --git a/base/src/main/cpp/include/base/errno_restorer.h b/base/src/main/cpp/include/base/errno_restorer.h new file mode 100644 index 000000000..fa0451c01 --- /dev/null +++ b/base/src/main/cpp/include/base/errno_restorer.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2020 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. + */ + +#pragma once + +#include + +namespace ndksamples::base { + +class ErrnoRestorer { + public: + ErrnoRestorer() : saved_errno_(errno) {} + ErrnoRestorer(const ErrnoRestorer&) = delete; + + ~ErrnoRestorer() { errno = saved_errno_; } + + ErrnoRestorer& operator=(const ErrnoRestorer&) = delete; + + // Allow this object to be used as part of && operation. + explicit operator bool() const { return true; } + + private: + const int saved_errno_; +}; + +} // namespace ndksamples::base diff --git a/base/src/main/cpp/include/base/logging.h b/base/src/main/cpp/include/base/logging.h new file mode 100644 index 000000000..9718cbec4 --- /dev/null +++ b/base/src/main/cpp/include/base/logging.h @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2015 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. + */ + +#pragma once + +/** + * @file logging.h + * @brief Logging and assertion utilities. + * + * This is a modified version of AOSP's android-base/logging.h: + * https://cs.android.com/android/platform/superproject/main/+/main:system/libbase/include/android-base/logging.h + * + * The original file contained a lot of dependencies for things we don't need + * (kernel logging, support for non-Android platforms, non-logd loggers, etc). + * That's all been removed so we don't need to pull in those dependencies. + * + * If you copy from this sample, you may want to replace this with something + * like absl, which provides a very similar (if not identical) interface for all + * platforms. absl was not used here because we didn't need much, and it was + * preferable to avoid an additional dependency. + */ + +// +// Google-style C++ logging. +// + +// This header provides a C++ stream interface to logging. +// +// To log: +// +// LOG(INFO) << "Some text; " << some_value; +// +// Replace `INFO` with any severity from `enum LogSeverity`. +// Most devices filter out VERBOSE logs by default, run +// `adb shell setprop log.tag. V` to see them in adb logcat. +// +// To log the result of a failed function and include the string +// representation of `errno` at the end: +// +// PLOG(ERROR) << "Write failed"; +// +// The output will be something like `Write failed: I/O error`. +// Remember this as 'P' as in perror(3). +// +// To output your own types, simply implement operator<< as normal. +// +// By default, output goes to logcat on Android and stderr on the host. +// A process can use `SetLogger` to decide where all logging goes. +// Implementations are provided for logcat, stderr, and dmesg. +// +// By default, the process' name is used as the log tag. +// Code can choose a specific log tag by defining LOG_TAG +// before including this header. + +// This header also provides assertions: +// +// CHECK(must_be_true); +// CHECK_EQ(a, b) << z_is_interesting_too; + +#include +#include + +#include +#include +#include +#include +#include + +// Note: DO NOT USE DIRECTLY. Use LOG_TAG instead. +#ifdef _LOG_TAG_INTERNAL +#error "_LOG_TAG_INTERNAL must not be defined" +#endif +#ifdef LOG_TAG +#define _LOG_TAG_INTERNAL LOG_TAG +#else +#define _LOG_TAG_INTERNAL nullptr +#endif + +namespace ndksamples::base { + +enum LogSeverity { + VERBOSE, + DEBUG, + INFO, + WARNING, + ERROR, + FATAL_WITHOUT_ABORT, // For loggability tests, this is considered identical + // to FATAL. + FATAL, +}; + +enum LogId { + DEFAULT, + MAIN, + SYSTEM, + RADIO, + CRASH, +}; + +using LogFunction = std::function; +using AbortFunction = std::function; + +void DefaultAborter(const char* abort_message); + +void SetDefaultTag(const std::string& tag); + +// The LogdLogger sends chunks of up to ~4000 bytes at a time to logd. It does +// not prevent other threads from writing to logd between sending each chunk, so +// other threads may interleave their messages. If preventing interleaving is +// required, then a custom logger that takes a lock before calling this logger +// should be provided. +class LogdLogger { + public: + explicit LogdLogger(LogId default_log_id = ndksamples::base::MAIN); + + void operator()(LogId, LogSeverity, const char* tag, const char* file, + unsigned int line, const char* message); + + private: + LogId default_log_id_; +}; + +// Configure logging based on ANDROID_LOG_TAGS environment variable. +// We need to parse a string that looks like +// +// *:v jdwp:d dalvikvm:d dalvikvm-gc:i dalvikvmi:i +// +// The tag (or '*' for the global level) comes first, followed by a colon and a +// letter indicating the minimum priority level we're expected to log. This can +// be used to reveal or conceal logs with specific tags. +#define INIT_LOGGING_DEFAULT_LOGGER LogdLogger() +void InitLogging(const std::optional default_tag = {}, + std::optional log_level = {}, + LogFunction&& logger = INIT_LOGGING_DEFAULT_LOGGER, + AbortFunction&& aborter = DefaultAborter); +#undef INIT_LOGGING_DEFAULT_LOGGER + +// Replace the current logger and return the old one. +LogFunction SetLogger(LogFunction&& logger); + +// Replace the current aborter and return the old one. +AbortFunction SetAborter(AbortFunction&& aborter); + +// A helper macro that produces an expression that accepts both a qualified name +// and an unqualified name for a LogSeverity, and returns a LogSeverity value. +// Note: DO NOT USE DIRECTLY. This is an implementation detail. +#define SEVERITY_LAMBDA(severity) \ + ([&]() { \ + using ::ndksamples::base::VERBOSE; \ + using ::ndksamples::base::DEBUG; \ + using ::ndksamples::base::INFO; \ + using ::ndksamples::base::WARNING; \ + using ::ndksamples::base::ERROR; \ + using ::ndksamples::base::FATAL_WITHOUT_ABORT; \ + using ::ndksamples::base::FATAL; \ + return (severity); \ + }()) + +#define ABORT_AFTER_LOG_FATAL +#define ABORT_AFTER_LOG_EXPR_IF(c, x) (x) +#define MUST_LOG_MESSAGE(severity) false +#define ABORT_AFTER_LOG_FATAL_EXPR(x) ABORT_AFTER_LOG_EXPR_IF(true, x) + +// Defines whether the given severity will be logged or silently swallowed. +#define WOULD_LOG(severity) \ + (UNLIKELY(::ndksamples::base::ShouldLog(SEVERITY_LAMBDA(severity), \ + _LOG_TAG_INTERNAL)) || \ + MUST_LOG_MESSAGE(severity)) + +// Get an ostream that can be used for logging at the given severity and to the +// default destination. +// +// Notes: +// 1) This will not check whether the severity is high enough. One should use +// WOULD_LOG to filter +// usage manually. +// 2) This does not save and restore errno. +#define LOG_STREAM(severity) \ + ::ndksamples::base::LogMessage( \ + __FILE__, __LINE__, SEVERITY_LAMBDA(severity), _LOG_TAG_INTERNAL, -1) \ + .stream() + +// Logs a message to logcat on Android otherwise to stderr. If the severity is +// FATAL it also causes an abort. For example: +// +// LOG(FATAL) << "We didn't expect to reach here"; +#define LOG(severity) LOGGING_PREAMBLE(severity) && LOG_STREAM(severity) + +// Checks if we want to log something, and sets up appropriate RAII objects if +// so. +// Note: DO NOT USE DIRECTLY. This is an implementation detail. +#define LOGGING_PREAMBLE(severity) \ + (WOULD_LOG(severity) && \ + ABORT_AFTER_LOG_EXPR_IF( \ + (SEVERITY_LAMBDA(severity)) == ::ndksamples::base::FATAL, true) && \ + ::ndksamples::base::ErrnoRestorer()) + +// A variant of LOG that also logs the current errno value. To be used when +// library calls fail. +#define PLOG(severity) \ + LOGGING_PREAMBLE(severity) && \ + ::ndksamples::base::LogMessage(__FILE__, __LINE__, \ + SEVERITY_LAMBDA(severity), \ + _LOG_TAG_INTERNAL, errno) \ + .stream() + +// Marker that code is yet to be implemented. +#define UNIMPLEMENTED(level) \ + LOG(level) << __PRETTY_FUNCTION__ << " unimplemented " + +// Check whether condition x holds and LOG(FATAL) if not. The value of the +// expression x is only evaluated once. Extra logging can be appended using << +// after. For example: +// +// CHECK(false == true) results in a log message of +// "Check failed: false == true". +#define CHECK(x) \ + LIKELY((x)) || ABORT_AFTER_LOG_FATAL_EXPR(false) || \ + ::ndksamples::base::LogMessage(__FILE__, __LINE__, \ + ::ndksamples::base::FATAL, \ + _LOG_TAG_INTERNAL, -1) \ + .stream() \ + << "Check failed: " #x << " " + +// clang-format off +// Helper for CHECK_xx(x,y) macros. +#define CHECK_OP(LHS, RHS, OP) \ + for (auto _values = ::ndksamples::base::MakeEagerEvaluator(LHS, RHS); \ + UNLIKELY(!(_values.lhs.v OP _values.rhs.v)); \ + /* empty */) \ + ABORT_AFTER_LOG_FATAL \ + ::ndksamples::base::LogMessage(__FILE__, __LINE__, ::ndksamples::base::FATAL, _LOG_TAG_INTERNAL, -1) \ + .stream() \ + << "Check failed: " << #LHS << " " << #OP << " " << #RHS << " (" #LHS "=" \ + << ::ndksamples::base::LogNullGuard::Guard(_values.lhs.v) \ + << ", " #RHS "=" \ + << ::ndksamples::base::LogNullGuard::Guard(_values.rhs.v) \ + << ") " +// clang-format on + +// Check whether a condition holds between x and y, LOG(FATAL) if not. The value +// of the expressions x and y is evaluated once. Extra logging can be appended +// using << after. For example: +// +// CHECK_NE(0 == 1, false) results in +// "Check failed: false != false (0==1=false, false=false) ". +#define CHECK_EQ(x, y) CHECK_OP(x, y, ==) +#define CHECK_NE(x, y) CHECK_OP(x, y, !=) +#define CHECK_LE(x, y) CHECK_OP(x, y, <=) +#define CHECK_LT(x, y) CHECK_OP(x, y, <) +#define CHECK_GE(x, y) CHECK_OP(x, y, >=) +#define CHECK_GT(x, y) CHECK_OP(x, y, >) + +// clang-format off +// Helper for CHECK_STRxx(s1,s2) macros. +#define CHECK_STROP(s1, s2, sense) \ + while (UNLIKELY((strcmp(s1, s2) == 0) != (sense))) \ + ABORT_AFTER_LOG_FATAL \ + ::ndksamples::base::LogMessage(__FILE__, __LINE__, ::ndksamples::base::FATAL, \ + _LOG_TAG_INTERNAL, -1) \ + .stream() \ + << "Check failed: " << "\"" << (s1) << "\"" \ + << ((sense) ? " == " : " != ") << "\"" << (s2) << "\"" +// clang-format on + +// Check for string (const char*) equality between s1 and s2, LOG(FATAL) if not. +#define CHECK_STREQ(s1, s2) CHECK_STROP(s1, s2, true) +#define CHECK_STRNE(s1, s2) CHECK_STROP(s1, s2, false) + +// Perform the pthread function call(args), LOG(FATAL) on error. +#define CHECK_PTHREAD_CALL(call, args, what) \ + do { \ + int rc = call args; \ + if (rc != 0) { \ + errno = rc; \ + ABORT_AFTER_LOG_FATAL \ + PLOG(FATAL) << #call << " failed for " << (what); \ + } \ + } while (false) + +// DCHECKs are debug variants of CHECKs only enabled in debug builds. Generally +// CHECK should be used unless profiling identifies a CHECK as being in +// performance critical code. +#if defined(NDEBUG) && !defined(__clang_analyzer__) +static constexpr bool kEnableDChecks = false; +#else +static constexpr bool kEnableDChecks = true; +#endif + +#define DCHECK(x) \ + if (::ndksamples::base::kEnableDChecks) CHECK(x) +#define DCHECK_EQ(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_EQ(x, y) +#define DCHECK_NE(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_NE(x, y) +#define DCHECK_LE(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_LE(x, y) +#define DCHECK_LT(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_LT(x, y) +#define DCHECK_GE(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_GE(x, y) +#define DCHECK_GT(x, y) \ + if (::ndksamples::base::kEnableDChecks) CHECK_GT(x, y) +#define DCHECK_STREQ(s1, s2) \ + if (::ndksamples::base::kEnableDChecks) CHECK_STREQ(s1, s2) +#define DCHECK_STRNE(s1, s2) \ + if (::ndksamples::base::kEnableDChecks) CHECK_STRNE(s1, s2) + +namespace log_detail { + +// Temporary storage for a single eagerly evaluated check expression operand. +template +struct Storage { + template + explicit constexpr Storage(U&& u) : v(std::forward(u)) {} + explicit Storage(const Storage& t) = delete; + explicit Storage(Storage&& t) = delete; + T v; +}; + +// Partial specialization for smart pointers to avoid copying. +template +struct Storage> { + explicit constexpr Storage(const std::unique_ptr& ptr) : v(ptr.get()) {} + const T* v; +}; +template +struct Storage> { + explicit constexpr Storage(const std::shared_ptr& ptr) : v(ptr.get()) {} + const T* v; +}; + +// Type trait that checks if a type is a (potentially const) char pointer. +template +struct IsCharPointer { + using Pointee = std::remove_cv_t>; + static constexpr bool value = + std::is_pointer_v && + (std::is_same_v || std::is_same_v || + std::is_same_v); +}; + +// Counterpart to Storage that depends on both operands. This is used to prevent +// char pointers being treated as strings in the log output - they might point +// to buffers of unprintable binary data. +template +struct StorageTypes { + static constexpr bool voidptr = + IsCharPointer::value && IsCharPointer::value; + using LHSType = std::conditional_t; + using RHSType = std::conditional_t; +}; + +// Temporary class created to evaluate the LHS and RHS, used with +// MakeEagerEvaluator to infer the types of LHS and RHS. +template +struct EagerEvaluator { + template + constexpr EagerEvaluator(A&& l, B&& r) + : lhs(std::forward(l)), rhs(std::forward(r)) {} + const Storage::LHSType> lhs; + const Storage::RHSType> rhs; +}; + +} // namespace log_detail + +// Converts std::nullptr_t and null char pointers to the string "null" +// when writing the failure message. +template +struct LogNullGuard { + static const T& Guard(const T& v) { return v; } +}; +template <> +struct LogNullGuard { + static const char* Guard(const std::nullptr_t&) { return "(null)"; } +}; +template <> +struct LogNullGuard { + static const char* Guard(const char* v) { return v ? v : "(null)"; } +}; +template <> +struct LogNullGuard { + static const char* Guard(const char* v) { return v ? v : "(null)"; } +}; + +// Helper function for CHECK_xx. +template +constexpr auto MakeEagerEvaluator(LHS&& lhs, RHS&& rhs) { + return log_detail::EagerEvaluator, std::decay_t>( + std::forward(lhs), std::forward(rhs)); +} + +// Data for the log message, not stored in LogMessage to avoid increasing the +// stack size. +class LogMessageData; + +// A LogMessage is a temporarily scoped object used by LOG and the unlikely part +// of a CHECK. The destructor will abort if the severity is FATAL. +class LogMessage { + public: + // LogId has been deprecated, but this constructor must exist for prebuilts. + LogMessage(const char* file, unsigned int line, LogId, LogSeverity severity, + const char* tag, int error); + LogMessage(const char* file, unsigned int line, LogSeverity severity, + const char* tag, int error); + + DISALLOW_COPY_AND_ASSIGN(LogMessage); + + ~LogMessage(); + + // Returns the stream associated with the message, the LogMessage performs + // output when it goes out of scope. + std::ostream& stream(); + + // The routine that performs the actual logging. + static void LogLine(const char* file, unsigned int line, LogSeverity severity, + const char* tag, const char* msg); + + private: + const std::unique_ptr data_; +}; + +// Get the minimum severity level for logging. +LogSeverity GetMinimumLogSeverity(); + +// Set the minimum severity level for logging, returning the old severity. +LogSeverity SetMinimumLogSeverity(LogSeverity new_severity); + +// Return whether or not a log message with the associated tag should be logged. +bool ShouldLog(LogSeverity severity, const char* tag); + +// Allows to temporarily change the minimum severity level for logging. +class ScopedLogSeverity { + public: + explicit ScopedLogSeverity(LogSeverity level); + ~ScopedLogSeverity(); + + private: + LogSeverity old_; +}; + +} // namespace ndksamples::base + +namespace std { // NOLINT(cert-dcl58-cpp) + +// Emit a warning of ostream<< with std::string*. The intention was most likely +// to print *string. +// +// Note: for this to work, we need to have this in a namespace. +// Note: using a pragma because "-Wgcc-compat" (included in "-Weverything") +// complains about +// diagnose_if. +// Note: to print the pointer, use "<< static_cast(string_pointer)" +// instead. Note: a not-recommended alternative is to let Clang ignore the +// warning by adding +// -Wno-user-defined-warnings to CPPFLAGS. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wgcc-compat" +#define OSTREAM_STRING_POINTER_USAGE_WARNING \ + __attribute__(( \ + diagnose_if(true, "Unexpected logging of string pointer", "warning"))) +inline OSTREAM_STRING_POINTER_USAGE_WARNING std::ostream& operator<<( + std::ostream& stream, const std::string* string_pointer) { + return stream << static_cast(string_pointer); +} +#pragma clang diagnostic pop + +} // namespace std diff --git a/base/src/main/cpp/include/base/macros.h b/base/src/main/cpp/include/base/macros.h new file mode 100644 index 000000000..509b1237d --- /dev/null +++ b/base/src/main/cpp/include/base/macros.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 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. + */ + +#pragma once + +#include // for size_t + +#include + +// A macro to disallow the copy constructor and operator= functions +// This must be placed in the private: declarations for a class. +// +// For disallowing only assign or copy, delete the relevant operator or +// constructor, for example: +// void operator=(const TypeName&) = delete; +// Note, that most uses of DISALLOW_ASSIGN and DISALLOW_COPY are broken +// semantically, one should either use disallow both or neither. Try to +// avoid these in new code. +#define DISALLOW_COPY_AND_ASSIGN(TypeName) \ + TypeName(const TypeName &) = delete; \ + void operator=(const TypeName &) = delete + +// A macro to disallow all the implicit constructors, namely the +// default constructor, copy constructor and operator= functions. +// +// This should be used in the private: declarations for a class +// that wants to prevent anyone from instantiating it. This is +// especially useful for classes containing only static methods. +#define DISALLOW_IMPLICIT_CONSTRUCTORS(TypeName) \ + TypeName() = delete; \ + DISALLOW_COPY_AND_ASSIGN(TypeName) + +// The arraysize(arr) macro returns the # of elements in an array arr. +// The expression is a compile-time constant, and therefore can be +// used in defining new arrays, for example. If you use arraysize on +// a pointer by mistake, you will get a compile-time error. +// +// One caveat is that arraysize() doesn't accept any array of an +// anonymous type or a type defined inside a function. In these rare +// cases, you have to use the unsafe ARRAYSIZE_UNSAFE() macro below. This is +// due to a limitation in C++'s template system. The limitation might +// eventually be removed, but it hasn't happened yet. + +// This template function declaration is used in defining arraysize. +// Note that the function doesn't need an implementation, as we only +// use its type. +template +char (&ArraySizeHelper(T (&array)[N]))[N]; // NOLINT(readability/casting) + +#define arraysize(array) (sizeof(ArraySizeHelper(array))) + +#define SIZEOF_MEMBER(t, f) sizeof(std::declval().f) + +// Changing this definition will cause you a lot of pain. A majority of +// vendor code defines LIKELY and UNLIKELY this way, and includes +// this header through an indirect path. +#define LIKELY(exp) (__builtin_expect((exp) != 0, true)) +#define UNLIKELY(exp) (__builtin_expect((exp) != 0, false)) + +/// True if the (runtime) version of the OS is at least x. +/// +/// Clang is very particular about how __builtin_available is used. Logical +/// operations (including negation) may not be combined with +/// __builtin_available, so to negate this check you must do: +/// +/// if (API_AT_LEAST(x)) { +/// } else { +/// // do negated stuff +/// } +#define API_AT_LEAST(x) __builtin_available(android x, *) + +/// Marks a function as not callable on OS versions older than x. +/// +/// This is a minor abuse of Clang's __attribute__((availability)), so the +/// diagnostic for this will be a little odd, but it allows us to extract +/// functions from code that already has an API_AT_LEAST guard without rewriting +/// the guard in every called function. +#define REQUIRES_API(x) __INTRODUCED_IN(x) diff --git a/base/src/main/cpp/logging.cpp b/base/src/main/cpp/logging.cpp new file mode 100644 index 000000000..84c6c4a88 --- /dev/null +++ b/base/src/main/cpp/logging.cpp @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2015 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 "base/logging.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "base/macros.h" +#include "logging_splitters.h" + +namespace ndksamples::base { + +static const char* GetFileBasename(const char* file) { + // We can't use basename(3) even on Unix because the Mac doesn't + // have a non-modifying basename. + const char* last_slash = strrchr(file, '/'); + if (last_slash != nullptr) { + return last_slash + 1; + } + return file; +} + +static int32_t LogIdTolog_id_t(LogId log_id) { + switch (log_id) { + case MAIN: + return LOG_ID_MAIN; + case SYSTEM: + return LOG_ID_SYSTEM; + case RADIO: + return LOG_ID_RADIO; + case CRASH: + return LOG_ID_CRASH; + case DEFAULT: + default: + return LOG_ID_DEFAULT; + } +} + +static int32_t LogSeverityToPriority(LogSeverity severity) { + switch (severity) { + case VERBOSE: + return ANDROID_LOG_VERBOSE; + case DEBUG: + return ANDROID_LOG_DEBUG; + case INFO: + return ANDROID_LOG_INFO; + case WARNING: + return ANDROID_LOG_WARN; + case ERROR: + return ANDROID_LOG_ERROR; + case FATAL_WITHOUT_ABORT: + case FATAL: + default: + return ANDROID_LOG_FATAL; + } +} + +static LogFunction& Logger() { + static auto& logger = *new LogFunction(LogdLogger()); + return logger; +} + +static AbortFunction& Aborter() { + static auto& aborter = *new AbortFunction(DefaultAborter); + return aborter; +} + +// Only used for Q fallback. +static std::recursive_mutex& TagLock() { + static auto& tag_lock = *new std::recursive_mutex(); + return tag_lock; +} + +static std::string* gDefaultTag; + +void SetDefaultTag(const std::string_view tag) { + std::lock_guard lock(TagLock()); + if (gDefaultTag != nullptr) { + delete gDefaultTag; + gDefaultTag = nullptr; + } + if (!tag.empty()) { + gDefaultTag = new std::string(tag); + } +} + +static bool gInitialized = false; + +// Only used for Q fallback. +static LogSeverity gMinimumLogSeverity = INFO; + +void DefaultAborter(const char* abort_message) { + android_set_abort_message(abort_message); + abort(); +} + +static void LogdLogChunk(LogId id, LogSeverity severity, const char* tag, + const char* message) { + int32_t lg_id = LogIdTolog_id_t(id); + int32_t priority = LogSeverityToPriority(severity); + + __android_log_buf_print(lg_id, priority, tag, "%s", message); +} + +LogdLogger::LogdLogger(LogId default_log_id) + : default_log_id_(default_log_id) {} + +void LogdLogger::operator()(LogId id, LogSeverity severity, const char* tag, + const char* file, unsigned int line, + const char* message) { + if (id == DEFAULT) { + id = default_log_id_; + } + + SplitByLogdChunks(id, severity, tag, file, line, message, LogdLogChunk); +} + +void InitLogging(const std::optional default_tag, + std::optional log_level, LogFunction&& logger, + AbortFunction&& aborter) { + SetLogger(std::forward(logger)); + SetAborter(std::forward(aborter)); + + if (gInitialized) { + return; + } + + gInitialized = true; + + if (default_tag.has_value()) { + SetDefaultTag(default_tag.value()); + } + + const char* tags = getenv("ANDROID_LOG_TAGS"); + if (tags == nullptr) { + return; + } + + if (log_level.has_value()) { + SetMinimumLogSeverity(log_level.value()); + } +} + +LogFunction SetLogger(LogFunction&& logger) { + LogFunction old_logger = std::move(Logger()); + Logger() = std::move(logger); + return old_logger; +} + +AbortFunction SetAborter(AbortFunction&& aborter) { + AbortFunction old_aborter = std::move(Aborter()); + Aborter() = std::move(aborter); + return old_aborter; +} + +// This indirection greatly reduces the stack impact of having lots of +// checks/logging in a function. +class LogMessageData { + public: + LogMessageData(const char* file, unsigned int line, LogSeverity severity, + const char* tag, int error) + : file_(GetFileBasename(file)), + line_number_(line), + severity_(severity), + tag_(tag), + error_(error) {} + + DISALLOW_COPY_AND_ASSIGN(LogMessageData); + + const char* GetFile() const { return file_; } + + unsigned int GetLineNumber() const { return line_number_; } + + LogSeverity GetSeverity() const { return severity_; } + + const char* GetTag() const { return tag_; } + + int GetError() const { return error_; } + + std::ostream& GetBuffer() { return buffer_; } + + std::string ToString() const { return buffer_.str(); } + + private: + std::ostringstream buffer_; + const char* const file_; + const unsigned int line_number_; + const LogSeverity severity_; + const char* const tag_; + const int error_; +}; + +LogMessage::LogMessage(const char* file, unsigned int line, LogId, + LogSeverity severity, const char* tag, int error) + : LogMessage(file, line, severity, tag, error) {} + +LogMessage::LogMessage(const char* file, unsigned int line, + LogSeverity severity, const char* tag, int error) + : data_(new LogMessageData(file, line, severity, tag, error)) {} + +LogMessage::~LogMessage() { + // Check severity again. This is duplicate work wrt/ LOG macros, but not + // LOG_STREAM. + if (!WOULD_LOG(data_->GetSeverity())) { + return; + } + + // Finish constructing the message. + if (data_->GetError() != -1) { + data_->GetBuffer() << ": " << strerror(data_->GetError()); + } + std::string msg(data_->ToString()); + + if (data_->GetSeverity() == FATAL) { + // Set the bionic abort message early to avoid liblog doing it + // with the individual lines, so that we get the whole message. + android_set_abort_message(msg.c_str()); + } + + LogLine(data_->GetFile(), data_->GetLineNumber(), data_->GetSeverity(), + data_->GetTag(), msg.c_str()); + + // Abort if necessary. + if (data_->GetSeverity() == FATAL) { + Aborter()(msg.c_str()); + } +} + +std::ostream& LogMessage::stream() { return data_->GetBuffer(); } + +void LogMessage::LogLine(const char* file, unsigned int line, + LogSeverity severity, const char* tag, + const char* message) { + if (tag == nullptr) { + std::lock_guard lock(TagLock()); + if (gDefaultTag == nullptr) { + gDefaultTag = new std::string(getprogname()); + } + + Logger()(DEFAULT, severity, gDefaultTag->c_str(), file, line, message); + } else { + Logger()(DEFAULT, severity, tag, file, line, message); + } +} + +LogSeverity GetMinimumLogSeverity() { return gMinimumLogSeverity; } + +bool ShouldLog(LogSeverity severity, const char*) { + return severity >= gMinimumLogSeverity; +} + +LogSeverity SetMinimumLogSeverity(LogSeverity new_severity) { + LogSeverity old_severity = gMinimumLogSeverity; + gMinimumLogSeverity = new_severity; + return old_severity; +} + +ScopedLogSeverity::ScopedLogSeverity(LogSeverity new_severity) { + old_ = SetMinimumLogSeverity(new_severity); +} + +ScopedLogSeverity::~ScopedLogSeverity() { SetMinimumLogSeverity(old_); } + +} // namespace ndksamples::base diff --git a/base/src/main/cpp/logging_splitters.h b/base/src/main/cpp/logging_splitters.h new file mode 100644 index 000000000..6795b3ba0 --- /dev/null +++ b/base/src/main/cpp/logging_splitters.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2020 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. + */ + +#pragma once + +#include +#include + +#include + +#include "base/logging.h" + +#define LOGGER_ENTRY_MAX_PAYLOAD 4068 // This constant is not in the NDK. + +namespace ndksamples::base { + +// This splits the message up line by line, by calling log_function with a +// pointer to the start of each line and the size up to the newline character. +// It sends size = -1 for the final line. +template +static void SplitByLines(const char* msg, const F& log_function, + Args&&... args) { + const char* newline = strchr(msg, '\n'); + while (newline != nullptr) { + log_function(msg, newline - msg, args...); + msg = newline + 1; + newline = strchr(msg, '\n'); + } + + log_function(msg, -1, args...); +} + +// This splits the message up into chunks that logs can process delimited by new +// lines. It calls log_function with the exact null terminated message that +// should be sent to logd. Note, despite the loops and snprintf's, if severity +// is not fatal and there are no new lines, this function simply calls +// log_function with msg without any extra overhead. +template +static void SplitByLogdChunks(LogId log_id, LogSeverity severity, + const char* tag, const char* file, + unsigned int line, const char* msg, + const F& log_function) { + // The maximum size of a payload, after the log header that logd will accept + // is LOGGER_ENTRY_MAX_PAYLOAD, so subtract the other elements in the payload + // to find the size of the string that we can log in each pass. The protocol + // is documented in liblog/README.protocol.md. Specifically we subtract a byte + // for the priority, the length of the tag + its null terminator, and an + // additional byte for the null terminator on the payload. We subtract an + // additional 32 bytes for slack, similar to java/android/util/Log.java. + ptrdiff_t max_size = LOGGER_ENTRY_MAX_PAYLOAD - strlen(tag) - 35; + if (max_size <= 0) { + abort(); + } + // If we're logging a fatal message, we'll append the file and line numbers. + bool add_file = + file != nullptr && (severity == FATAL || severity == FATAL_WITHOUT_ABORT); + + std::string file_header; + if (add_file) { + file_header = std::format("{}:{}]", file, line); + } + int file_header_size = file_header.size(); + + __attribute__((uninitialized)) char logd_chunk[max_size + 1]; + ptrdiff_t chunk_position = 0; + + auto call_log_function = [&]() { + log_function(log_id, severity, tag, logd_chunk); + chunk_position = 0; + }; + + auto write_to_logd_chunk = [&](const char* message, int length) { + int size_written = 0; + const char* new_line = chunk_position > 0 ? "\n" : ""; + if (add_file) { + size_written = snprintf(logd_chunk + chunk_position, + sizeof(logd_chunk) - chunk_position, "%s%s%.*s", + new_line, file_header.c_str(), length, message); + } else { + size_written = snprintf(logd_chunk + chunk_position, + sizeof(logd_chunk) - chunk_position, "%s%.*s", + new_line, length, message); + } + + // This should never fail, if it does and we set size_written to 0, which + // will skip this line and move to the next one. + if (size_written < 0) { + size_written = 0; + } + chunk_position += size_written; + }; + + const char* newline = strchr(msg, '\n'); + while (newline != nullptr) { + // If we have data in the buffer and this next line doesn't fit, write the + // buffer. + if (chunk_position != 0 && + chunk_position + (newline - msg) + 1 + file_header_size > max_size) { + call_log_function(); + } + + // Otherwise, either the next line fits or we have any empty buffer and too + // large of a line to ever fit, in both cases, we add it to the buffer and + // continue. + write_to_logd_chunk(msg, newline - msg); + + msg = newline + 1; + newline = strchr(msg, '\n'); + } + + // If we have left over data in the buffer and we can fit the rest of msg, add + // it to the buffer then write the buffer. + if (chunk_position != 0 && + chunk_position + static_cast(strlen(msg)) + 1 + file_header_size <= + max_size) { + write_to_logd_chunk(msg, -1); + call_log_function(); + } else { + // If the buffer is not empty and we can't fit the rest of msg into it, + // write its contents. + if (chunk_position != 0) { + call_log_function(); + } + // Then write the rest of the msg. + if (add_file) { + snprintf(logd_chunk, sizeof(logd_chunk), "%s%s", file_header.c_str(), + msg); + log_function(log_id, severity, tag, logd_chunk); + } else { + log_function(log_id, severity, tag, msg); + } + } +} + +} // namespace ndksamples::base diff --git a/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidApplicationConventionPlugin.kt b/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidApplicationConventionPlugin.kt index 23f521823..3f6ed4d26 100644 --- a/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidApplicationConventionPlugin.kt +++ b/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidApplicationConventionPlugin.kt @@ -24,6 +24,18 @@ class AndroidApplicationConventionPlugin : Plugin { sourceCompatibility = Versions.JAVA targetCompatibility = Versions.JAVA } + + // Studio will not automatically pass logcat through ndk-stack, so we need to avoid + // stripping debug binaries if we want the crash trace to be readable. + buildTypes { + debug { + packaging { + jniLibs { + keepDebugSymbols += "**/*.so" + } + } + } + } } } } diff --git a/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidLibraryConventionPlugin.kt b/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidLibraryConventionPlugin.kt index 6df914b6c..53f567eb0 100644 --- a/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidLibraryConventionPlugin.kt +++ b/build-logic/src/main/java/com/android/ndk/samples/buildlogic/AndroidLibraryConventionPlugin.kt @@ -28,6 +28,18 @@ class AndroidLibraryConventionPlugin : Plugin { sourceCompatibility = Versions.JAVA targetCompatibility = Versions.JAVA } + + // Studio will not automatically pass logcat through ndk-stack, so we need to avoid + // stripping debug binaries if we want the crash trace to be readable. + buildTypes { + debug { + packaging { + jniLibs { + keepDebugSymbols += "**/*.so" + } + } + } + } } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cda7ad250..f42267a97 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ curl = "7.79.1-beta-1" googletest = "1.11.0-beta-1" jsoncpp = "1.9.5-beta-1" openssl = "1.1.1q-beta-1" +coreKtx = "1.13.1" [libraries] junit = { group = "junit", name = "junit", version.ref = "junit" } @@ -40,6 +41,7 @@ openssl = { group = "com.android.ndk.thirdparty", name = "openssl", version.ref # build-logic dependencies android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle b/settings.gradle index 1bac1cdee..f0d4b009a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -22,6 +22,7 @@ dependencyResolutionManagement { rootProject.name = "NDK Samples" include(":audio-echo:app") +include(":base") include(":bitmap-plasma:app") include(":camera:basic") include(":camera:texture-view")