리눅스 & 안드로이드2016. 1. 22. 13:41

Input Device의 input event를 App.에서 직접 받아 볼 필요가 있어, NDK로 epoll을 이용해 구현 했던 내용을 정리 해 본다.

epoll 관련 설명은 The Event Poll Interface를 참고 하였다.


우선, device driver file의 fd를 사용해야 하기 때문에, 해당 device file을 열 수 있는 권한을 App.이 가지고 있어야 한다. (예를 들면 App.이 system 권한을 가지고 있어야 한다.)

아래 예제 소스 각각의 특징에 대해서, 간략하게만 정리 해 둔다. 구현 했던 걸, 생각나는 대로 다시 정리한 것이라서, 그대로 copy & paste 하면 제대로 빌드가 되지 않을 수는 있겠으나, 전체적인 맥락은 유효하니 나중에 필요할 때 다시 볼 수 있을 것으로 믿는다.ㅎㅎ


[Header]

  • epoll이나 device driver 파일을 열고나면, 열린 file descriptor를 복사한다는거 자체가 적절하지 않으므로, 복사/대입을 막아 두었다.
  • epoll로 이벤트를 받고 처리하기위해 thread를 하나 만들었으며, 전달 받은 이벤트를 container에 저장 해 두고 사용하기 위해서 mutex를 하나 두었다.
  • JNI에서 instance를 생성하고 사용해야 하므로, 간단하게 constructor에서 모든 초기화를 마치고, destructor에서 file descriptor등을 닫도록 처리 했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#ifndef __EPOLL_SAMPLE_H__
#define __EPOLL_SAMPLE_H__
 
#include <string>
#include <thread>
#include <mutex>
 
class EpollSample
{
private:
    const int EPOLL_HINT_SIZE = 1;
 
private:
    bool mInitialized;
    int mEpollFd;
    int mDeviceFd;
 
    bool mThreadDone;
    std::thread mEventThread;
    std::mutex mMutex;
 
public:
    explicit EpollSample(std::string const& devicePath);
    virtual ~EpollSample();
    EpollSample(EpollSample const&) = delete;
    EpollSample(EpollSample&&) = delete;
    EpollSample& operator=(EpollSample const&) = delete;
    EpollSample& operator=(EpollSample&&) = delete;
 
    bool isInitialized() const noexcept { return mInitialized; }
 
    int getEpollFd() const noexcept { return mEpollFd; }
    int getDeviceFd() const noexcept { return mDeviceFd; }
     
    bool isEventThreadAvilable() const noexcept { return !mThreadDone; }
    void terminateEventThread() noexcept;
};
 
#endif //__EPOLL_SAMPLE_H__


[Source]

  • threadHandler에서는 epoll_wait으로 이벤트를 무한정 대기한다.(timeout 시간을 지정할 수도 있음)
  • 정상적으로 이벤트를 받은 경우, epoll_wait이 이벤트의 개수를 반환한다. 이 개수 만큼, 대상 file descriptor를 통해 이벤트 데이터를 읽어 들인다.
  • 오류를 검사하고, input_event 구조체를 통해 이벤트 데이터를 활용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
#include <linux/input.h>
#include <sys/epoll.h>
#include <errno.h>
#include <EpollSample.h>
 
void threadHandler(EpollSample& instance)
{
    if (&instance == nullptr)
    {
        //("EpollSample instance is null!");
        return;
    }
     
    static int const EPOLL_MAX_EVENTS = 16;
    struct epoll_event eventItems[EPOLL_MAX_EVENTS];
 
    static int const EVENT_BUFFER_SIZE = 256;
    struct input_event eventBuffer[EVENT_BUFFER_SIZE];
 
    while (instance.isEventThreadAvilable()) {
 
        int const numEvents = epoll_wait(instance.getEpollFd(), eventItems,
                                    EPOLL_MAX_EVENTS, -1);
 
        if (numEvents < 0 && (errno == EBADF || errno == EINVAL))
        {
            //("epfd is not a valid file descriptor or a valid epoll instance");
            break;
        }
 
        for (auto i = 0; i < numEvents; ++i)
        {
            struct epoll_event& epollEvent = eventItems[i];
            if (epollEvent.events & EPOLLIN)
            {
                int const readSize = ::read(instance.getDeviceFd(), eventBuffer,
                    sizeof(struct input_event) * EVENT_BUFFER_SIZE);
 
                if (readSize <= 0)
                {
                    //("Could not get event, errno = %d", errno);
                    break;
                }
                else if (readSize % sizeof(struct input_event) != 0)
                {
                    //("Could not get event, wrong size = %d", readSize);
                    break;
                }
                else
                {
                    auto const numInputEvents = static_cast<std::size_t>(readSize)
                                         / sizeof(struct input_event);
                    for (auto j = 0; j < numInputEvents; ++j)
                    {
                        struct input_event& inputEvent = eventBuffer[j];
                        //Do something
                    }
                }
            }
            else if (epollEvent.events & EPOLLHUP)
            {
                //("The epoll hang-up event has been received");
            }
            else
            {
                //("Unexpected epoll event has been received, event = 0x%08x",
                     epollEvent.events);
            }
        }
    }
}
 
