Android System & AOSP Engineering/AOSP Framework & Custom Services

안드로이드 14+ 커스텀 HAL 모듈 만들기: 최신 Stable AIDL 기반 실전 AOSP 빌드 가이드

임베디드 친구 2025. 3. 26. 09:35
반응형

안드로이드 기반의 로봇 하드웨어나 스마트 가전, 혹은 커스텀 임베디드 보드를 개발하다 보면 필연적으로 세상에 없던 나만의 커스텀 센서나 물리 모터를 제어해야 하는 순간을 마주합니다. 리눅스 커널 단에 드라이버 노드(/dev/my_sensor)를 멋지게 뚫어놓았더라도, 이를 상위 안드로이드 시스템 서비스와 앱 레이어까지 안전하게 끌어올리려면 결국 나만의 커스텀 HAL(Hardware Abstraction Layer) 모듈을 빌드해야 하는데요.

하지만 구글 공식 문서를 찾아보면 구형 HIDL 방식과 최신 AIDL 방식이 뒤섞여 있어 초보 플랫폼 엔지니어들은 시작부터 갈 길을 잃기 십상입니다. 과거 안드로이드 8~9 시절에 쓰던 Passthrough HIDL 구조는 이제 완전히 역사의 뒤안길로 사라졌기 때문이죠. 이번 포스팅에서는 현재 안드로이드 표준 아키텍처인 'Stable AIDL'을 기반으로 빈 디렉터리부터 시작해 인터페이스를 정의하고, C++ 서비스를 구현하여 실제 AOSP 빌드 시스템에 완벽히 등록하는 전체 프로세스를 A to Z로 짚어보겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  1. 안드로이드 최신 버전의 커스텀 HAL 개발은 구형 HIDL 패러다임을 버리고 성능과 버전 관리가 통합된 Stable AIDL을 사용하는 것이 표준입니다.
  2. Android.bp에 aidl_interface 빌드 규칙을 선언하면 컴파일러가 네이티브 C++ 바인더 통신을 위한 코어 라이브러리를 자동으로 생성해 줍니다.
  3. 구현된 HAL 서비스는 독립된 벤더 바이너리로 빌드되어 시스템 스타트업 스크립트(init.rc) 및 VINTF 규격서 등록을 통해 완전히 독립된 데몬으로 구동됩니다.

1. 커스텀 Stable AIDL HAL 개발을 위한 4단계 아키텍처 맵

새로운 하드웨어 모듈을 시스템에 안착시키기 위해 우리가 차례대로 빌드하고 선언해야 하는 파일들과 그 역할을 일목요연하게 정리했습니다.

개발 단계 (Step) 작성 및 수정 파일명 핵심 역할 및 컴파일 아키텍처상의 의미
Step 1. 인터페이스 정의 IExample.aidl 하드웨어가 상위에 노출할 함수 규격을 Stable 규격으로 선언
Step 2. 빌드 스크립트 작성 Android.bp aidl_interface 및 서비스 바이너리(cc_binary) 빌드 규칙 정의
Step 3. HAL 서비스 구현 Example.h / Example.cpp AIDL 스텁을 상속받아 커널 드라이버 제어 인터페이스 구체화
Step 4. 시스템 데몬 진입점 main.cpp servicemanager에 서비스를 정식 등록하고 바인더 스레드 가동

2. 1단계: Stable AIDL 인터페이스 정의

먼저 하드웨어 데이터를 주고받을 통로의 계약서인 .aidl 파일을 만듭니다. 구조의 일관성을 위해 AOSP 표준 경로인 hardware/interfaces/example/ 폴더를 생성하고 진행하시는 것을 추천합니다.

Java
 
// hardware/interfaces/example/aidl/android/hardware/example/IExample.aidl
package android.hardware.example;

@VintfStability // 안드로이드 벤더-프레임워크 간 안정성 검증을 위한 필수 어노테이션
interface IExample {
    // 하드웨어 칩셋으로부터 계측 데이터를 문자열로 읽어오는 메서드
    String getExampleData();
    
    // 상위 프레임워크가 하드웨어 소자에 특정 제어 데이터를 내리는 메서드
    void setExampleData(in String data);
}

3. 2단계: 빌드 스크립트 환경 아키텍처 구성 (Android.bp)

안드로이드의 빌드 엔진인 소ong(Soong)에게 이 AIDL을 어떻게 요리할지 명시해 주어야 합니다. 같은 디렉터리에 Android.bp 파일을 아래와 같이 설계합니다.

코드 스니펫
 
// hardware/interfaces/example/Android.bp

