05/03/2020

Великолепная Java (1): сборка проекта — компиляция, JAR

Как начинающим изучать новый язык программирования, так и опытным разработчикам иногда нужно разобраться в том как из кода получается «конечный продукт» — программа, которую можно запускать и поставлять пользователям. В этой статье мы рассмотрим все этапы, необходимые для того чтобы из Java-кода получить программу. Будем изучать Java Basic Tools или Java Development Tools, то есть, ручную, консольную сборку проекта. JDK является основным инструментом в работе с Java. Инструменты эти находятся в каталоге $JAVA_HOME/bin (например /opt/JDK-1.8.0_221/bin/).
Напишем простую программу, файл назовём Hello.java:
class HelloWorld
{
  public static void main (String args[])
  {
    System.out.println ("Hello, Java-World!");
  }
}
Для того чтобы скомпилировать Java-программу, нужно выполнить:
$JAVA_HOME/bin/javac ./Hello.java
javac — компилятор Java, транслирует .java-файлы в .class-файлы. В нашем случае, после работы javac в каталоге появится файл HelloWorld.class. Чтобы запустить нашу программу выполняем:
$JAVA_HOME/bin/java HelloWorld
Обратите внимание на три вещи. Первое — запускаем мы именно класс, то есть, нужно указывать не имя .class-файла, а имя класса (то есть имя .class-файла без расширения). Java сама найдёт (или не найдёт) в текущем каталоге файл, содержащий заданный в параметре класс. Поскольку мы указываем имя класса, а не файла, «./» перед ним ставить не нужно — это приведёт к ошибке. Второе — Java чувствительна к регистру в именах классов, файлов, переменных, поэтому все названия должны указываться с учётом регистра. Третье — каждый .class-файл соответствует имени класса, который он содержит. Имя .class-файла не связано с именем .java-файла, в котором он был реализован. Если в одном .java-файле будут реализованы несколько классов, то javac «разберёт» этот файл на соответствующее количество .class-файл'ов. Продемонстрируем это примером. Дополним файл Hello.java другим классом Hello (дополните предыдущий файл этим фрагментом кода):
class Hello
{
  public static void main(String _Args[])
  {
    try
    {
      System.out.println ("Hello, " + _Args[0] + "!");
    } catch (ArrayIndexOutOfBoundsException _E)
    {
      System.out.println ("Please pass parameter to program.");
    }
  }
}
P.S.
В Java список аргументов организован несколько иначе: нулевой элемент — это уже первый параметр, а не имя исполняемого файла.
Теперь, после компиляции (та же команда, что мы использовали в первый раз) мы увидим два .class-файла: Hello.class и HelloWorld.class. Так как оба файла имеют функцию main (), мы можем запустить оба. HelloWorld — как описано выше, и Hello (который принимает параметр командной строки):
$JAVA_HOME/bin/java Hello Daft
Отсюда мы постепенно приходим к необходимости создания какого-то единого файла для поставки пользователям. Мы же не хотим высылать пользователям море непонятных ему .class-файлов и объяснять какой именно является главным и как его запускать, так?
Для этих целей в Java реализованы Java-архивы (.jar-файлы). Чтобы создать .jar-файл и записать в него класс, нужно выполнить команду:
$JAVA_HOME/bin/jar -cvf ./Hello.jar ./Hello.class
Эта команда заархивирует указанный .class-файл в .jar-файл с заданным именем.
Чтобы запустить .jar-файл выполняем следующее:
$JAVA_HOME/bin/java -cp ./Hello.jar Hello
В предыдущей команде мы запаковывали .class-файл и указывали имя файла (с расширением), здесь мы запускаем класс — указываем имя класса (без расширения). Ключ -cp означает classpath — путь, где нужно искать заданный класс. В данной ситуации — это наш .jar-файл.
Как я уже указывал, .jar-файл является архивом и он действительно сжимается при создании, что дополнительно экономит место на носителе или время при передаче его по сети. Конкретно это zip-архив, мы можем посмотреть его содержимое:
unzip -l ./Hello.jar
Увидим:
Archive:  ./Hello.jar
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-04-2020 17:50   META-INF/
       69  03-04-2020 17:50   META-INF/MANIFEST.MF
      736  03-04-2020 17:36   Hello.class
---------                     -------
      805                     3 files
В вышеописанную команду запаковки можно добавить не только классы, но и любые другие файлы, которые в последствии можно будет использовать как ресурсы из ваших программ (иконки, картинки, любое иное содержание и даже другие .jar-файлы).
Но пока запуск скомпилированных и даже уже запакованных в один файл классов является всё-таки не самым «дружественным». Как упростить запуск .jar-файла? Для этого предусмотрена возможность указать главный класс, добавив ключ e (что означает указать точку входа в программу, entry point): 
$JAVA_HOME/bin/jar -cvfe ./Hello.jar Hello ./Hello.class
Теперь мы можем запустить .jar-файл самым простым для Java способом:
$JAVA_HOME/bin/java -jar ./Hello.jar
На этом на сегодня всё. В следующих статьях мы рассмотрим сборку более сложных Java-программ (да, ещё есть куда двигаться).

25/02/2020

Многопоточность в POSIX или Multithreading в ANSI C (1): поток и процесс, начало работы с libpthread

