From 94a8afce13ccbae3b548210ac16c522fdee653b5 Mon Sep 17 00:00:00 2001 From: Ranjithkumar Ragavan Date: Wed, 19 Aug 2026 00:28:15 -0700 Subject: [PATCH] Android: export curated EXECUTORCH_1_0 ABI from libexecutorch.so (hot-pluggable backends, milestone 1) First step of the hot-pluggable backends plan (#10457) to let Android apps opt out of unused backends (#19329). Today libexecutorch.so exports only JNI_OnLoad and Java_*. A backend built as a standalone .so cannot reach the process-wide backend/kernel registries, so its static registration writes into a private copy and delegation fails with 'backend not found' / duplicate-kernel errors. This change adds a versioned EXECUTORCH_1_0 node to extension/android/jni/version_script.txt exporting the curated surface an external .so needs to self-register at load time via ELF symbol interposition: - registration entry points: register_backend, get_backend_class, register_kernels - RTTI/vtables for polymorphic public types such as BackendInterface (its key function is emitted in the core .so) - the public executorch::runtime / executorch::aten API namespaces that backend implementations are written against (Tensor, EValue, MemoryManager, FreeableBuffer, logging) - the et_pal_* C API so backend .so files share process-wide logging/allocation hooks The boundary is namespace-scoped instead of symbol-by-symbol so it stays maintainable as public headers evolve; everything else stays hidden for LTO/gc-sections. Future additive symbols go into a new version node so external .so files can depend on a stable ABI tag. The monolithic .so topology is unchanged: no target, link, or loading behavior changes for existing consumers. Verification: build_android_library.sh now asserts the built libexecutorch.so actually exports the required symbols (via NDK llvm-nm), failing the build if the ABI surface regresses. Existing Android CI exercises the monolithic path end to end. --- extension/android/check_exported_symbols.sh | 65 +++++++++++++++++++++ extension/android/jni/version_script.txt | 46 ++++++++++++++- scripts/build_android_library.sh | 6 ++ 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100755 extension/android/check_exported_symbols.sh diff --git a/extension/android/check_exported_symbols.sh b/extension/android/check_exported_symbols.sh new file mode 100755 index 00000000000..4c594a43a97 --- /dev/null +++ b/extension/android/check_exported_symbols.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +# Verifies that the Android JNI shared library exports the curated +# EXECUTORCH_1_0 ABI defined in extension/android/jni/version_script.txt. +# +# External backend/kernel .so files rely on these symbols being visible in the +# core .so so that ELF symbol interposition gives them access to the +# process-wide backend/kernel registries at load time. If any of these symbols +# go missing (e.g. the version script is accidentally narrowed or stops being +# applied), split backends would silently register into a private registry and +# delegation would fail with "backend not found" at runtime. Fail the build +# instead. +# +# Usage: check_exported_symbols.sh [path-to-llvm-nm] + +set -euo pipefail + +SO_PATH="${1:?usage: check_exported_symbols.sh [llvm-nm]}" +NM="${2:-llvm-nm}" + +# Demangled symbol fragments that must be exported by the core .so. +REQUIRED_SYMBOLS=( + # Backend registration (runtime/backend/interface.h) + "executorch::runtime::register_backend" + "executorch::runtime::get_backend_class" + # BackendInterface RTTI/vtable; external backends subclass it and the key + # function (the destructor) is emitted in the core .so. + "typeinfo for executorch::runtime::BackendInterface" + "vtable for executorch::runtime::BackendInterface" + # Kernel registration (runtime/kernel/operator_registry.h) + "executorch::runtime::register_kernels" + # exec_aten data model used by backend implementations (the portable Tensor + # is executorch::runtime::etensor::Tensor; executorch::aten::Tensor is a + # using-alias, so symbols carry the real namespace). + "executorch::runtime::etensor::Tensor" + "executorch::runtime::EValue" + # PAL hooks so backend .so files share process-wide logging + "et_pal_emit_log_message" + # JNI entry points (pre-existing export contract) + "JNI_OnLoad" +) + +EXPORTED="$("${NM}" -D --defined-only -C "${SO_PATH}")" + +missing=0 +for sym in "${REQUIRED_SYMBOLS[@]}"; do + if ! grep -qF -- "${sym}" <<<"${EXPORTED}"; then + echo "ERROR: ${SO_PATH} does not export required ABI symbol: ${sym}" >&2 + missing=1 + fi +done + +if [[ "${missing}" -ne 0 ]]; then + echo "The EXECUTORCH_1_0 ABI surface is incomplete." >&2 + echo "See extension/android/jni/version_script.txt and" >&2 + echo "https://github.com/pytorch/executorch/issues/10457 (milestone 1)." >&2 + exit 1 +fi + +echo "OK: ${SO_PATH} exports the EXECUTORCH_1_0 ABI surface." diff --git a/extension/android/jni/version_script.txt b/extension/android/jni/version_script.txt index a8b7672c1eb..5a277d934b3 100644 --- a/extension/android/jni/version_script.txt +++ b/extension/android/jni/version_script.txt @@ -1,7 +1,51 @@ -{ +# Export map for the Android JNI shared library (libexecutorch_jni.so, +# repackaged as libexecutorch.so in the AAR). +# +# In addition to the JNI entry points, the EXECUTORCH_1_0 node exports the +# curated C++ runtime API surface that an external backend or kernel .so needs +# in order to self-register into this process's registries when it is loaded. +# This is milestone 1 of the hot-pluggable backends plan: +# https://github.com/pytorch/executorch/issues/10457 +# +# The export boundary is namespace-scoped rather than symbol-by-symbol so that +# it stays maintainable as the public headers evolve: +# - executorch::runtime::* registration entry points (register_backend, +# get_backend_class, register_kernels), BackendInterface, EValue, +# MemoryManager / MemoryAllocator, FreeableBuffer, logging, and the rest of +# the public runtime API declared under runtime/. +# - executorch::aten::* the exec_aten data model (Tensor, EValue, +# Scalar, ...) that backend implementations are written against in +# portable (non-ATen) builds. +# - et_pal_* the platform abstraction layer C API, so backend +# .so files share the process-wide logging / allocation / time hooks. +# +# Everything else — runtime internals outside the public namespaces, backend +# implementation details, and kernel implementations — stays hidden, which +# preserves the linker's ability to strip and optimize them. +# +# Additive changes must go into a new version node (e.g. EXECUTORCH_1_1) so +# that external .so files can depend on a stable ABI tag. Never remove or +# narrow symbols from an existing node. +EXECUTORCH_1_0 { global: + # JNI entry points. JNI_OnLoad; Java_*; + + # Platform abstraction layer (extern "C" API). + et_pal_*; + + extern "C++" { + "executorch::runtime::*"; + "executorch::aten::*"; + # RTTI and vtables for polymorphic public types (e.g. BackendInterface) + # whose key functions are emitted in this library; external .so files + # subclass these and need the symbols at link/load time. + "typeinfo for executorch::runtime::*"; + "typeinfo for executorch::aten::*"; + "vtable for executorch::runtime::*"; + "vtable for executorch::aten::*"; + }; local: *; }; diff --git a/scripts/build_android_library.sh b/scripts/build_android_library.sh index 5363c64b87c..d93e4196f72 100755 --- a/scripts/build_android_library.sh +++ b/scripts/build_android_library.sh @@ -70,6 +70,12 @@ build_android_native_library() { mkdir -p ${SO_STAGE_DIR} cp "${CMAKE_OUT}"/extension/android/*.so "${SO_STAGE_DIR}/libexecutorch.so" + # Verify the curated EXECUTORCH_1_0 ABI surface is exported (milestone 1 of + # https://github.com/pytorch/executorch/issues/10457). Backend .so files + # loaded alongside the core .so depend on these symbols for self-registration. + NM_BIN=$(echo "${ANDROID_NDK}"/toolchains/llvm/prebuilt/*/bin/llvm-nm) + bash extension/android/check_exported_symbols.sh "${SO_STAGE_DIR}/libexecutorch.so" "${NM_BIN}" + # Copy QNN related so library if [ -n "$QNN_SDK_ROOT" ] && [ "$ANDROID_ABI" == "arm64-v8a" ]; then cp "${CMAKE_OUT}"/lib/executorch/backends/qualcomm/libqnn_executorch_backend.so ${SO_STAGE_DIR}