// 1. AIDL 인터페이스를 C++ 및 자바 바인더 라이브러리로 변환하는 규칙
aidl_interface {
    name: "android.hardware.example",
    vendor_available: true, // 벤더 영역 레이어에서 접근 가능하도록 허용
    srcs: ["aidl/android/hardware/example/*.aidl"],
    stability: "vintf", // VINTF 안정성 규격 강제
    backend: {
        cpp: { enabled: false },
        java: { enabled: false },
        ndk: {
            enabled: true, // 최신 Stable HAL의 핵심인 고성능 NDK 백엔드 활성화
        },
    },
}

// 2. 실제 구동될 하드웨어 HAL 데몬 서비스 바이너리 빌드 규칙
cc_binary {
    name: "android.hardware.example-service",
    relative_install_path: "hw",
    init_rc: ["android.hardware.example-service.rc"], // 부팅 시 자동 시작 스크립트
    vintf_fragments: ["android.hardware.example-service.xml"], // VINTF 매니페스트 조각
    vendor: true, // /vendor/bin/hw/ 경로에 안전하게 설치
    srcs: [
        "Example.cpp",
        "main.cpp",
    ],
    shared_libs: [
        "libbase",
        "libbinder_ndk", // Stable AIDL의 심장인 NDK 바인더 라이브러리
        "android.hardware.example-V1-ndk", // 컴파일러가 자동 생성해준 NDK 백엔드 라이브러리 참조
    ],
}

4. 3단계: 네이티브 C++ 코어 HAL 서비스 구현 (Example.cpp)

이제 빌드 시스템이 자동으로 만들어 준 기본 베이스 클래스(BnExample)를 구현체로 상속받아 알맹이 비즈니스 로직을 구현할 차례입니다.

C++
 
// hardware/interfaces/example/Example.cpp
#include <aidl/android/hardware/example/BnExample.h>
#include <android-base/logging.h>

namespace aidl::android::hardware::example {

class Example : public BnExample {
public:
    // 상위 레이어에서 하드웨어 제어 명령을 내릴 때 트리거
    ::ndk::ScopedAStatus setExampleData(const std::string& in_data) override {
        LOG(INFO) << "커텀 하드웨어 제어 데이터 수신: " << in_data;
        // 실무 환경: 여기서 write() 시스템 콜을 날려 디바이스 노드에 데이터를 밀어 넣습니다.
        return ::ndk::ScopedAStatus::ok();
    }

    // 상위 레이어에서 하드웨어 상태를 조회할 때 트리거
    ::ndk::ScopedAStatus getExampleData(std::string* _aidl_return) override {
        // 실무 환경: read() 시스템 콜로 센서 드라이버에서 긁어온 생데이터 매핑
        *_aidl_return = "Hello, Custom Hardware Native Data!";
        return ::ndk::ScopedAStatus::ok();
    }
};

} // namespace aidl::android::hardware::example

5. 4단계: 메인 진입점 및 서비스 등록 (main.cpp)

마지막으로 시스템 부팅 시 바이너리가 실행되면서 통합 servicemanager에 "나 센서 제어용 HAL인데 등록해 줘"라고 명함을 내미는 진입점 엔드포인트를 구축합니다.

C++
 
// hardware/interfaces/example/main.cpp
#include <android/binder_manager.h>
#include <android/binder_process.h>
#include "Example.cpp" // 위에서 구현한 클래스 헤더 참조 대체

using aidl::android::hardware::example::Example;

int main() {
    // 1. 커스텀 커스텀 커스텀 HAL 바이너리 로그 초기화
    android::base::InitLogging(nullptr, android::base::KernelLogger);
    ABnderProcess_setThreadPoolMaxThreadCount(4); // 바인더 요청을 받아낼 스레드풀 크기 지정

    // 2. 하드웨어 인스턴스 할당
    std::shared_ptr<Example> my_hal = ::ndk::SharedRefBase::make<Example>();
    
    // 3. 최신 Stable AIDL 전용 네임서버 등록 함수 가동
    const std::string instance_name = std::string() + Example::descriptor + "/default";
    binder_status_t status = AServiceManager_addService(my_hal->asBinder().get(), instance_name.c_str());

    if (status != STATUS_OK) {
        LOG(ERROR) << "커스텀 HAL 서비스 등록 실패!";
        return -1;
    }

    LOG(INFO) << "커스텀 Stable AIDL HAL 서비스 정상 기동 완료.";
    ABnderProcess_joinThreadPool(); // 스레드 루프 진입하여 무한 대기
    return 0;
}

6. 빌드 및 타겟 단말 실전 배포

모든 파일 준비가 끝났다면 AOSP 루트 디렉터리로 이동해 소오롱 컴파일러를 깨워줍니다.

Bash
 
# 1. 아키텍처 환경 변수 로드 및 타겟 런치 설정
source build/envsetup.sh
lunch aosp_arm64-eng # 본인의 타겟 보드 디바이스 셋으로 설정