В этой статье мы научимся создавать, запускать и останавливать потоки в POSIX-совместимых системах на Plain C.
Сначала вкратце рассмотрим, что такое поток. Воспользуемся последним стандартом POSIX.1-2017, чтобы разобраться в терминологии:
Live Process
An address space with one or more threads executing within that address space, and the required system resources for those threads.
Здесь написано следующее: процесс есть адресное пространство с одним или более потоков выполняющимися в нём и необходимыми системными ресурсами для этих потоков.
Приписка «живой» нас не интересует, в стандарте используется термин Live Process для отличия его от Zombie Process. В этой статье мы будем говорить только о «живых».
Так же в стандарте написано, что многие системные ресурсы определённые стандартом являются общими для всех потоков в процессе. Эти ресурсы включают в себя ID процесса, ID родительского процесса, ID группы процессов, текущую директорию, файловые дескрипторы и пр.
Thread
A single flow of control within a process. Each thread has its own thread ID, scheduling priority and policy, errno value, floating point environment, thread-specific key/value bindings, and the required system resources to support a flow of control. Anything whose address may be determined by a thread, including but not limited to static variables, storage obtained via malloc(), directly addressable storage obtained through implementation-defined functions, and automatic variables, are accessible to all threads in the same process.
Что означает: поток есть одно «течение контроля» (нам, русскоязычным, привычнее термин «поток выполнения»). Каждый поток имеет собственный ID, приоритет планировщика и политики, значение errno и пр.. Всё, чей адрес может быть определён потоком, включая, но не ограничиваясь статическими переменными, динамически выделенная память и др. доступны всем потокам одного процесса.

Переведя это на русский язык окончательно, получим следующее: процесс — это инстанция программы, которая может содержать один или более потоков, поток — это блок кода, работающий параллельно с основным и с другими потоками.

Я считаю, что разобраться в этом было важно, так как всё (во всяком случае системное) программирование заключается в распределении ресурсов операционной системы и управлении ими, а то, что мы выше почерпнули из стандарта даёт следующие выводы:
  1. Мы можем пользоваться (читать, писать) ресурсами в своих потоках в пределах одного процесса без использования т.н. IPC (методы межпроцессного взаимодействия).
  2. Мы не можем обращаться к ресурсам других процессов без применения IPC. Даже из т.н. основного потока выполнения (main (), например), даже из под root'а. А POSIX хоть и предоставляет инструменты IPC, но это достаточно сложные механизмы.
Теперь рассмотрим технологию создания второго и последующих потоков. На всякий случай, напомню, первый у нас всегда есть — его создала ОС при запуске вашей программы (возможно, о том как это происходит я напишу). Этот поток я называю основным.
В самой по себе ANSI C нет механизмов работы с многопоточностью. Есть различные реализации работы с потоками. Например, специфичная GNU Portable Threads, Protothreads и пр. Первая преследовала цель максимальной портируемости, вторая — легковесности. Мы будем рассматривать самый распространённый (хотя и не самый оптимальный, почему я распишу в последующих статьях) стандарт, т.н. POSIX Threads (pthread). POSIX Threads API реализован в библиотеке libpthread.

Приведём пример простой многопоточной программы:

#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>

/*
 * Здесь мы проверяем, есть ли в системе POSIX Threads
 */
#ifndef _POSIX_THREADS
#error No POSIX Threads!
#endif

/*
 * Функция, код которой будет выполняться в новом потоке.
 * Обратите внимание на то, что мы можем передать сюда
 * указатель на область памяти. Это можно использовать
 * как механизм передачи некоторых начальных данных потоку.
 */
void *ThreadRoutine (void *_Arg)
{
  printf ("String argument passed to thread routine: %s\n", 
          (char *)_Arg);
  while (0)
  {
    sleep (1);
    printf ("Thread is doing infinite work...\n");
  }
  printf ("Thread is about to exit normally...\n");
  
  /*
   * Завершить выполнение потока можно двумя способами: 
   * return и pthread_exit () с возвращаемым нами значением 
   * в параметре. Они эквивалентны за исключением некоторой
   * (незначительной) разницы в быстродействии, что, в свою
   * очередь, связано с функционалом вызова обработчиков 
   * завершения потока. Но мы их здесь не будем рассматривать.
   */
  /* return (void *)(uintptr_t)0xC0FFEE; */
  pthread_exit ((void *)(uintptr_t)0xC0FFEE);
  /*
   * А что будет, если вызвать exit () из функции потока?
   * Приложение завершится, так как exit () это не возврат
   * из функции, а более сложный системный вызов, который
   * "корректно" закрывает приложение вместе со всеми 
   * потоками. Недопонимание этой разницы может возникнуть 
   * из-за того, что мы часто завершаем наше приложение 
   * простым return'ом в main'е. На самом деле, при 
   * "возврате" из main'а компилятор подставляет функционал 
   * "корректного" закрытия программы. Поэтому завершать 
   * работу потока нужно только через return/pthread_exit (). 
   * Вы можете заменить return/pthread_exit () на exit (0) 
   * и посмотреть что произойдёт - вы не увидите вывода 
   * "Thread return".
   */
  /* exit (0); */
}

