Разница между API и ABI

Я новичок в системном программировании Linux, и я столкнулся с API и ABI, читаясистемное программирование Linux.

Определение API:

API определяет интерфейсы, с помощью которых одна часть программного обеспечения взаимодействует с другой на уровне источника.

Определение ABI:

Принимая во внимание, что API определяет интерфейс источника, ABI определяет низкоуровневый двоичный интерфейс между двумя или более частями программного обеспечения в конкретной архитектуре. Он определяет, как приложение взаимодействует с самим собой, как приложение взаимодействует с ядром и как приложение взаимодействует с библиотеками.

Как программа может общаться на уровне источника? Что такое уровень источника? Это связано с исходным кодом в любом случае? Или источник библиотеки включается в основную программу?

Единственное отличие, которое я знаю, это то, что API в основном используется программистами, а ABI - компилятором.

13 ответов

Решение

API - это то, что используют люди. Мы пишем исходный код. Когда мы пишем программу и хотим использовать некоторую библиотечную функцию, мы пишем такой код:

 long howManyDecibels = 123L;
 int ok = livenMyHills( howManyDecibels);

и нам нужно было знать, что есть метод livenMyHills(), который принимает длинный целочисленный параметр. Так что как интерфейс программирования все это выражается в исходном коде. Компилятор превращает это в исполняемые инструкции, которые соответствуют реализации этого языка в данной операционной системе. И в этом случае приводят к некоторым операциям низкого уровня на аудиоустройстве. Таким образом, определенные биты и байты выделяются на некотором оборудовании. Так что во время выполнения происходит множество действий на двоичном уровне, которые мы обычно не видим.

API: интерфейс прикладных программ

Это набор открытых типов / переменных / функций, которые вы выставляете из своего приложения / библиотеки.

В C/C++ это то, что вы выставляете в заголовочных файлах, которые поставляются вместе с приложением.

ABI: двоичный интерфейс приложения

Вот как компилятор создает приложение.
Он определяет вещи (но не ограничивается ими):

  • Как параметры передаются в функции (регистры / стек).
  • Кто убирает параметры из стека (вызывающий / вызываемый).
  • Где возвращаемое значение помещается для возврата.
  • Как распространяются исключения.

Я в основном сталкиваюсь с этими терминами в смысле API-несовместимых изменений или ABI-несовместимых изменений.

Изменение API, по сути, происходит там, где код, который был бы скомпилирован с предыдущей версией, больше не будет работать. Это может произойти, потому что вы добавили аргумент в функцию или изменили имя чего-то доступного за пределами вашего локального кода. Каждый раз, когда вы изменяете заголовок, и это заставляет вас что-то менять в файле.c/.cpp, вы вносите изменение API.

Изменение ABI - это то, где код, который уже скомпилирован для версии 1, больше не будет работать с версией 2 кодовой базы (обычно это библиотека). Это обычно сложнее, чем отслеживать изменения, несовместимые с API, поскольку такая простая вещь, как добавление виртуального метода в класс, может быть несовместимой с ABI.

Я нашел два чрезвычайно полезных ресурса для выяснения, что такое совместимость с ABI и как ее сохранить:

API-интерфейс совместно используемой библиотеки Linux с примером ABI

Этот ответ был извлечен из моего другого ответа: Что такое двоичный интерфейс приложения (ABI)? но я чувствовал, что это прямо отвечает и на этот вопрос, и что вопросы не являются дубликатами.

В контексте разделяемых библиотек наиболее важным следствием "наличия стабильного ABI" является то, что вам не нужно перекомпилировать свои программы после изменений библиотеки.

Как мы увидим в приведенном ниже примере, можно изменять ABI, нарушая работу программ, даже если API не изменился.

main.c

#include <assert.h>
#include <stdlib.h>

#include "mylib.h"

int main(void) {
    mylib_mystrict *myobject = mylib_init(1);
    assert(myobject->old_field == 1);
    free(myobject);
    return EXIT_SUCCESS;
}

mylib.c

#include <stdlib.h>

#include "mylib.h"

mylib_mystruct* mylib_init(int old_field) {
    mylib_mystruct *myobject;
    myobject = malloc(sizeof(mylib_mystruct));
    myobject->old_field = old_field;
    return myobject;
}

