Почему падает внешняя компонента 1С на Android и как это исправить

Программист 1С v8.3 (Управляемые формы) IT и автоматизация бизнеса
← На главную

Разработка внешних компонент (ВК) для мобильной платформы 1С — задача повышенной сложности, так как она требует одновременного владения стеком технологий C++ (NDK), Java (Android SDK) и понимания внутренних механизмов взаимодействия 1С с операционной системой. Часто разработчики сталкиваются с тем, что компонента "вроде бы работает", но периодически вызывает аварийное завершение приложения (Crash) с ошибкой SIGSEGV. В этой статье мы подробно проанализируем причины таких падений на примере реального кейса и разберем способы их устранения.

Анализ проблемы: почему возникает SIGSEGV

Ошибка signal 11 (SIGSEGV) означает нарушение сегментации. Проще говоря, программа попыталась обратиться к участку памяти, который ей не принадлежит или уже был освобожден. В контексте Android и 1С это чаще всего происходит в трех случаях:

  1. Переполнение буфера при работе со строками: Несоответствие размеров типов данных C++ и Java.
  2. Некорректная работа с JNI-ссылками: Использование локальных ссылок (LocalRef) вместо глобальных (GlobalRef) или их несвоевременное удаление.
  3. Потоковая небезопасность: Вызов методов UI-потока Android из фоновых потоков 1С без надлежащей синхронизации.

Разберем каждую из этих причин на основе кода из обсуждения.

Критическая ошибка №1: Конвертация строк и размеры типов

Рассмотрим внимательно функцию конвертации wsz2jstr, которую использовал автор темы. На первый взгляд она кажется логичной, но именно здесь кроется "бомба замедленного действия":


static jstring wsz2jstr(std::wstring cstr) {
    JNIEnv* env = getJniEnv();
    Addin1C::platformString str = Addin1C::convertStringToPlatform(cstr);
    int len = str.size();
    jchar* raw = new jchar[len];
    memcpy(raw, str.c_str(), len * sizeof(wchar_t)); // ОШИБКА ЗДЕСЬ!
    jstring result = env->NewString(raw, len);
    delete[] raw;
    return result;
}

Разберем по шагам, что здесь происходит. В операционной системе Windows тип wchar_t обычно занимает 2 байта (UTF-16). Однако в Android (Linux) тип wchar_t занимает 4 байта (UTF-32). При этом тип jchar в JNI всегда строго равен 2 байтам.

Когда выполняется инструкция memcpy(raw, str.c_str(), len * sizeof(wchar_t)), происходит следующее: мы выделяем массив raw размером len * 2 байт, но пытаемся скопировать в него данные объемом len * 4 байт. Это приводит к классическому heap buffer overflow (переполнению кучи). Мы затираем память, находящуюся за пределами массива raw. В этой памяти могут лежать системные указатели 1С или структуры JNI. Приложение падает не в момент копирования, а позже, когда система пытается обратиться к поврежденным данным — например, в методе StopAPKMethod().

Как исправить: Необходимо использовать преобразование в UTF-16. Самый надежный способ для Android — использовать стандартные средства конвертации или передавать строки через UTF-8:


jstring wsz2jstr(JNIEnv* env, const std::wstring& wstr) {
    // Конвертируем wstring (UTF-32) в string (UTF-8)
    std::wstring_convert<std::codecvt_utf8<wchar_t>, wchar_t> converter;
    std::string utf8_str = converter.to_bytes(wstr);
    return env->NewStringUTF(utf8_str.c_str());
}

Критическая ошибка №2: Управление ссылками JNI (Global vs Local)

В JNI существует два типа ссылок на объекты Java: локальные и глобальные. Локальные ссылки (LocalRef) автоматически освобождаются при выходе из функции Native-кода. Глобальные ссылки (GlobalRef) живут до тех пор, пока разработчик не удалит их вручную через DeleteGlobalRef.