int main (int _argc, char * _argv[])
{
  /* Идентификатор потока, на Linux'е это целое число, на
   * Mac OS это структура.
   */
  pthread_t thread_id;
  /* Возвращаемое значение функции потока */
  void *ThreadRet = NULL;
  /*
   * Функция pthread_create создаёт поток. Первый параметр - 
   * идентификатор потока (для дальнейших операций с ним). 
   * Второй - аттрибуты, третий - функция потока, четвёртый - 
   * аргумент (мы передадим туда название исполняемого файла).
   */
  pthread_create (&thread_id, NULL, ThreadRoutine, _argv[0]);
  /*
   * Функция pthread_join ожидает выхода из функции потока.
   * Так же здесь мы получаем возвращаемое значение из потока.
   * Перед тем как раскомментировать этот вызов, убедитесь, что
   * цикл while в функции потока выключен (например, поставьте 
   * while (0)). Иначе программа зависнет в ожидании завершения 
   * потока. Ведь pthread_join () ждёт корректного завершения
   * работы потока.
   */
   pthread_join (thread_id, &ThreadRet);
  /*
   * Перед тем как включить этот эксперимент, поставьте в функции
   * потока while (1), чтобы включить его бесконечную работу.
   * Функция pthread_cancel () аварийно завершает поток.
   * Дадим ему 5 секунд "пожить" и завершим.
   */
  /* sleep (5); */
  /* pthread_cancel (thread_id); */
  /* Выводим значение, возвращённое из функции потока */
  printf ("Thread return: %lX\n", (uintptr_t)ThreadRet);
  return EXIT_SUCCESS;
}


NB!
Обратите внимание на то, как при возврате из функции потока, я создаю указатель на некий адрес. Это своеобразный трюк. Чтобы не выделять память для возвращаемого значения, с последующим её освобождением, но вернуть простой int, как результат работы, я вернул указатель на адрес памяти, который придумал из головы. Вы можете возвращать указатели на любые адреса, и анализировать их по возврату из pthread_join (). Но стоит помнить, что обращаться по таким, вымышленным адресам нельзя.

Забрать пример:
https://gitlab.com/daftsoft/pthread1.git

Собирать программы с POSIX Threads нужно с указанием библиотеки pthread:
gcc -std=c89 -pedantic -Wall pthread1.c -lpthread -o pthread1

К слову, в Linux'е не всегда был реализован стандарт POSIX Threads. Как же были реализованы потоки в Linux'е до «принятия» POSIX Threads? В то время (c 2.0 до 2.4 включительно) в Linux'е была своя технология, называлась она LinuxThreads. В общих чертах, она заключалась в копировании кода процесса с предоставлением доступа к памяти родительского процесса. При этом, у потоков были разные идентификаторы процессов (PID) и взаимодействие между ними осуществлялось посредством одного из механизмов IPC — сигналов (USR1 и USR2), что исключало возможность использовать этих сигналов разработчиками. То есть, по современным меркам (или по меркам стандарта), это были не совсем потоки. Всё это несовершенство продолжалось до начала 2000-х годов, когда LinuxThreads была заменена библиотекой потоков POSIX (Native POSIX Thread Library, NPTL), реализующей стандарт POSIX Threads в Linux.
Представленного примера достаточно для того, чтобы начать писать свои первые многопоточные программы.
В следующей статье мы, на примере, рассмотрим как действительно работают «общие ресурсы» в потоках в пределах одного процесса, а так же иные аспекты управления потоками.

19/02/2020

Интересно о размерах (2): модели данных

В продолжение рассуждений-исследований о соотношениях разрядности машины и размерностей типов (Интересно о размерах (1): типы и разрядные сетки машин), рассмотрим, как это выглядит формально (научно), как эти соотношения формализованы в стандартах — внесём окончательную ясность в одном из основополагающих аспектах вычислительной техники.

Соотношение разрядности машины и разрядностей типов данных (на самом деле просто отношения разрядностей типов друг к другу) называется моделью данных. И на самом деле модель данных задаётся компилятором, а не разрядностью машины, ровно, как и не операционной системой. То есть, даже на одной ОС, используя разные компиляторы, можно получить разные соотношения (которые мы косвенно рассмотрели в предыдущей статье на примере программы). Моделей данных существует много, но обычно в распространённых связках разрядность машины/ОС/компилятор используется ограниченное количество.

Принятый (сокращённый) формат названия модели данных состоит из перечисления типов, которые относятся к максимальной разрядности и значения самой разрядности.

Типы это (в порядке возрастания): S  короткое целое число (short), I  целое число (int), L  длинное целое число (long), LL  двойное длинное целое число (long long), P  указатель (void *). Разрядность указывается в битах  32, 64, 128.

char (условно) всегда 8-битный (про историю этого замечательного типа данных я напишу отдельно) и даже не указывается.

Пример, макрос которого мы использовали в предыдущей статье LP64, означает что компилятором задана следующая модель данных — 64-битными являются типы начиная с L (long), а именно — long и pointer. Указание LL излишне, так как он стоит рядом с указателем, и в таком случае считается равным ему. Если бы он был больше чем указатель, в конце было бы дописано LL128 (LP64LL128). Реальный пример ILP32LL64 (int, long и pointer — 32-битные, long long — 64-битный), эта модель использовалась в Win32 и некоторых UNIX'ах в 90-ые года. Можно задаться вопросом — как машина, разрядностью менее разрядности максимального типа вообще может оперировать такими типами? Верно — на операции с этими типами уходит более одной инструкции (такта), что крайне неэффективно с точки зрения вычислительных мощностей. 