mylib.h

#ifndef MYLIB_H
#define MYLIB_H

typedef struct {
    int old_field;
} mylib_mystruct;

mylib_mystruct* mylib_init(int old_field);

#endif

Компилируется и работает нормально с:

cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out

Теперь предположим, что для v2 библиотеки мы хотим добавить новое поле в mylib_mystrict называется new_field,

Если мы добавили поле раньше old_field как в:

typedef struct {
    int new_field;
    int old_field;
} mylib_mystruct;

и перестроить библиотеку, но не main.outтогда утверждать не удастся!

Это потому что строка:

myobject->old_field == 1

сгенерировал сборку, которая пытается получить доступ к самому первому int структуры, которая сейчас new_field вместо ожидаемого old_field,

Поэтому это изменение сломало ABI.

Если, однако, мы добавим new_field после old_field:

typedef struct {
    int old_field;
    int new_field;
} mylib_mystruct;

тогда старая сгенерированная сборка все еще обращается к первой int структуры, и программа все еще работает, потому что мы сохранили ABI стабильным.

Вот полностью автоматизированная версия этого примера на GitHub.

Другой способ сохранить стабильность ABI - лечить mylib_mystruct как непрозрачная структура, и доступ к ее полям только через помощников метода. Это облегчает поддержание стабильности ABI, но может повлечь за собой снижение производительности, поскольку мы выполняем больше вызовов функций.

API против ABI

В предыдущем примере интересно отметить, что добавление new_field до old_field, только сломал ABI, но не API.

Это означает, что если бы мы перекомпилировали main.c Программа против библиотеки, это работало бы независимо.

Однако мы бы также нарушили API, если бы изменили, например, сигнатуру функции:

mylib_mystruct* mylib_init(int old_field, int new_field);

так как в этом случае, main.c перестанет компилироваться вообще.

Семантический API против API программирования против ABI

Мы также можем классифицировать изменения API по третьему типу: семантические изменения.

Например, если мы изменили

myobject->old_field = old_field;

чтобы:

myobject->old_field = old_field + 1;

тогда это не сломало бы ни API, ни ABI, но main.c все равно сломается!

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

У меня только что было философское понимание, что формальная проверка программного обеспечения в некотором смысле перемещает больше "семантического API" в более "программно проверяемый API".

Семантический API против API программирования

Мы также можем классифицировать изменения API по третьему типу: семантические изменения.

Семантический API, как правило, представляет собой естественное описание того, что должен делать API, обычно включается в документацию API.

Поэтому возможно нарушить семантический API, не нарушая саму сборку программы.

Например, если мы изменили

myobject->old_field = old_field;

чтобы:

myobject->old_field = old_field + 1;

тогда это не сломало бы ни API программирования, ни ABI, но main.c семантический API сломался бы.

Существует два способа программной проверки API контракта:

  • проверить кучу угловых случаев. Это легко сделать, но вы всегда можете пропустить один.
  • формальная проверка. Сложнее сделать, но производит математическое доказательство правильности, по существу объединяя документацию и тесты в "человеческий" / машинно проверяемый способ! Пока, конечно, в вашем официальном описании нет ошибки;-)

Протестировано в Ubuntu 18.10, GCC 8.2.0.

Это мои непрофессиональные объяснения:

  • api - думай include файлы. они обеспечивают интерфейс программирования
  • abi - думаю, модуль ядра. когда вы запускаете его на каком-то ядре, они должны договориться о том, как общаться без включаемых файлов, то есть как низкоуровневый двоичный интерфейс

(Приложение B inary I интерфейс) Спецификация для конкретной аппаратной платформы в сочетании с операционной системой. Это один шаг за API (интерфейс приложения), который определяет вызовы из приложения в операционную систему. ABI определяет API плюс машинный язык для определенного семейства процессоров. API не гарантирует совместимость во время выполнения, но ABI делает, потому что он определяет машинный язык или формат времени исполнения.

введите описание изображения здесь

учтивость

Позвольте мне привести конкретный пример того, как ABI и API отличаются в Java.