Проанализируем метод Initialize:


jclass ccloc = helper->FindClass(...);
if (ccloc) {
    cc = static_cast<jclass>(env->NewGlobalRef(ccloc));
    env->DeleteLocalRef(ccloc);
    // ...
    jobject objloc = env->NewObject(cc, methID, ...);
    obj = static_cast<jobject>(env->NewGlobalRef(objloc));
    env->DeleteLocalRef(objloc);
}

Здесь автор правильно создает глобальные ссылки для cc (класс) и obj (экземпляр объекта). Однако в методе StopAPKMethod происходит удаление этих ссылок:


void StopAPKMethod(){
    if (obj) {
        JNIEnv* env = getJniEnv();
        env->CallVoidMethod(obj, methID);
        env->DeleteGlobalRef(obj);
        env->DeleteGlobalRef(cc);
        obj = nullptr;
        cc = nullptr;
    }
}

Важный нюанс: Если метод StopAPKMethod будет вызван дважды (например, один раз программно из 1С, а второй раз платформой при уничтожении объекта в методе Done), возникнет попытка удалить уже невалидный указатель, что приведет к моментальному падению. Необходимо всегда проверять указатели и обнулять их атомарно.

Критическая ошибка №3: Работа с потоками (UI Thread)

В коде Java-части мы видим работу с BroadcastReceiver:


public void start() {
    mActivity.registerReceiver(mReceiver, new IntentFilter(mIntentAction));
}
public void stop() {
    mActivity.unregisterReceiver(mReceiver);
}

Методы registerReceiver и unregisterReceiver в Android должны вызываться в том же потоке, где живет Activity (в главном UI-потоке). Внешняя компонента 1С вызывает Native-методы в своих рабочих потоках. Если 1С вызовет StopAPKMethod в момент, когда Android занят обновлением интерфейса или пересозданием Activity, возникнет конфликт.

Решение: Всегда оборачивайте работу с системными сервисами Android в runOnUiThread:


public void stop() {
    mActivity.runOnUiThread(new Runnable() {
        @Override
        public void run() {
            if (mReceiver != null) {
                mActivity.unregisterReceiver(mReceiver);
                mReceiver = null;
            }
        }
    });
}

Методика отладки Native-кода в связке с 1С

Выясним причину, почему стандартная отладка часто не работает. Когда вы запускаете проект из Android Studio, она ожидает, что библиотека .so будет упакована внутри вашего APK. Но в случае с 1С ситуация иная: мобильная платформа сама загружает библиотеку из своих внутренних каталогов (куда она распаковала её из макета 1С).

Для эффективной отладки выполните следующие шаги:

  1. Использование логов (Logcat): Это самый надежный способ. Вместо вывода в консоль используйте стандартный лог Android NDK:
    
    #include <android/log.h>
    #define LOG_TAG "MyComponent"
    #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
    // Использование:
    LOGD("Инициализация успешно пройдена, объект: %p", obj);
    
  2. Attach Debugger: В Android Studio выберите пункт Run -> Attach Debugger to Android Process. В списке процессов выберите ваше приложение 1С. Чтобы отладчик "подцепил" исходный код C++, убедитесь, что библиотека собрана с отладочными символами (в CMakeLists.txt должен быть установлен тип сборки Debug).
  3. Проверка жизненного цикла: Убедитесь, что вы не вызываете JNI-методы после того, как вызвали DeleteGlobalRef. В Native-коде полезно добавить проверки:
    
    if (env->IsSameObject(obj, NULL)) {
        LOGD("Объект уже удален Garbage Collector-ом или вручную");
        return;
    }
    

Итоговые рекомендации для стабильной работы

Проанализировав ситуацию, мы можем составить чеклист для разработки надежной ВК под Android:

Соблюдение этих правил позволит избежать трудноуловимых багов, связанных с памятью, и сделает вашу мобильную разработку на базе 1С стабильной и предсказуемой.

← На главную