Все типы, определённые до типов, указанных в названии модели считаются 32-битными, за исключением типа short — пока он, начиная с PDP-11 является 16-битным всегда. Это подразумевается, хотя возможна и полная запись: I32LP64 (и даже S16L32P64— эта модель данных и принята в стандартах Open System (UNIX является ОС относящейся к этому стандарту). В конце 90-х годов эта модель была согласована как основная для UNIX-систем совместным решением компаний, в том числе DEC, IBM, Sun Microsystems, HP, Intel, NCR.

Есть и отличный от предыдущих пример. В системах UNICOS (UNIX-подобная ОС для линейки суперкомпьютеров Cray) все типы являются 64-битными, и модель данных на этих машинах используется SILP64. И здесь используется символ S, так как даже short здесь 64-битный.

Ну и чтобы наше описание было полным, укажем какая модель данных была в Win16 и Win64. В Win16 использовалась LP32, что означает — 32-битные long и ponter, а int был 16-битным. А в Win64 используется LLP64, то есть, все типы до long long оставлены 32-битными. Сделано это из-за проблем с совместимостью и переписыванием WinAPI при переходе на 64-разрядные системы.

Для большинства UNIX-разработчиков ещё знакомы 32-разрядные системы, там принята модель данных ILP32, что означает что все типы, кроме short, являются 32-битными. И, собственно, единственным отличием 32-разрядных машин (компиляторов) от 64-битных является то, что pointer и long стал 64-битным. Что я и рассмотрел и доказал как один из методов определения разрядности машины в предыдущей статье. Все остальные типы остаются таких же размерностей, что и на 32-битных системах. Сделано это для совместимости кода. Но здесь стоит быть осторожным. В некоторых 32-битных программах типы int, long и pointer используются как синонимы, полагается, что их разрядности равны. Но так как размеры long и указателя в модели данных LP64 стали 64-битными мы должны помнить, что это изменение может вызвать много проблем при переносе кода с 32-разрядной платформы на 64-битную.
Поиск оптимальной модели данных, при каждом переходе на более высокую разрядность системы, является основополагающим моментом в обеспечении качественного процесса разработки ПО. Можно сказать, что модель данных является инструментом разработчика. Так как влиять мы на неё (модель) не можем, наша задача хорошо в них разбираться.

На этом пока всё.

18/02/2020

Web (1): RayCasting vs GPU Color Picking (на примере Three.js)

В определённый момент, перед разработчиком графических приложений с использованием OpenGL/WebGL встаёт задача взаимодействия пользователя с объектами. Будь то элементы двухмерного интерфейса (частный случай трёхмерного пространства) или объекты трёхмерной сцены, существует два подхода к решению этой задачи. Первый заключается в поиске объекта, на который указывает мышь математическим (тригонометрическим) расчётом.  Второй — поиск объекта по цвету пикселя, на который пользователь указал мышью. Первый кажется логичным, понятным и красивым, второй — на первый взгляд кажется странным, но у обоих методов есть свои плюсы и минусы и свои области применения.

В этой статье мы рассмотрим два метода выбора объекта на сцене (OpenGL/WebGL на примере Three.js) — RayCasting и GPU Color Pick.

RayCasting — это технология определения пересечения луча с объектом. При определении выбранного объекта — объекта, на который навели мышь, например, выбирается первый объект, с которым произошло пересечение. На самом деле собираются все объекты, с которыми пересёкся луч, а уже мы используем только первый. RayCasting работает на стороне CPU, то есть, не задействует специфические вычислительные мощности GPU. 

В Three.js для этих целей имеется уже разработанный объект THREE.Raycaster. Пользоваться им достаточно просто, в примере, которым я завершу эту статью, этот метод будет представлен. Описывать его механику смысла не вижу — это обычная тригонометрия.

У RayCasting’а есть два минуса:

  1. Он работает медленнее, так как работает на CPU, и в случае, когда у нас много объектов, это может быть очень тяжёлой задачей для центрального процессора. А в современной графике с использованием актуальных методов создания карт (т.н. сцен) количество объектов может быть очень большим.
  2. Он не учитывает факторы текстуры — прозрачность, и просто «выбирает» объекты, которые встречаются лучу (в нашем случае — выбор объекта мышью, луч рассчитывается от камеры), то есть работает исключительно геометрия (математика).

Технология GPU Color Pick заключается в следующем. При создании сцены, все объекты дублируются в другую сцену, обычно называемую PickingScene и связываются некоторыми ID реальных объектов с цветами объектов в PickingScene. Для исключения излишней нагрузки на GPU, PickingScene, как правило, делается очень маленькой (выходной буфер рендеринга очень маленькой площади, это в некоторой степени сокращает время на рендеринг). В нашем случае, по наведению мыши, вызывается функция поиска объекта в PickingScene и затем выбирается ID объекта в реальной сцене. В силу того, что технология GPU Color Pick работает по цвету, она лишена второго минуса RayCasting’а — мы можем использовать альфа-канал в текстурах объектов. Это значит, что мы можем дать пользователю возможность «выбирать» объекты стоящие «за», сквозь «прозрачные» области стоящих «перед». В силу того, что технология GPU Color Pick работает на GPU, а он, в свою очередь очень «заточен» под такие задачи, эта технология работает гораздо быстрее и почти полностью разгружает CPU (на нём остаётся лишь задача соотнести цвета и ID объектов).

У GPU Color Pick (как ни странно) есть два минуса:

  1. Мы не можем получить координаты места «встречи» указателя (здесь это не луч и не вектор) и объекта — только сам объект, так как объект выбирается не геометрией, а цветом.
  2. Подгружается GPU. Это хорошо тем, что разгружается CPU, но может быть плохо, если вычислительные мощности GPU для нас критичны. Плюс, в момент процесса самого Picking’а, нам надо ждать на CPU пока GPU выполнит задачу поиска объекта по цвету.


Все эти технологии, конечно же применяются и на Bare WebGL и на чистом OpenGL.

Чтобы поиграться (работает без Web-сервера):

git clone https://gitlab.com/daftsoft/gpu-color-pick-vs-raycasting



12/12/2019

Системы сборки (1): GNU Make - Simple Makefile


Системы сборки (1): Make - Simple Makefile  простой Makefile для несложных программ
Когда проект растёт в размерах файлов, разработчики начинают делить его на несколько файлов, потом ещё и ещё делить. Компилировать один файл командой gcc (или иной другой) не сложно. Иногда, когда проект ещё не слишком большой, пишут скрипты типа build.sh, в которых записывают все действия для сборки. Но когда проект становится очень большим, и/или усугубляется использованием сторонних библиотек, возникает необходимость автоматизировать и «обрамить» процесс сборки. Для этих целей разработана система GNU Make.
С одной стороны, в этой статье описана лишь малая доля возможностей и функционала системы GNU Make, с другой — для начала работы, для большинства простых программ и для понимания того, что написано в других Makefile'ах вам этой информации хватит.
Настройка сборки происходит через задание переменных. Все переменные могут быть пустыми (как наши LDFLAGS и INCLUDE ниже). Более того, переменные могут быть неинициализированными вовсе — не объявлены в Makefile. В таком случае, они не будут приводить к ошибкам при попытке использовать их, а будут раскрываться как пустые. Обращение к переменной осуществляется через символ $ и скобки — $(CXXFLAGS). Начнём: Имена компиляторов (CC — для C, CXX — для C++):
CC=gcc
CXX=g++
Эти переменные в make-системе есть по-умолчанию — cc и g++ (на моей системе cc — это ссылка на gcc) для компиляторов C и C++ соответственно. То есть, можно опустить эти объявления, если вы согласны собирать проекты под родную систему и стандартными компиляторами. Возможность изменять эти переменные нужна для кросс-компиляции (сборки проектов под системы, отличные от той, на которой сборка производится) или для работы с разными версиями компиляторов на одной машине. Объявления этих переменных (как и некоторых других ниже) закомментированы. Я делаю это для того чтобы, описывая значения полей и структуру Makefile'а, заодно показать насколько маленьким может быть простой Makefile. Флаги компиляции. Сюда вы можете ставить оптимизацию (например, -O3), стандарт языка (-std=c99), параметры -Werror, -Wall, -pedantic и т.п.
CFLAGS=-Wall
CXXFLAGS=-Wall
Имя исполняемого файла, который мы хотим получить на выходе:
EXECUTABLE=simple
Эта команда собирает все файлы с расширением .cpp в переменную:
SOURCES=$(wildcard *.cpp)
Эта команда создаёт переменную с именами объектных файлов. Это достигается путём замены расширения .cpp на расширение .o:
OBJECTS=$(SOURCES:.cpp=.o)
Так как нам в проекте нужен только список объектных файлов (для чего будет нужен этот список и почему список имён файлов с исходниками не будет нужен — будет видно позже), две предыдущие команды можно объединить в одну:
OBJECTS=$(patsubst %.cpp,%.o, $(wildcard *.cpp))
Здесь описано как добавить к проекту исходные файлы на C:
OBJECTS+=$(patsubst %.c,%.o, $(wildcard *.c))
LDFLAGS — флаги для линковщика (пока оставляем пустыми):
LDFLAGS=
INCLUDE — пути к заголовочным файлам, включаемым в код нашего проекта (здесь оставляем пустыми, то есть будут использованы системные):
INCLUDE=
Далее идут правила. Вся система make построена на так называемых «правилах». Правило — это основной параметр, передаваемый команде make. В описании правила, за его именем указывают зависимости, необходимые для его выполнения. Зависимости — это файлы (имена файлов). После, на новой строке, идут команды, которые будут выполняться при вызове этого правила — строки с командами. Они должны начинаться с символа табуляции. Правило может не иметь команд, правило может не иметь зависимостей. Улитка перед командой является указанием не выводить команду (в противном случае make перед выполнением команды выведет её на консоль).
all — это общепринятое название правила, выполняющего сборку проекта. В нашем случае all завязан на значение имени исполняемого файла — EXECUTABLE, а оно в свою очередь является правилом, описанным ниже. Так же можно видеть, что само по себе правило all не имеет «тела» (команд для выполнения).
all: $(EXECUTABLE)
Правило с именем исполняемого файла, получаемого из значения переменной, выглядит следующим образом:
$(EXECUTABLE): $(OBJECTS)
        $(CXX) $(CXXFLAGS) -o $@ $^ $(INCLUDE) $(LDFLAGS)
Здесь написано следующее — для создания файла, записанного в значении переменной EXECUTABLE, нужны объектные файлы (записанные в созданной нами ранее переменной-списке OBJECTS). И описано само правило, являющееся, если присмотреться, командой линковки, в нашем случае раскрывающееся в следующую команду: g++ -Wall -o simple main.o module.o extern.o Что означает: слинковать перечисленные объектные файлы (main.o module.o extern.o) в исполняемый файл simple. Откуда берутся имена объектных файлов мы помним — выше описана команда создания списка путём замены расширения .c и .cpp на .o и записи их в переменную OBJECTS. Но вы можете спросить — а где команда (или правило) для построения компиляции) исходных файлов в объектные? А его нет. Дело в том, что в системе make есть набор стандартных неявно описанных правил. Все они описаны здесь: GNU Make Catalogue of Built-In Rules
Одно из стандартных правил make выглядит примерно так:
# .o: .c
#         $(CC) $(CPPFLAGS) $(CFLAGS) -c
Что означает скомпилировать (без линковки) все .c-файлы. Компиляция без линковки выдаёт объектный файл. Вот так получаются сами объектные файлы необходимые для линковки в исполняемый файл. Автоматизация make доходит до того, что можно дать команду:
make имя файла с исходным кодом даже без расширения и даже не имея Makefile! make main По этой команде make попытается скомпилировать main.c (в случае его отсутствия make попытается сделать то же с файлом main.cpp) в объектный файл main.o, затем слинковать его в исполняемый файл main. Неявное правило можно переписать. Например, правило, описанное ниже приведёт к ошибке сборки всего проекта. Оно гласит: для сборки объектного файла extern.o нужен файл с исходным кодом extern.c. Но само по себе не создаёт объектного файла нужного для сборки нашего проекта. Что приведёт к тому, что проект никогда не будет собран. Можете раскомментировать это правило сделать make clean all и посмотреть что будет.
#extern.o: extern.c
#        echo Rule to never make this project!
clean — правило для очистки. Сюда вписываем всё, что мы хотим делать при очистке проекта. В нашем случае мы удаляем объектные файлы, исполняемый файл и временные файлы. Это правило, в том виде, в котором оно описано, удалит только те объектные файлы, которые относятся к нашему проекту — то есть имеющие соответствующие файлы с исходным кодом. Вы можете проверить это, удалив какой-нибудь .с/.cpp-файл и выполнив команду make clean — вы увидите, что объектный файл соответствующий удалённому останется. Можно поставить rm *.o, но в приличных и крупных проектах принято удалять только своё. А вдруг вы (или пользователь вашего проекта) держите ещё какие-то файлы в каталоге с вашим Makefile (например, объектные файлы, которые вы получаете без исходного кода)?
clean:
        rm -rf $(OBJECTS) $(EXECUTABLE) *~