ABI несовместимое изменение, если я изменяю метод A#m() от принятия String в качестве аргумента String... аргумент. Это не совместимо с ABI, потому что вы должны перекомпилировать код, который вызывает это, но это API-совместимый, поскольку вы можете разрешить его путем перекомпиляции без каких-либо изменений кода в вызывающей стороне.

Вот пример, изложенный. У меня есть библиотека Java с классом A

// Version 1.0.0
public class A {
    public void m(String string) {
        System.out.println(string);
    }
}

И у меня есть класс, который использует эту библиотеку

public class Main {
    public static void main(String[] args) {
        (new A()).m("string");
    }
}

Теперь, автор библиотеки скомпилировал их класс A, я скомпилировал свой класс Main, и все это прекрасно работает. Представьте себе новую версию A

// Version 2.0.0
public class A {
    public void m(String... string) {
        System.out.println(string[0]);
    }
}

Если я просто возьму новый скомпилированный класс A и урону его вместе с ранее скомпилированным классом Main, я получу исключение при попытке вызвать метод

Exception in thread "main" java.lang.NoSuchMethodError: A.m(Ljava/lang/String;)V
        at Main.main(Main.java:5)

Если я перекомпилирую Main, это исправлено, и все снова работает.

Ваша программа (исходный код) может быть скомпилирована с модулями, которые предоставляют надлежащий API.

Ваша программа (двоичная) может работать на платформах, которые обеспечивают надлежащий ABI.

API ограничивает определения типов, определения функций, макросы, иногда глобальные переменные, которые должна предоставлять библиотека.

ABI ограничивает то, что "платформа" должна предоставить вашей программе для запуска. Мне нравится рассматривать это в 3 уровнях:

  • уровень процессора - набор инструкций, соглашение о вызовах

  • уровень ядра - соглашение о системных вызовах, специальное соглашение о пути к файлу (например, /proc а также /sys файлы в Linux) и др.

  • Уровень ОС - формат объекта, библиотеки времени выполнения и т. Д.

Рассмотрим кросс-компилятор с именем arm-linux-gnueabi-gcc, "arm" обозначает архитектуру процессора, "linux" обозначает ядро, "gnu" указывает, что его целевые программы используют libc GNU как библиотеку времени выполнения, отличную от arm-linux-androideabi-gcc которые используют реализацию libc Android.

API - Application Programming Interface- это интерфейс времени компиляции, который может использоваться разработчиком для использования не связанных с проектом функций, таких как библиотека, ОС, вызовы ядра в исходном коде

ABI [О себе] -Application Binary Interfaceэто интерфейс времени выполнения, который используется программой во время выполнения для связи между компонентами в машинном коде

  • API — интерфейс прикладного программирования
    • связь между программным обеспечением на исходном уровне
    • Независимая платформа
  • ABI — двоичный интерфейс приложения
    • Низкоуровневый двоичный интерфейс между программным обеспечением на «определенной архитектуре».
    • Зависит от платформы

ABI относится к макету объектного файла / библиотеки и конечного двоичного файла с точки зрения успешного связывания, загрузки и выполнения определенных двоичных файлов без ошибок связи или логических ошибок, возникающих из-за двоичной несовместимости.

  • Спецификация двоичного формата (PE, COFF, ELF, .obj, .o, .a, .lib (библиотека импорта, статическая библиотека), сборка .NET, .pyc, COM .dll): заголовки, формат заголовка, определение где находятся разделы и где находятся таблицы импорта / экспорта / исключений и формат этих
  • Набор команд, используемый для кодирования байтов в разделе кода, а также конкретных машинных инструкций
  • Фактическая подпись функций и данных, как определено в API (а также то, как они представлены в двоичном формате (следующие 2 балла))
  • Соглашение о вызовах экспортируемых функций в разделе кода, которые могут быть вызваны другими двоичными файлами
  • Способ представления и выравнивания экспортированных данных в разделе данных по их типу
  • Номера системных вызовов или векторы прерываний, привязанные к коду
  • Украшение имени экспортируемых функций и данных
  • Директивы компоновщика в объектных файлах
  • Флаги и директивы препроцессора / компилятора / ассемблера / компоновщика, используемые программистом API, и их интерпретация для исключения, оптимизации, встраивания или изменения связывания определенных символов или кода в библиотеке или окончательном двоичном файле (будь то двоичный файл .dll или исполняемый файл в случае статической ссылки)