# 2. 커스텀 HAL 서비스 바이너리 타겟 정밀 컴파일
m -j$(nproc) android.hardware.example-service

# 3. 빌드 완료된 아티팩트를 단말기 벤더 디렉터리에 강제 푸시 (루팅 권한 필수)
adb root && adb remount
adb push $OUT/vendor/bin/hw/android.hardware.example-service /vendor/bin/hw/

# 4. 바이너리 실행 권한 인가 및 백그라운드 수동 테스트 가동
adb shell chmod +x /vendor/bin/hw/android.hardware.example-service
adb shell /vendor/bin/hw/android.hardware.example-service &

# 5. 시스템 로그캣을 통해 커스텀 HAL 로그 모니터링
adb logcat | grep -i example-service

💡 실전 커스텀 HAL 모듈 개발을 위한 단단한 팁

  1. VINTF 매니페스트 조각(xml) 변경 사항 누락 주의: 최신 안드로이드 하드웨어 레이어는 VINTF(Vendor Interface Object)라는 보안 명세서 메커니즘을 엄격하게 검증합니다. 내 커스텀 HAL이 시스템 서비스 매니페스트 규격에 공식 등록되어 있지 않으면, 아무리 바이너리를 빌드해서 억지로 띄워봤자 상위 프레임워크가 AServiceManager_waitForService로 호출할 때 커널 단에서 통신을 원천 차단해 버립니다. 데몬 바이너리를 설계할 때 반드시 패키지 토큰명이 명시된 패그먼트 xml 파일을 작성하여 소스 트리 내 디바이스 선언부에 병합해 주어야 런타임에 안전하게 로드됩니다.
  2. HAL 프로세스 크래시 시 자동 재시작을 위한 init.rc 스크립트 튜닝: 물리 센서 보드와 통신하다 보면 정전기나 노이즈로 인해 HAL 서비스 데몬이 런타임 세그멘테이션 폴트(SIGSEGV)를 일으키며 뻗어버릴 수 있습니다. 이때 시스템이 영구적으로 멈추는 대참사를 막기 위해 cc_binary에 맵핑해 둔 .rc 스크립트 내부에 user halmaster, group imu_driver 등 최소한의 샌드박스 권한을 설정하고, oneshot 키워드를 제거하여 프로세스가 죽더라도 init 데몬이 자동으로 프로세스를 즉시 되살려내는 복원력 아키텍처를 가동해야 완성도 높은 양산형 제품이 됩니다.

⚠️ 흔히 하는 실수

  1. 더 이상 지원되지 않는 레거시 HIDL Passthrough 패턴의 코드 모방: 깃허브나 철 지난 기술 블로그에서 안드로이드 8.0~9.0 시절 예제를 보고 HIDL_FETCH_ 함수를 구현하거나 defaultPassthroughServiceImplementation 매크로 함수를 커스텀 소스에 그대로 복사해 붙여넣는 경우가 정말 많습니다. 최신 AOSP 마스터 빌드 트리에서는 해당 라이브러리 백엔드 툴체인 자체가 제외되었거나 빌드 타임에 에러(deprecated)를 뿜어내며 중단되므로, 신규 HAL 아키텍처는 머릿속에서 HIDL을 완전히 지우고 무조건 Stable AIDL과 NDK 백엔드 결합 구조로 코딩해야 공수를 낭비하지 않습니다.
  2. AIDL in/out/inout 방향성 키워드 오용으로 인한 데이터 복사 오버헤드: AIDL 파일에서 파라미터를 넘길 때 방향성을 뜻하는 in, out, inout 키워드를 무심코 전부 inout으로 지정해 버리는 실수가 잦습니다. inout을 쓰면 바인더 드라이버가 커널 공간에서 데이터를 보낼 때 한 번, 다시 함수가 끝난 후 호출자 메모리에 결과를 덮어쓸 때 또 한 번, 총 두 번의 무거운 메모리 전체 복사(Deep Copy)를 수행합니다. 하드웨어에 데이터를 주기만 하는 파라미터는 명확하게 in으로 단방향 제약을 걸어주어야 바인더 트랜잭션 오버헤드를 제로에 가깝게 줄일 수 있습니다.

7. 결론

나만의 커스텀 HAL을 작성하고 시스템 매니페스트에 정식 등록해 보는 경험은 안드로이드 주니어 플랫폼 엔지니어가 시스템 전체 아키텍처를 조망할 수 있는 시니어 개발자로 점프업하기 위한 가장 중요한 통과의례입니다. 복잡해 보이던 Stable AIDL 규격도 인터페이스 계약서 작성, NDK 백엔드 컴파일, 벤더 데몬 등록이라는 정형화된 4단계 공식을 이해하고 나면 아주 명쾌하게 내 손안에서 통제할 수 있습니다.

반응형