.PHONY — Это список имён правил, которые не являются файлами. Создаётся во избежание конфликтов между названиями этих правил и именами файлов. Если не указать clean и all в списке .PHONY, make запутается, потому что по умолчанию правила clean и all будут связаны с файлами с такими именами и make будет запускать их только тогда, когда файл не будет обновлен в отношении его зависимостей. Можно создать файл clean и попробовать сделать

make clean

с закомментированной строкой .PHONY, результат будет такой:

make: 'clean' is up to date.

То есть make увидит, что файл clean уже существует и не станет выполнять это правило вообще.
Список .PHONY может быть использован для форсированной компиляции каких либо файлов, вне зависимости от их состояния. Например форсированная перестройка модуля или всего проекта. Вообще считается хорошим тоном вписывать в список .PHONY все правила, не создающие файлов — clean, all, install, test, и пр.
.PHONY: clean all
Возможно вы обратили внимание, что выше мы использовали такие переменные как $@ и $^. Это название правила и список зависимостей соответственно. Ниже написано тестовое правило, которое выводит значение этих переменных. Вы можете поэкспериментировать — поменять его название и список зависимостей. И вопрос на внимательность — как сделать так, чтобы это правило выполнялось бесконечное количество раз?
test: Makefile main.cpp
        @echo "@ = " $@
        @echo "^ = " $^
        @touch $@