Формат байт-кода .NET - это ABI, который включает в себя формат DLL сборки .NET. Виртуальная машина, которая интерпретирует байт-код, имеет ABI, основанный на C++, где типы необходимо упорядочить между собственными типами C++, которые использует ABI собственного кода, и упакованными типами ABI виртуальной машины при вызове байт-кода из машинного кода и машинного кода из байт-код.

API относится к набору определений типов, экспортируемых определенной библиотекой, импортированной и используемой в конкретной единице трансляции, с точки зрения компилятора единицы трансляции, для успешного разрешения и проверки ссылок на типы, чтобы иметь возможность скомпилировать двоичный файл, и этот двоичный файл будет соответствовать стандарту целевого ABI, так что если библиотека, которая фактически реализует API, также скомпилирована в совместимый ABI, она будет связываться и работать по назначению. Если API изменяется, если приложение продолжает использовать старый API, оно будет компилироваться, но не должно использовать новую библиотеку, потому что ABI библиотеки изменился, даже если использует тот же компилятор и флаги компиляции.

API включает в себя:

  • Функции, переменные, классы, объекты, константы, их имена, типы и определения, представленные на языке, на котором они кодируются синтаксически и семантически правильным образом
  • Что на самом деле делают эти функции и как их использовать в исходном языке
  • Файлы исходного кода, которые необходимо включить / двоичные файлы, которые необходимо связать, чтобы использовать их, и их совместимость с ABI

Application programming interface (API) - представлен высшим уровнем абстракции. Этот API связывает приложения с библиотеками и / или основной ОС.

Application Binary Interface (ABI) охватывает факты, такие как типы данных низкого уровня и соглашения о вызовах, а также определяет формат для многих программ. В основном, системные вызовы определяются на этом уровне. Кроме того, этот тип интерфейса обеспечивает переносимость различных приложений и библиотек в ОС, в которых используется один и тот же ABI.

Узнайте больше здесь

Я начну с ответов на ваши конкретные вопросы.

1.Что такое исходный уровень? Это как-то связано с исходным кодом?

Да, термин уровень исходного кода относится к уровню исходного кода. Термин «уровень» относится к семантическому уровню требований к вычислениям, когда они переводятся с уровня предметной области приложения на уровень исходного кода и с уровня исходного кода на уровень машинного кода (двоичные коды). Уровень домена приложения относится к тому, что конечные пользователи программного обеспечения хотят и указывают в качестве своих вычислительных требований. Уровень исходного кода относится к тому, что программисты делают из требований уровня приложения, а затем определяют как программу на определенном языке.

  1. Как программа может взаимодействовать на исходном уровне? Или исходник библиотеки включается в основную программу?

Языковой API относится конкретно ко всему, что язык требует (указывает) (следовательно, интерфейсы) для написания повторно используемых модулей на этом языке. Повторно используемая программа соответствует этим требованиям к интерфейсу (API) для повторного использования в других программах на том же языке. Каждое повторное использование также должно соответствовать тем же требованиям API. Итак, слово «общаться» относится к повторному использованию.

Да, включение исходного кода (многоразового модуля; в случае C/C++ файлов .h) (копирование на этапе предварительной обработки) является распространенным способом повторного использования в C/C++ и, таким образом, является частью C++ API. Даже когда вы просто пишете простую функцию foo() в глобальном пространстве программы на C++, а затем вызываете функцию как foo();любое количество раз используется повторно в соответствии с API языка C++. Классы Java в пакетах Java являются повторно используемыми модулями в Java. Спецификация Java-бинов также является Java API, позволяющим многократно использовать программы (бины) для повторного использования другими модулями (может быть другим компонентом) с помощью сред выполнения/контейнеров (соответствующих этой спецификации).

Что касается вашего общего вопроса о разнице между языковым API и ABI и о том, как сервис-ориентированные API сравниваются с языковыми API, мой ответ здесь, на SO, должен быть полезен.

Другие вопросы по тегам