EpollSample::EpollSample(std::string const & devicePath)
    : mInitialized(false), mEpollFd(-1), mDeviceFd(-1), mThreadDone(false)
{
    mEpollFd = epoll_create(EPOLL_HINT_SIZE);
    if (mEpollFd < 0)
    {
        //("Could not create epoll instance!");
        return;
    }
 
    mDeviceFd = ::open(devicePath.c_str(), O_RDWR | O_CLOEXEC);
    if (mDeviceFd < 0)
    {
        //("Could not open virtual device to hook: %d", mDeviceFd);
        //("errno: %d", errno);
        return;
    }
 
    char buffer[80];
    if (ioctl(mDeviceFd, EVIOCGNAME(sizeof(buffer)-1), &buffer) < 1)
    {
        //("Could not get the device name");
    }
    else
    {
        buffer[sizeof(buffer) - 1] = '\0';
        //("The name of opened device = %s", buffer);
    }
 
    if (fcntl(mDeviceFd, F_SETFL, O_NONBLOCK)) {
        //("Error %d making device file descriptor non-blocking.", errno);
        return;
    }
 
    struct epoll_event evt;
    memset(&evt, 0, sizeof(evt));
    evt.events = EPOLLIN;
    if (epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mDeviceFd, &evt))
    {
        //("Could not add device fd to epoll instance. errno=%d", errno);
        return;
    }
 
    mEventThread = std::thread(threadHandler, std::ref(*this));
 
    mInitialized = true;
}
 
EpollSample::~EpollSample()
{
    mInitialized = false;
 
    if (isEventThreadAvilable()) {
        terminateEventThread();
    }
 
    if (mEpollFd >= 0)
    {
        close(mEpollFd);
    }
 
    if (mDeviceFd >= 0)
    {
        close(mDeviceFd);
    }
}
 
void EpollSample::terminateEventThread() noexcept
{
    mThreadDone = true;
    mEventThread.join();
}


[JNI interface for JAVA]

  • App.에서 JAVA로 호출하게 될 native 함수 정의이다.
  • native도 복사/대입 금지를 하도록 되어 있으므로, JAVA interface도 singleton으로 구현하는 것이 적절하다고 판단 되므로, static으로 선언!
  • native instance의 pointer를 JAVA instance에서 보관하고 활용하는 방식으로 구현되어 있으므로, native interface를 추가하고 활용하기 위해서는, long 타입의 native instance pointer를 전달하는 것이 필요하다.
※ 참고로 native library를 동적으로 로드하기 위해 아래 코드가 JAVA layer에 추가되어 있어야 함.

static {
System.loadLibrary("epoll_sample");
}

1
2
private static native long nativeInit();
private static native void nativeFinalize(long ptr);


[JNI interface for C/C++]

  • App.이 native 초기화를 진행하면, native instance를 생성하면서 초기화를 진행하고, 문제가 없으면 instance pointer를 반환한다.
  • 현재는 device driver 파일경로를 직접 표시 했는데, android property 등의 환경변수나 다른 입력을 통해 전달 받도록 할 수 있겠다.

1
2
3
4
5
6
7
8
9
10
11
12
JNIEXPORT jlong JNICALL nativeInit(JNIEnv *, jclass)
{
    EpollSample* epollSample = new EpollSample("/dev/input/event3");
    assert(epollSample != nullptr);
    return reinterpret_cast<jlong>(epollSample);
}
 
JNIEXPORT void JNICALL nativeFinalize(JNIEnv *, jclass, jlong ptr)
{
    EpollSample* epollSample = reinterpret_cast<EpollSample*>(ptr);
    delete epollSample;
}


[Android.mk]

  • 일반적인 NDK makefile이며, C++11을 사용하기위해 -std=c++11을 추가함. (Application.mk에서와 중복되어 있다)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LOCAL_PATH:=$(call my-dir)
include $(CLEAR_VARS)
 
LOCAL_CFLAGS := \
    -DLOG_NDEBUG=0 \
    -std=c++11
 
LOCAL_C_INCLUDES := \
    $(JNI_H_INCLUDE) \
    $(LOCAL_PATH)/include
 
LOCAL_SRC_FILES := \
    EpollSample.cpp
 
LOCAL_LDLIBS := \
    -llog
 
LOCAL_MODULE := libepoll_sample
 
include $(BUILD_SHARED_LIBRARY)


[Application.mk]

  • GCC 4.9와 STL 사용을 위해 명시적으로 추가 해 두었다.
  • platform이 32bit/64bit ABI를 사용하므로 두 종류 모두 빌드 할 수 있도록, armeabi-v7a, arm64-v8a를 추가 했다.

1
2
3
4
NDK_TOOLCHAIN_VERSION := 4.9
APP_STL := gnustl_shared
APP_ABI := armeabi-v7a arm64-v8a
APP_CPPFLAGS += -std=c++11


※ NDK로는 아무 문제없이 빌드가 되는데, Android platform에 추가해서 빌드를 해 보니 STL header(string 등등)의 path를 기본적으로 찾지 못했고, prebuilt에 포함된 NDK v9을 이용해 빌드 하면 컴파일은 가능하지만, arm64-v8a binary link를 진행하면서 unsupported ELF machine number 183 에러가 발생하였다. 구글링을 통해 google group이나 stack overflow의 글들을 찾아 보고, 적용해 볼만한건 다 해 봤지만 빌드가 되지 않았다.

해결 방법을 아시는 분은 덧글로 알려주시면 감사하겠습니다. (__)


Posted by 세월의돌