Для тех, кто впервые читает или встречается с GNU Make пару слов об этой системе. GNU Make — система сборки (компиляции) программного обеспечения. Работа с ней выглядит следующим образом — (вами или не вами) пишутся правила, затем выполняется команда формата

make -f Makefile -jJobs rule_1 ... rule_n 

где:
Makefile — имя Makefile'а из которого брать правила. Эту опцию можно опустить. По-умолчанию GNU Make будет искать Makefile'ы в текущем каталоге в следующем порядке: GNUmakefile, makefile и Makefile. Рекомендуемым авторами системы является имя — Makefile.
Jobs — количество потоков выполнения. Make умеет распараллеливать работу. Обычно эту цифру ставят равной количеству процессоров либо количеству процессоров умноженному на два. Если указать ключ -j без параметра, Make сам рассчитает оптимальное количество потоков для выполнения. Здесь стоит отметить два момента. Первое — этот ключ имеет смысл только для достаточно больших проектов. Второе — задавать слишком большое значение опасно — Make никак не проверяет производительность машины, на которой запущен и её способность вытянуть заданное количество «работ». В итоге можно перегрузить машину так, что вы даже нажав Ctrl-C будете ждать пол часа. Я попадал в такую ситуацию. Ещё бывает так что большие проекты не собираются в параллельном режиме — будет ошибка компиляции, которую можно обойти запустив make ещё раз, но без многопоточной сборки. Иногда можно «пройти» это место, остановить сборку и опять запустить в многопоточном режиме. Бывает такое, как я понимаю, из-за неправильно составленных Makefile'ов — получается так, что часть кода, требующая неких объектных файлов, уже собрана, а объектные файлы ещё нет. Своеобразный race condition получается.
rule_1 ... rule_n — список правил для выполнения. Да, правила можно передавать Make по несколько штук — они будут выполняться в очерёдности, в которой были заданы пользователем. Если этот параметр опустить — Make выполнит правило, описанное первым в Makefile.
Изначально GNU Make создавался как бы «в паре» с (или «под»GCC — отсюда такая простота и прозрачность в работе Make именно с процессом сборки программ при помощи этой коллекции компиляторов. Но несмотря на это, можно написать абсолютно любые правила в Makefile'е — настройка сети, запуск/остановка сервисов и пр.

На этом о make пока всё. К более сложным аспектам работы с этой системой мы ещё вернёмся позже.
В следующей статье мы рассмотрим как смешивать C и C++ код в одном проекте (в этом Makefile мы уже затронули это с точки зрения сборки), как происходит построение программы — как компилятор и линковщик создают исполняемый файл и при чём здесь ассемблер?

22/10/2019

Интересно о размерах (1): типы и разрядные сетки машин

Короткая заметка о том, как соотносятся размеры целочисленных типов с разрядностью машины (процессора, контроллера).

Лет 10-15 назад я почему-то думал, что размер int'a (sizeof (int)) равен разрядности машины. Не знаю откуда я это взял, может, я как-то по-своему интерпретировал слова Б.Кернигана и Д.Ритчи:

«int  целое число, обычно имеющее типовой размер для целых чисел в данной системе»

Но выглядела эта идея довольно красиво  представьте себе, что у вас int всегда имеет размер разрядности машины: 16 — для 286, 32 — для 386 и после, 64  для Itanium (или кто там был первым среди x86-64?). В последствии, на практике, я увидел, что это не так.
Недавно (неделю-две назад) я решил окончательно разобраться с этим вопросом. Опишу это здесь.

Мои изыскания привели к следующим результатам:
1. Размер int'а никак не связан с разрядностью машины. Стандарт языка C описывает только одно правило соотношения между базовыми типами:

sizeof (short) <= sizeof (int) <= sizeof (long)

по-русски гласящее, что тип short будет либо меньше, либо равен int, который, в свою очередь, либо меньше, либо равен long. На практике, мы видим, что чаще всего размер int'а равен 4 байтам. Но размеры этих типов не определяются стандартом.

2. Да, функцией sizeof () можно определить разрядность машины  только не через размер типа, а через размер указателя на тип (подойдёт любой тип). Именно размер указателя равен разрядности машины, так как разрядность  это не только количество бит, с которыми машина работает за один такт и/или размер инструкции, но и размер поддерживаемого объёма памяти, для адресации которого машина выделяет и оперирует типами соответствующего размера. То есть, чтобы узнать разрядность машины можно воспользоваться sizeof([любой тип]*), например sizeof(int*).

Так как функция sizeof () реализована так, что размерности типов (и прочих констант) подставляются в момент компиляции (compile-time), по сути дела информативность её достаточно мала в силу того, что разрядность процессора задаётся (и может быть ограничена) компилятором и/или операционной системой и результат выполнения её после компиляции будет одинаков при всех последующих запусках. Для определения разрядности машины есть макрос __LP64__ и __LP32__ соответственно (эти макросы означают модель памяти, о 64-ёх разрядной можно почитать здесь 64 bit data models). Макрос использованный в коде, на этапе компиляции развернётся и не будет занимать процессорного времени на этапе выполнения, при этом выполнит задачу sizeof (указатель на тип).

Завершим описанное демонстрацией простенькой программой:

#include "stdio.h"
int main ()
{
  printf ("Compiled on ");
#if defined(__LP64__)
  printf ("64-bit platform\n");
#endif
#if defined (__LP32__)
  printf ("32-bit platform\n");
#endif
  printf ("sizeof (char)\t= %d bytes\n", sizeof(char));
  printf ("sizeof (short)\t= %d bytes\n", sizeof(short));
  printf ("sizeof (int)\t= %d bytes\n", sizeof(int));
  printf ("sizeof (long)\t= %d bytes\n", sizeof(long));
  printf ("sizeof (float)\t= %d bytes\n", sizeof(float));
  printf ("sizeof (double)\t= %d bytes\n", sizeof(double));
  printf ("sizeof (int*)\t= %d bytes\n", sizeof(int*));
}

И проверяем:
gcc ./main.c && ./a.out 
Compiled on 64-bit platform
sizeof (char)   = 1 bytes
sizeof (short)  = 2 bytes
sizeof (int)    = 4 bytes
sizeof (long)   = 8 bytes
sizeof (float)  = 4 bytes
sizeof (double) = 8 bytes
sizeof (int*)   = 8 bytes
64-ёх битная платформа (определена макросом и вычислением sizeof(int*)), и правило

sizeof (short) <= sizeof (int) <= sizeof (long)

выполняется.
В этой заметке мы использовали байты в качестве единицы измерения. Далее планирую написать о том из чего байты состоят  о битах, и об их разновидностях. Позже разберёмся более детально в так называемых моделях памяти.

15/07/2019

STM32 (2): удобная среда разработки — SES + CubeMX: методика ускорения разработки (часть 1)


STM32 (2): удобная среда разработки 
— SES + CubeMX: методика ускорения разработки (часть 1)

В предыдущей статье мы рассмотрели и настроили среду разработки на основе
Segger Embedded Studio (SES).
В этой статье я опишу методику ускорения разработки ПО под STM32 путём подключения HAL (Hardware Abstraction Level, библиотека абстракции от аппаратуры) от ST.
В прошлый раз мы упоминали пакет CMSIS (Cortex-M Software Interface Standard) — это набор общих для всех Cortex-M контроллеров дефайнов (#define). Эта, условно говоря, библиотека разработана для стандартизации процесса разработки и повышения переносимости кода между всеми Cortex-M контроллерами. У неё есть очевидное положительное качество — её легковесность, так как дефайны, раскрываясь при компиляции, не занимают места. И, да — она облегчает работу на Cortex-M (в том числе и на STM32). Но если вы будете писать код с использованием лишь CMSIS, вам придётся заполнять все структуры (множество структур) самостоятельно. И, более того, о, ужас, писать непонятные вещи, подобные этому:
GPIOA->ODR &= ~GPIO_ODR_ODR0;

Компания STMicroelectronics пошла дальше и разработала продукт называемый STM32CubeMX (CubeMX). CubeMX — свободный, кроссплатформенный пакет для настройки HAL от ST в графической среде. Процесс работы с использованием CubeMX выглядит примерно так: выбираем тип контроллера, настраиваем его интерфейсы, выбираем IDE, под которую нужно создать проект, нажимаем «Сгенерировать проект», открываем проект в выбранной IDE, работаем над функционалом. К сожалению CubeMX не поддерживает SES. Как это компенсировать я и расскажу на простом примере.

Для начала работы, скачайте CubeMX:
https://www.st.com/en/development-tools/stm32cubemx.html
Нужно нажать Get Software и, к сожалению без регистрации/авторизации скачать не получится.

Устанавливаем, запускаем, создаём проект (New Project > ACCESS TO MCU SELECTOR):

В открывшемся окне (загружается из базы продуктов ST) выберите свой контроллер — либо в списке, либо введите часть его названия в поле поиска слева вверху. По идее на генерацию кода это не должно влиять, но так как CubeMX у ST выполняет частично и функции продажи, придётся выбрать даже тип корпуса. В прочем, если вам придётся реально отлаживать комплекс на уровне железа (осциллограф, анализатор, тестер) — будет ещё удобнее видеть где какие конкретные выводы находятся. Ведь CubeMX нарисует вам выбранный контроллер в выбранном корпусе с реальным расположением и названиями ног и выводов!


Вверху вы увидите три вкладки — Pinout & Configuration, Clock Configuration и Project Manager.
Начнём с самого основного — настройки частоты на которой будет работать наш контроллер. Перейдите на вкладку Clock Configuration. Вы увидите следующее (картина будет отличаться в зависимости от выбранного контроллера, я для примера рассматриваю один из самых простых — STM32L011):


Видите как много параметров в настройке частоты? А теперь введите в поле HCLK (MHz) (обычно чуть выше и правее середины схемы) значение. CubeMX даже подскажет вам максимальную частоту, которую вы можете себе «позволить» на выбранном контроллере.


После нажатия Enter, CubeMX сам рассчитает всю цепь, а в случае некорректного значения сообщит об ошибке.

Далее во вкладке Project Manager введите название проекта, пути и прочее. В поле Toolchain / IDE выберите MDK-ARM V5. Последнее важно для импортирования проекта в SES. Теперь нажмите «GENERATE CODE» (находится справа вверху). CubeMX создаст проект, скопирует HAL под нужный контроллер и, что самое главное — сгенерирует высокоуровневый код (с использованием HAL'а), который выполнит инициализацию всего того, что мы настроили в графической среде.

Как я уже писал — под SES CubeMX не умеет делать проекты. Сейчас мы будем это исправлять. Открываем SES и создаём проект, выбираем тот же контроллер, что и в CubeMX. Здесь не повторяюсь — этот процесс описан в предыдущей статье.

Открываем оба каталога — с проектом CubeMX и с проектом SES. Далее будем копировать файлы из каталога с проектом CubeMX в корень каталога проекта SES (себе). Я буду показывать процесс на примере проекта под STM32L0xx. В случае использования другого контроллера, имена каталогов изменятся соответственно.
1. Удаляем из проекта SES файл main.c (нажать на нём правой кнопкой и выбрать «X Delete»).
2. Копируем себе каталоги Src и Inc.
3. Перемещаем файл Src/system_stm32l0xx.c в каталог STM32L0xx/CMSIS/Device/Source (с замещением).
4. Копируем каталог STM32L0xx_HAL_Driver (у CubeMX находится в каталоге Drivers). Я обычно копирую его в каталог STM32L0xx, который SES создаёт в корне проекта.
5. Добавляем новые файлы исходного кода: жмём правой кнопкой мыши по Source Files, выбираем Add Existing File... и указываем все файлы из каталога Src.
6. Повторяем эту процедуру, указывая все файлы из каталога STM32L0xx/STM32L0xx_HAL_Driver/Src
7. Открываем свойства проекта (правая кнопка на проекте —> Options...), ищем User Include Directories и указываем:
.
./Inc
STM32L0xx/STM32L0xx_HAL_Driver/Inc



На этом самая сложная часть импорта ST HAL'а в проект завершилась. На данном этапе ваш проект должен компилироваться, собираться, загружаться и работать.
Для проверки правильно ли выставилась частота контроллера, добавим в наш main следующий функционал. Найдите вызов функции SystemClock_Config() и добавьте до и после неё соответствующий вывод:

printf ("Unset clock speed is: %dMHz ", HAL_RCC_GetSysClockFreq () / (1000 * 1000));
SystemClock_Config(); printf ("Clock speed is set to: %dMHz ", HAL_RCC_GetSysClockFreq () / (1000 * 1000));

У меня выводит следующее:
Unset clock speed is: 2MHz Clock speed is set to: 32MHz

Завершающее эту статью замечание. В коде, созданном в CubeMX вы увидите множество комментариев, в том числе и вида
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */

По задумке разработчиков из ST CubeMX должен по этим меткам определять, где пользовательский код и при последующих изменениях настройки контроллера сохранять эти изменения. Так как с SES CubeMX не работает, эти комментарии нам уже не понадобятся. Лично я их удаляю все и сразу. Вы можете абсолютно без риска поступить так же.
Разумеется выставить частоту контроллера не единственная задача на наших проектах, поэтому в следующей статье я покажу что ещё можно делать в CubeMX и как добавлять в свой проект дальнейшие изменения если вы удалили комментарии USER CODE BEGIN/END.