Внедрение зависимостей (dependency injection) в Swift 5.1

 
 
   

Внедрение зависимостей очень горячая тема в любой области разработки, где мы пишем что-то более сложное чем Hello, World. Однако несмотря на казалось бы изученный вдоль и поперек вопрос, вариантов его решения вы можете на просторах интернета найти великое множество. И в каждом месте оно подается как единственно правильное. И как же выбрать? Предлагаю в этой статье немножко рассмотреть подходы, их плюсы и минусы, немножко поиграться со Swift’ом вообще и попробовать его новые фичи в виде @PropertyWrapper’s.

Итак, постановка задачи у нас будет такая - у нас есть два класса BooksRenderer, который просто каким-то образом рисует книжки, и BooksProvider, который ему их поставляет. На Swift это будет выглядеть примерно так:

final class BooksRenderer {
    let provider: BooksProvider = ... /* за это троеточие и будет вестись основная борьба */

    func draw() {
        let books = provider.books
        /* тут каким-то образом рисуются книги из массива books */
    }
}

protocol BooksProvider {
    var books: [Book] { get }
}

Будем также считать что есть некая реализация протокола BooksProvider, например такая наивная

final class NaiveBooksProvider: BooksProvider {
    var books: [Book] {
        return [
            Book(title: "Dune", author: "Frank Herbert"),
            Book(title: "Lord of the Rings", author: "John R.R. Tolkien")
        ]
    }
}

Теперь наша задача каким-то образом доставить экземпляр класса NaiveBooksProvider в BooksRenderer. Самый наивный подход такой, создать экземлляр класса прямо на месте:

final class BooksRenderer {
    let provider: BooksProvider = NaiveBooksProvider()
}

Несмотря на то, что этот подход, каким бы наивным он не был, много где применяется, у него есть очевидные недостатки:

  1. Мы можем захотеть как-то менять конкретный класс реализации, а он тут “прибит гвоздями”
  2. Мы можем захотеть unit-протестировать класс BooksRenderer, и тогда вместо провайдера захотим вставить какой-нибудь мок и т.д.

Нам надо что-то лучше. И много где предлагают хорошо известный паттер ServiceLocator. Если его применить, то выглядеть это будет примерно так:

final class ServiceLocator {
    static let booksProvider: BooksProvider = NaiveBooksProvider()
}

final class BooksRenderer {
    let provider: BooksProvider = ServiceLocator.booksProvider
}

Уже лучше, ответственность за выбор конкретного класса мы достали из BooksRenderer и наделили этой почетной обязанностью класс ServiceLocator. И мы даже можем сделать разные ServiceLocator’ы для основного приложения и для тестов, которые будут создавать разные BooksProvider’ы, однако:

  1. Теперь 90% кода будет знать про класс ServiceLocator
  2. Класс ServiceLocator будет огромным (кто там что говорил про Massive View Controller?, у нас тут Massive Service Locator)

Прежде чем пойти дальше, давайте сделаем некоторое лирическое отступление, разберемся в терминологии зависимостей. Вообще внедряемых зависимостей может быть два типа: прости хоспади singleton (но это не то что вы подумали) и prototype. “singleton” зависимости - это такие зависимости, которые сколько бы вы не внедряли в рамках одного конкретного модуля, это всегда будет один экземпляр. “prototype” же - даст на каждую точку внедрения новый экземпляр.

Поэтому если говорить про наш пример с ServiceLocator’ом, то например booksProvider - это singleton зависимость, а bookUpdateOperation - prototype:

final class ServiceLocator {
    static let booksProvider: BooksProvider = NaiveBooksProvider()

    static func bookUpdateOperation() -> Operation & BookUpdate {
        return NaiveBookUpdateOperation(...)
    }
}

Теперь давайте сделаем еще одно лирическое отступление, подчерпнутое мной когда я еще занимался “кровавым” enterprise и работал с Srping Framework. Хороший DI контейнер это такой контейнер, который не видно. Тут можно еще пофилософствовать и вспомнить ТРИЗ с ее идеальным конечным результатом, который на наш DI’ный контекст перефразируется так: “хороший DI контейнер - это такой, которого нет, а зависимости внедряются”.

Таким образом, можно сделать такой DI на базе initializer injection (оно лучше property injection, потому что компилятор в этом случае не даст вам озорничать, а с property injection легко забыть что-нибудь присвоить и грохнуться в рантайме):

final class AppContainer {
    let booksProvider: BooksProvider

    init(with appDelegate: AppDelegate) {
        booksProvider = NaiveBooksProvider(...)
        appDelegate.booksRenderer = BooksRenderer(provider: booksProvider)
    }
}

@UIApplicationMain
final class AppDelegate: UIResponder {
    let container: AppContainer
    var booksRenderer: BooksRenderer!

    func applicationDidFinishLauncherWithOptions(...) {
        container = AppContainer(with: self)
    }
}

final class BooksRenderer {
    let provider: BooksProvider

    init(provider: BooksProvider) {
        self.provider = provider
	}
}

Причем такой подход будет гарантировать вам проверку компилятором. И при этом про AppContainer будет знать только AppDelegate. Да, корневые зависимости в самом AppDelegate’е будут force unwrapped (что исть не хорошо, но лучше я не придумал), но эта вольность доступна только тут.

prootype зависимости в таком подходе можно оформить либо в виде фабрики

final class SomeFactory {
    let superDep: SuperDep

    init(superDep: SuperDep) {
        self.superDep = superDep
    }

    func makeSomeDep(...) -> SomeDep {
        return SomeDep(superDep, ...)
    }
}

и потом внедрять это фабрику как singleton зависимость туда где нужно генерить prototype’ные, или в виде замыкания

typealias SomeFactory = (_ superDep: SuperDep, ...) -> SomeDep { SomeDep(superDep, ...) }

и также ее внедрять как singleton зависимость.

Но я обещал немножко Swift 5.1 и @PropertyWrapper, их легко сделать так (пусть будет наш пример с ServiceLocator’ом, хотя его можно легко модифицировать):

@propertyWrapper
public class Inject<Dep> {
    private let name: String?
    private var kept: Dep?

    public var wrappedValue: Dep {
        kept ?? {
            let dependency: Dep = ServiceLocator.resolve(for: name)
            kept = dependency
            return dependency
        }()
    }

    public convenience init() {
        self.init(nil)
    }
    
    public init(_ name: String?) {
        self.name = name
    }
}

final class ServiceLocator {
    static func register<T>(for name: String, resolver: @escaping () -> T) {
        // register
    }

    static func resolve<T>(for name: String?) -> T {
        // do some magic
    }
}

final class BooksRenderer {
    @Inject private var provider: BooksProvider
}

И вуаля! Однако несмотря на всю прелесть такой магии, есть проблема в месте где творится магия (“do some magic”). Если вы вдруг забыли сделать register, то упс, вы получаете рантайм крэш. И сделать это красиво с compile time check непонятно как, так как регистрация динамическая.

Предлагаю подискутировать, оформляйте issues тут

P.S.

Разрабатываем и отлаживаем С++ в Docker с помощью VSCode (2019 edition - clang8 lldb)

     
     

Какое-то время назад я написал статью Разрабатываем и отлаживаем С++ в Docker с помощью VSCode в котором для сборки использовался gcc и для отладки gdbserver. В этом обновленном посте хочу затронуть такой же вопрос, но уже на базе clang 8 и lldb-server.

Также в отличии от предыдущей статьи мы не будем каждый раз пересоздавать контейнер, а запустим его один раз и будет с ним работать через docker exec.

Итак, начнем, Dockerfile

FROM ubuntu:18.04

RUN apt-get update 
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential xz-utils curl cmake
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y lldb

RUN curl -SL http://releases.llvm.org/8.0.0/clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-18.04.tar.xz | tar -xJC . && \
    mv clang+llvm-8.0.0-x86_64-linux-gnu-ubuntu-18.04 clang_8.0.0 && \
    mv clang_8.0.0 /usr/local

ENV PATH="/usr/local/clang_8.0.0/bin:${PATH}"
ENV LD_LIBRARY_PATH="/usr/local/clang_8.0.0/lib:${LD_LIBRARY_PATH}"

WORKDIR /opt/build
VOLUME ["/opt"]

lldb (для lldb-server) используем поставляемый в пакетах (на момент написания статьи для 18.04 это был LLDB 6). Также из пакетов ставим cmake (вроде ничего специфичного пока не надо). clang ставим руками, но уже собранный.

Дальше нам нужен скрипт сборки контейнера (/scripts/docker_build.sh)

#!/bin/bash

cwd=$(pwd)

docker build \
    -t something/cmaker \
    .

И таска для vscode

{
    "label": "build docker container",
    "command": "${workspaceFolder}/scripts/docker_build.sh"
}

после этого нам понадобится запустить контейнер для последующего использования, для этого нам нужен скрипт (/scripts/docker_run.sh). Тут попутно запускается lldb-server, о нем позже:

#!/bin/bash

cwd=$(pwd)

docker stop сmaker
docker rm сmaker
docker run \
    -dt \
    --name сmaker \
    -p 7000:7000 \
    -p 7001:7001 \
    -p 7002:7002 \
    -p 7003:7003 \
    -v ${cwd}:/opt \
    --privileged \
    something/cmaker \
    "${@}"

и таска для vscode

{
    "label": "run container for future builds",
    "command": "${workspaceFolder}/scripts/docker_run.sh",
    "args": [
        "lldb-server", "platform", "--server", "--listen=0.0.0.0:7000", "-m", "7001", "-M", "7003"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

После этого можем приступать к сборке проекта. Структуру проекта сразу предусмотрел для многомодульного проекта

include
scripts
  docker_build.sh
  docker_run.sh
hword
  include
  CMakeLists.txt
  main.cpp
CMakeLists.txt
Dockerfile

Корневой CMakeLists.txt выглядит так

cmake_minimum_required(VERSION 3.0)
project(something)

set(CMAKE_BINARY_DIR ${CMAKE_SOURCE_DIR}/build)

set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR})
set(LIBRARY_OUTPUT_PATH ${CMAKE_BINARY_DIR})

set(PROJECT_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)

include_directories("${PROJECT_INCLUDE_DIR}")
include_directories("${PROJECT_SOURCE_DIR}")

add_subdirectory(hword)

CMakeLists.txt для hword выглядит так

cmake_minimum_required(VERSION 3.0)
project(hword)

set(PROJECT_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include)
set(HWORD_VERSION_MAJOR 1)
set(HWORD_VERSION_MINOR 0)

set(PROJECT_HWORD_SRCS
${PROJECT_SOURCE_DIR}/main.cpp
)

include_directories("${PROJECT_BINARY_DIR}")
add_executable(${PROJECT_NAME}_r ${PROJECT_HWORD_SRCS})
include_directories("${PROJECT_INCLUDE_DIR}")

Обратите внимание что к имени проекта добавлен постфикс _r

Для примера содержимое main.cpp

#include <unistd.h>
#include <limits.h>

#include <iostream>

using namespace std;

static const int HNAME_MAX = 512;
static const int LNAME_MAX = 512;

int main()
{
    char hostname[HNAME_MAX];
    char username[LNAME_MAX];
    gethostname(hostname, HNAME_MAX);
    getlogin_r(username, LNAME_MAX);

    cout << "hostname: " << hostname << ", username: " << username << endl;
    return 0;
}

Теперь соберем все это дело, запустим и отладим

Нам понадобится скрипт для docker exec

#!/bin/bash

cwd=$(pwd)

docker exec \
    -it \
    cmaker \
    "${@}"

Таска для cmake

{
    "label": "cmake initialize scripts (Makefile, Debug)",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [                
        "cmake", "-G", "Unix Makefiles", 
        "-DCMAKE_BUILD_TYPE=Debug", 
        "-DCMAKE_C_COMPILER=/usr/local/clang_8.0.0/bin/clang",
        "-DCMAKE_CXX_COMPILER=/usr/local/clang_8.0.0/bin/clang++",
        "/opt"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

Обратите внимание, что тут явным образом указаны пути до компиляторов, это очень важно.

Далее таска для сборки всего проекта и для сборки конкретного

{
    "label": "make full project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "make", "-j", "8"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}
{
    "label": "make (hword) project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "make", "-j", "8", "hword_r"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
}

И для простого запуска

{
    "label": "run (hword) project",
    "command": "${workspaceFolder}/scripts/docker_exec.sh",
    "args": [
        "/opt/build/hword_r"
    ]
}

С отладкой есть несколько проблем. Во-первых, кроме порта на котором слушает lldb-server надо пробросить еще несколько, на которых будет работать собственно соединение (для этого при запуске контейнера несколько опций -p). Во-вторых, lldb-server’у этот диапазон портов надо явно задать. И в третьих, надо правильно написать launch.json, выглядит он так:

{
    "name": "debug: hword",
    "type": "lldb",
    "request": "launch",
    "program": "/opt/build/hword_r",
    "initCommands": [
        "platform select remote-linux",
        "platform connect connect://127.0.0.1:7000"
    ],
    "cwd": "/opt",
    "sourceMap": { 
        "/opt" : "${workspaceFolder}" 
    }
}

Жмем F5 и выуля - мы в отладке. Надеюсь это поможет :)

Сравнение Swift vs Kotlin

 
   

Небольшая табличка сравнения кода на Swift и кода на Kotlin для справки. Этакий cheet sheet.

Переменные и константы

Swift

let a: Int = 10
var b: Int = 20

Kotlin

val a: Int = 10
var b: Int = 20

Optionals

Swift

let some: Something? = nil
var some: Something? = Something(text: "Hello")

// optional chaining
let greeting = some?.greet()

// elvis
let value = some?.value ?? 30

// optional unwraping
if let some = some {
    print(some) // some is not optional
}

// force unwraping
let forced = some!

Kotlin

val some: Something? = nil
var some: Something? = Something("Hello")

// optional chaining
val greeting = some?.greet()

// elvis
val value = some?.value ?: 30

// optional unwraping
some?.let { some -> // it if name is not provided
    print(some) // some is not optional
}
if (some != null) {
    print(some) // some is smart casted and not optional
}

// force unwraping
val forced = some!!

Форматирование кода

Скобки (круглые и фигурные)

Swift

  • Круглые - не обязательны
  • Фигурные - обязательны
let str = "Hello, Swift"

func increse(_ value: Int) -> Int {
    return value + 1
}

func statements() {
    var i = 0
    while i < 10 {
        i = increase(i)
        if str == "something" {
            print(str)
        }
    }
}

Kotlin

  • Круглые - обязательны
  • Фигурные - не обязательны
val str = "Hello, Kotlin"

fun increase(value: Int): Int = value + 1

fun statements() {
    var i = 0
    while (i < 10) i = increase(i)
    if (str == "something") print(str)
}

Управляющие конструкции

Pattern Matching

Swift

let number = 42
switch number {
    case 0...7, 8, 9: print("1 digit")
    case 10: print("2 digits")
    case 11...99: print("2 digits")
    case 100...999: print("3 digits")
    default: print("4 or more digits")
}

Kotlin

val number = 42
when (number) {
    in 0..7, 8, 9 -> println("1 digit")
    10 -> println("2 digits")
    in 11..99 -> println("2 digits")
    in 100..999 -> println("3 digits")
    else -> println("4 or more digits")
}

if, when = expressions, not statements (!)

Swift

  • Не применимо

Kotlin

val test = if (true) "Test" else "False"

val state = State.Off
fun decide() = when(state) {
    State.Off -> "Off"
    State.On -> "On"
}
val decision = decide()

Типы

Любой тип

Swift

let a: Any = "Hello"
let b: AnyObject = SomeClass()

Kotlin

val a: Any = "Hello"
// no AnyObject yet

Строковая интерполяция

Swift

let str = "Hello \(nickname)"
let str2 = "Hello \(user.nickname)"

Kotlin

val str = "Hello $nickname"
val str2 = "Hello ${user.nickname}"

Интервалы (Ranges)

Swift

let rng1 = 0...10
let rng2 = 0..<10

Kotlin

val rng1 = 0..10
// no rng2

Коллекции

Swift

let stringArray = [String]()
// no stringList
let stringFloatMap = [String: Float]()
let stringSet = Set<String>()

var countries: [String] = ["Switzerland", "France", "Germany"]
countries.append("Italy")
countries.remove("France")

var jobs: [String: String] = [
    "Roger": "CEO",
    "Martin": "CTO"
]
jobs["Adrian"] = "Writer"

Kotlin

  • Нельзя использовать [] для инициализации массива, используется arrayOf
  • в arrayOf добавлять и удалять элементы нельзя, можно только их менять
val stringArray = arrayOf<String>()
val stringList = listOf<String>()
val stringFloatMap = mapOf<String, Float>()
val stringSet = setOf<String>()

val countries = mutableListOf<String>("Switzerland", "France", "Germany")
countries.add("Italy")
countries.remove("France")

val jobs = mutableMapOf(
    "Roger" to "CEO",
    "Martin" to "CTO"
)
jobs["Adrian"] = "Writer"

Итерирование коллекций

Swift

for str in stringArray {}
for (index, str) in stringArray.enumerated() {}
// no for str in stringList
for (str, num) in stringFloatMap {}
for str in stringSet {}

Kotlin

for (str in stringArray) {}
for (str in stringList) {}
for ((str, num) in stringFloatMap) {}
for (str in stringSet) {}

Процедурное программирование

Процедуры и функции

Swift

func method1(input: String) -> Int {
    return input.count
}

// no method2

func method3<T>(input: T) -> String {
    return String(describing: input)
}

Kotlin

fun method1(input: String): Int {
    return input.length
}

fun method2(input: String) = input.length

fun <T>method3(input: T) = input.toString()

ООП

Интерфейсы

Swift

protocol Person {
    var name: String { get set }
    
    func greet() -> String
    
    func showMoreInformation()
}

extension Person {
	func greet() -> String {
	    return "Hello! I am \(self)"
	}
}

Kotlin

interface Person {
    
    var name: String
        get set

    
    fun greet() = "Hello! I am $this"

    
    fun showMoreInformation()
}

Конструкторы

Swift

class Manager: Person {
    var backingName: String
    var staff: [Person]
    var state: State
    let isActive: Bool
    init(backingName = "": String, staff: [Person] = [], state: State = .off) {
        self.backingName = backingName
        self.staff = staff
        self.state = state
        self.isActive = true
    }

Kotlin

class Manager(private var backingName: String = "",
              private var staff: MutableList<Person> = mutableListOf<Person>(),
              var state: State = State.Off) : Person {
    private val isActive: Boolean
    init {
        isActive = true
    }
}

Data Classes (PONSO/POSS vs POJO)

Swift

struct Employee {
    let backingName: String
    let age: Int
    init(backingName: String = "", age: Int = 30) {
        self.backingName = backingName
        self.age = age
    }
}

Kotlin

data class Employee(private var backingName: String = "",
                    var age: Int = 30) : Person {

Инстанцирование объектов

Swift

let person = Employee(
    name: "Olivia", 
    age: 45
)

Kotlin

val person1 = Employee("Olivia", 45) 

val person2 = Employee().apply { 
    name = "Thomas"
    age = 56
}

Расширения классов

Swift

extension Double {
    var fahrenheit: Double {
	    (self * 9 / 5) + 32
    }
    var celsius: Double {
       (self - 32) * 5 / 9
    }
}

let temperature = Double(32.0)
let fahrenheit = temperature.fahrenheit
let celsius = fahrenheit.celsius
print("\(temperature) degrees Celsius is \(fahrenheit) degrees Fahrenheit")

Kotlin

val Double.fahrenheit: Double get() = (this * 9 / 5) + 32
val Double.celsius: Double get() = (this - 32) * 5 / 9

val temperature: Double = 32.0
val fahrenheit = temperature.fahrenheit
val celsius = fahrenheit.celsius
println("$temperature degrees Celsius is $fahrenheit degrees Fahrenheit")

Простые объекты и Singleton’ы

Swift

enum Constants {
    static let PI = 3.14
    static let ANSWER = 42
    
    static func name() -> String {
        return "Math constants"
    }
}

Kotlin

object Constants {
    val PI = 3.14
    val ANSWER = 42

    fun name() = "Math contstants"
}

Объекты-компаньоны

Swift

  • Просто используйте static (class) члены

Kotlin

  • В Kotlin нет статических членов
companion object OptionalName {
    val MAXIMUM_EMPLOYEE_COUNT = 10

    fun managerFactory() = Manager("Maria Hill")
}

Перегрузка операторов

Swift

  • Можно делать свои операторы (например квадратный корень)
func + (lhs: Person, rhs: Person) -> Team {
    return Team(lhs, rhs)
}

let team = manager + person

Kotlin

operator fun plus(person: Person): Team {
    return Team(this, person)
}

val team = manager + person

Инфиксные методы

Swift

  • Не применимо

Kotlin

infix fun addPerson(person: Person) {
    if (staff.count() < MAXIMUM_EMPLOYEE_COUNT) {
        staff.add(person)
    }
    else {
        throw Exception("Cannot add more staff members")
    }
}

manager addPerson person

Перечисления

Swift

enum State {
    case on
    case off
    
    var desc: String { ... }
    func notify() { ... }
}

Kotlin

enum class State {
    On, Off
    
    var desc: String { ... }
    func notify() = ...
}

Резюмирующая табличка

  Kotlin 1.2 Objective-C 2.0 Swift 4
Наследование Одинарное, с интерфейсами и расширениями Одинарное, с протоколами Одинарное, с протоколами и расширениями
Точки с запятой ; Не обязательны Обязательны Не обязательны
Объявление класса class @interface & @implementation class
Интерфейсы реализует interface поддерживает @protocol поддерживает protocol
Подключение кода import (символы) #import (файлы) import (символы)
Расширение классов Расширения Категории Расширения
Динамическая типизация Any id Any
Суффикс приватных полей Не используется _ (подчеркивание) _ (подчеркивание)
Управление памятью Сборка мусора (GС) Ручное или автоматическое управление памятью (MRC vs ARC) Автоматическое управление памятью (ARC)
Дженерики да (type erasure) да (type erasure) да
Указатели на метод нет @selector #selector
Обратный вызов (callbacks) Лямбды Делегаты и блоки замыкания
Указатели нет да Через библиотечные классы
Корневые классы Object NSObject / NSProxy / … NSObject / NSProxy / …
Управление областью видимости public / internal / protected / private @public / @protected / @private (только поля) open / public / internal / fileprivate / private
Обработка ошибок try / catch / finally + Exception @try / @catch / @finally + NSException do / try / catch + Error
Пространства имен Пакеты Через префиксы классов Неявные, через модули
Грамматика kotlinlang.org   developer.apple.com

Разрабатываем и отлаживаем С++ в Docker с помощью VSCode

     
 

Продолжаем цикл статей “Как кодить на С++ под OSX для Ubuntu”. После сборки cross-toolchain’ов стояла задача научиться собирать, запускать и отлаживать С++ под OSX для Linux (Ubuntu в данном случае). Как оказалось cross-toolchain был не нужен :(. Но об этом позже.

Для начала нужна была IDE, в которой можно было бы комфортно редактировать код и отлаживаться. Претенденты были такие: Xcode, Eclipse (CDT), CLion, чуть позже к ним добавился VSCode и впоследствии выиграл соревнование.

Постановка задачи была такая: C++, CMake, Linux, при этом редактирование кода и отладка должна работать под OSX.

Xcode поэтому выпал из этой гонки сразу, он есть очень удобная IDE для C++, но в целом в основном для OSX. CMake наверное завести можно, но это будет большое количество сторонних средств.

CLion выпал из этой же гонки примерно по тем же причинам (хотя он вроде умеет в CMake). Плюс я не смог в нем нормально подружиться с Docker.

Остался Eclipse CDT, в нем получилось практически все что требовалось (с cross-toolchain), завелась даже отладка через cross-gdb, однако были странные махинации чтобы нормально работать с CMake и контейнером он управлял относительно сам (не совсем прозрачно), хотя надо отметить что поддержка Docker в Eclipse плюс-минус хорошая.

После этого я пошел посерфить интеренет в поисках каких-нибудь идей, как нормально подружить Eclipse и CMake, и тут я наткнулся на VSCode. Которые не позиционирует себя как IDE, но умеет в дебаг и всякие user-defined task’и. Несмотря на мое некоторое отношение к продуктам Microsoft, я решил дать этому редактору шанс и попробовал его… и надо признать, это очень хороший инструмент.

Теперь же собственно, как завести, чтобы все работало.

Для начала нам понадобится собственно VSCode, настроенный так, как вам наиболее удобно. Можно сразу поставить расширения для работы с C++ (C/C++, Native Debug), CMake (CMake, CMake Tools), Docker.

Далее нам потребуется такая структура проекта (в принципе вы можете выбрать такую структуру, какая вам больше нравится, это скажем так рабочий пример):

.vscode
  tasks.json
  launch.json
build
CMakeLists.txt
dbuild.sh
ddebugger.sh
dmaker.sh
Dockerfile
main.cpp

Начнем с С++, пример простой, однако чтобы понять что мы действительно работаем для Linux я добавил несколько linux’овых вещей.

#include <unistd.h>
#include <limits.h>

#include <iostream>

using namespace std;

int main() {
	char hostname[HOST_NAME_MAX];
	char username[LOGIN_NAME_MAX];
	gethostname(hostname, HOST_NAME_MAX);
	getlogin_r(username, LOGIN_NAME_MAX);

	cout << "hostname: " << hostname << ", login: " << username << endl; // prints Hello World!
	return 0;
}

Этот код мы будем собирать, запускать и отлаживать.

Далее CMakeLists.txt, который тоже простой и относительно шаблонный:

cmake_minimum_required(VERSION 3.5)

project(HWord)

set(HWORD_VERSION_MAJOR 1)
set(HWORD_VERSION_MINOR 0)

set(SOURCE ${PROJECT_SOURCE_DIR}/main.cpp)

add_executable(${PROJECT_NAME} ${SOURCE})

Собираем

Как я уже выше писал сначала я пытался собрать все это безобразие под OSX с помощью cross-toolchain. Это плюс-минус работало, но часто приходилось пересобирать toolchain с еще каким-нибудь волшебным флагом, а на моем машине это делается примерно 3 часа. В одну из таких пересборок мне пришла мысль, а почему бы не собирать docker’ом, т.е. gcc который в контейнере и в требуемой системе (linux).

Таким образом собираем такой контейнер:

Dockerfile

FROM ubuntu:16.04

RUN apt-get update 
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc g++ gdb cmake

WORKDIR /opt/build
VOLUME ["/opt"]

Для упрощения работы с командной строкой docker’а заведем пару скриптов:

Скрипт сборки образа:

dbuild.sh

#!/bin/bash

cwd=$(pwd)

docker build \
    -t mrdekk/maker \
    .

Ничего сверхординарного, но возможно вам лучше поставить свой тег, вместо mrdekk/maker сделать %username%/maker

Скрипт запуска сборок:

dmaker.sh

#!/bin/bash

cwd=$(pwd)

docker stop maker
docker rm maker
docker run \
	-it \
	--name maker \
	-p 6666:6666 \
	-v ${cwd}:/opt \
	--privileged \
	mrdekk/maker \
    "${@}" 

Скрипт запуска контейнера для отладки:

ddebugger.sh

#!/bin/bash

cwd=$(pwd)

docker stop maker
docker rm maker
docker run \
	-dt \
	--name maker \
	-p 6666:6666 \
	-v ${cwd}:/opt \
	--privileged \
	mrdekk/maker \
    "${@}" 

Скрипты запуска сборки и дебаггера в общем отличаются только ключами -it и -dt.

Теперь сделаем несколько задач (tasks) для VSCode

Задача сборки контейнера:

Ничего особенного, вызываем скрипт dbuild.sh

{
    "label": "Docker: Build Containers",
    "command": "${workspaceFolder}/dbuild.sh",
},

И заодно выполняем ее, после этого можете сделать docker images и проверить, что образ mrdekk/maker появился

Задача для CMake:

Теперь с помощью CMake сгенерим Makefile’ы для сборки проекта. Здесь мы воспользуемся скриптом dmaker.sh, но будем вызывать его с набором ключей:

{
    "label": "CMake: Initialize",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "cmake", "-G", "'Unix Makefiles'", "-DCMAKE_BUILD_TYPE=Debug", "/opt"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    }
},

/opt это путь до Volume’а в контейнере в который мы будем проецировать текущую папку с исходниками проекта - это в общем часть магии.

После этого в подпапке build вы должны увидеть сгенерированные cmake’ом файлы.

Задача для сборки бинарника:

Здесь снова используем скрипт dmaker.sh но с другими ключами

{
    "label": "Make: Build Project",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "make", "-j", "8"
    ],
    "options": {
        "cwd": "${workspaceFolder}"
    },
    "group": {
        "kind": "build",
        "isDefault": true
    }
},

После того, как собереться можете попробовать сделать так ./build/HWord на OSX, и вы должны получить примерно такое:

exec format error: ./build/HWord

Это правильно, наш бинарник собран для linux и на OSX запускаться не должен. Если же вы увидите вывод программы, это значит что что-то пошло не так (вы собрали OSX’овым компилятором).

Запускаем без отладки

Теперь у нас есть бинарник, надо проверить что он запускается. Для этого снова воспользуемся скриптом dmaker.sh и такой задачей:

{
    "label": "Docker: Run Containers",
    "command": "${workspaceFolder}/dmaker.sh",
    "args": [
        "/opt/build/HWord"
    ]
},

После этого вы должны увидеть в docker logs maker и терминале VSCode примерно такое

hostname: 03a3a4dee13f, login:

Это хорошо, наш бинарник на linux работает хорошо.

Отлаживаемся

С помощью VSCode поставьте breakpoint куда-нибудь в коде. Нам будет необходим запущенный в фоне контейнер, внутри которого мы через docker exec будем запускать gdb. Увы, завести VSCode так, чтобы он смог нормально в интерактивном режиме (dmaker.sh и -it) запускать debugger у меня не получилось (видимо механизм запуска дебаггера в VSCode как-то так хитро устроен), поэтому я воспользовался pipeTransport и все заработало.

Таким образом задача для предварительного запуска контейнера:

{
    "label": "Docker: Run Debugging Container",
    "command": "${workspaceFolder}/ddebugger.sh",
    "args": [
        "bash"
    ]
}

а launch.json выглядит так

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Container Debug",
            "type": "cppdbg",
            "request": "launch",
            "program": "/opt/build/HWord",
            "cwd": "/opt",
            "linux": {
                "MIMode": "gdb",
                "setupCommands": [
                    {
                        "description": "Enable pretty-printing for gdb",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ]
            },
            "osx": {
                "MIMode": "gdb",
                "setupCommands": [
                    {
                        "description": "Enable pretty-printing for gdb",
                        "text": "-enable-pretty-printing",
                        "ignoreFailures": true
                    }
                ]
            },
            // "preLaunchTask": "Docker: Run Debugging Container",
            "pipeTransport": {
                "pipeProgram": "docker",
                "pipeCwd": "${workspaceRoot}",
                "pipeArgs": [
                    "exec", "-i", "maker", "sh", "-c"
                ],
                "quoteArgs": false,
                "debuggerPath": "/usr/bin/gdb"
            },
            "sourceFileMap": {
                "/opt":"${workspaceFolder}"
            },
        }
    ]
}

Внимательно ко всем параметрам, они все имеют значение.

После этого жмем F5 (или в меню выбираем Start Debugging) и… отлаживаемся. Дебаггер должен остановить выполнение в установленном вами breakpoint’е.

Примеры кода и исходники к статье лежат тут mrdekk/viscose-cpp-docker.

P.S. Да, я тоже ненавижу автокомплит, когда он что-то исправляет. В названии репозитория должно было быть vscode-cpp-docker, а получилось viscose-cpp-docker. Но, ладно! Так тому и быть.

Собираем toolchain для С++ разработки под Linux (Ubuntu) на OSX

 
     
 

Crosstool-NG

Для начала нам необходимо crosstool-ng для сборки toolchain’а для компиляции под Linux на базе OSX.

Case Sensitive Volume

Для установки crosstool-ng и для сборки правильного toolchain’а необходим Case-Sensitive Volume. Для этого воспользуемся замечательным скриптом. Опционально надо поправить в нем ${HOME} на то, куда вы реально хотите сохранять образ - у меня например второй большой диск (HDD против SSD) для всякого такого, поэтому пришлось поправить это дело.

Заодно сделаем его, воспользовавшись скриптом

./casesafe.sh create
./casesafe.sh mount

После того как станет ненужным, не забудте сделать

./casesafe.sh unmount

Установка требуемых зависимостей через HomeBrew

brew install autoconf
brew install binutils
brew install gawk
brew install gmp
brew install gnu-sed
brew install help2man
brew install mpfr
brew install openssl
brew install pcre
brew install readline
brew install wget
brew install xz

Иногда brew может сказать что зависимость уже стоит, но устарела, тогда опционально можно сделать brew update …

Еще опционально просят поставить dupes/grep ввиду того, что libc результирующего toolchain’а будет неправильно сконфигурирована, ввиду отличия макового BSD grep от GNU grep. –with-default-names необходимо чтобы системный grep заменился новым, так как без этого параметры brew’шный grep поставиться как ggrep.

brew install grep --with-default-names

Установка собственно crosstool-ng

Я решил пойти по Hacker’s way и сделать все через исходники, а не через release tarballs. Поэтому первое что делаем - клонируем репу, нужен гит:

git clone https://github.com/crosstool-ng/crosstool-ng

Далее нам требуется выбрать конкретный релиз, релизы обозначены тегами, поэтому выгребем теги и зачекаутим нужный:

git fetch --all --tags --prune
git checkout tags/crosstool-ng-1.23.0 -b r1.23.0

Далее начинаем работу по сборке. Так как мы решили пойти Hacker’s Way, то будем запускать это дело из исходников (поэтому ./configure –enable-local).

./bootstrap
./configure --enable-local
make

Для проверки успешности сборки в текущей директории надо сделать

./ct-ng help

Должна вывестись инструкция.

Конфигурируем

./ct-ng menuconfig

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

А вообще можно сделать так

./ct-ng list-samples

И выбрать что-то из уже готовых настроек. И затем посмотреть конфигурацию так

./ct-ng show-x86_64-ubuntu16.04-linux-gnu

Выбираем preset

./ct-ng x86_64-ubuntu16.04-linux-gnu

И можно оттюнить с помощью menuconfig

Проблемы

Может случится так, что вы получите такую ошибку

/Volumes/OSXElCapitan/Users/mrdekk/casesafe/.build/src/gdb-7.12.1/gdb/doublest.c:258:19: error: use of undeclared identifier 'min'; did you mean 'fmin'?
[ERROR]    /Volumes/OSXElCapitan/Users/mrdekk/casesafe/.build/src/gdb-7.12.1/gdb/doublest.c:568:19: error: use of undeclared identifier 'min'; did you mean 'fmin'?
[ERROR]    /Volumes/OSXElCapitan/Users/mrdekk/casesafe/.build/src/gdb-7.12.1/gdb/doublest.c:912:25: error: use of undeclared identifier 'min'; did you mean 'fmin'?
[ERROR]    make[3]: *** [doublest.o] Error 1
[ERROR]    make[3]: *** Waiting for unfinished jobs....
[ERROR]    make[2]: *** [all-gdb] Error 2
[ERROR]    make[1]: *** [all] Error 2

Надо попробовать ветку master в crosstool-ng. Однако bash на OSX слишком старый для bootstrap, поэтому придется поставить bash из brew и поправить shell в bootstrap.

После однако начинаются проблемы с sha512sum, такие

/Volumes/OSXElCapitan/Users/mrdekk/Documents/Utils/crosstool-ng/scripts/functions: line 786: sha512sum: command not found

Для этого делаем так

brew install coreutils
ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum

Еще там не собирается glibc с binutils-2.29.1, есть патч, который позволяет так собираться

char *loc1 __attribute__ ((nocommon));
char *loc2 __attribute__ ((nocommon));
compat_symbol (libc, loc1, loc1, GLIBC_2_0);
compat_symbol (libc, loc2, loc2, GLIBC_2_0);

/* Although we do not support the use we define this variable as well.  */
char *locs __attribute__ ((nocommon));
compat_symbol (libc, locs, locs, GLIBC_2_0);

Еще появилась такая проблема

[ERROR]      ../sysdeps/ieee754/dbl-64/e_pow.c:469:13: error: '<<' in boolean context, did you mean '<' ? [-Werror=int-in-bool-context]
[ERROR]      ../sysdeps/ieee754/dbl-64/e_pow.c:471:17: error: '<<' in boolean context, did you mean '<' ? [-Werror=int-in-bool-context]
[ERROR]      ../sysdeps/ieee754/dbl-64/e_pow.c:477:9: error: '<<' in boolean context, did you mean '<' ? [-Werror=int-in-bool-context]
[ERROR]      ../sysdeps/ieee754/dbl-64/e_pow.c:479:13: error: '<<' in boolean context, did you mean '<' ? [-Werror=int-in-bool-context]

Для ее решения есть такой патч

diff --git a/sysdeps/ieee754/dbl-64/e_pow.c b/sysdeps/ieee754/dbl-64/e_pow.c
index 663fa39..bd758b5 100644
--- a/sysdeps/ieee754/dbl-64/e_pow.c
+++ b/sysdeps/ieee754/dbl-64/e_pow.c
@@ -466,15 +466,15 @@  checkint (double x)
     return (n & 1) ? -1 : 1;	/* odd or even */
   if (k > 20)
     {
-      if (n << (k - 20))
+      if (n << (k - 20) != 0)
 	return 0;		/* if not integer */
-      return (n << (k - 21)) ? -1 : 1;
+      return (n << (k - 21) != 0) ? -1 : 1;
     }
   if (n)
     return 0;			/*if  not integer */
   if (k == 20)
     return (m & 1) ? -1 : 1;
-  if (m << (k + 12))
+  if (m << (k + 12) != 0)
     return 0;
-  return (m << (k + 11)) ? -1 : 1;
+  return (m << (k + 11) != 0) ? -1 : 1;
 }

Далее опять проблемы

[ALL  ]      rpc_parse.c: In function 'get_prog_declaration':
[ERROR]      rpc_parse.c:543:23: error: '%d' directive writing between 1 and 10 bytes into a region of size 7 [-Werror=format-overflow=]
[ALL  ]           sprintf (name, "%s%d", ARGNAME, num); /* default name of argument */
[ALL  ]                             ^~
[ALL  ]      rpc_parse.c:543:20: note: directive argument in the range [1, 2147483647]
[ALL  ]           sprintf (name, "%s%d", ARGNAME, num); /* default name of argument */
[ALL  ]                          ^~~~~~
[ALL  ]      rpc_parse.c:543:5: note: 'sprintf' output between 5 and 14 bytes into a destination of size 10
[ALL  ]           sprintf (name, "%s%d", ARGNAME, num); /* default name of argument */
[ALL  ]           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
[ALL  ]      cc1: all warnings being treated as errors
[ERROR]      make[3]: *** [/Volumes/OSXElCapitan/Users/mrdekk/casesafe/.build/x86_64-ubuntu16.04-linux-gnu/build/build-libc-final/multilib/sunrpc/rpc_parse.o] Error 1
[ERROR]      make[3]: *** Waiting for unfinished jobs....
[ERROR]      make[2]: *** [sunrpc/others] Error 2
[ERROR]      make[1]: *** [all] Error 2

Решение этой проблемы найти было сложнее, но кажется есть вот

+--- a/sunrpc/rpc_parse.c
++++ b/sunrpc/rpc_parse.c
+@@ -521,7 +521,7 @@ static void
+ get_prog_declaration (declaration * dec, defkind dkind, int num /* arg number */ )
+ {
+   token tok;
+-  char name[10];		/* argument name */
++  char name[MAXLINESIZE];		/* argument name */
+ 
+   if (dkind == DEF_PROGRAM)
+     {

Дальше опять проблемы

nss_nisplus/nisplus-alias.c:300:12: error: argument 1 null where non-null expected [-Werror=nonnull]
[ERROR]      nss_nisplus/nisplus-alias.c:303:39: error: '%s' directive argument is null [-Werror=format-truncation=]
[ERROR]      make[3]: *** [/Volumes/OSXElCapitan/Users/mrdekk/casesafe/.build/x86_64-ubuntu16.04-linux-gnu/build/build-libc-final/multilib/nis/nisplus-alias.os] Error 1
[ERROR]      make[3]: *** Waiting for unfinished jobs....
[ERROR]      make[2]: *** [nis/others] Error 2
[ERROR]      make[1]: *** [all] Error 2

Лечаться так

diff --git a/nis/nss_nisplus/nisplus-alias.c b/nis/nss_nisplus/nisplus-alias.c
index 7f698b4e6d..509ace1f83 100644
--- a/nis/nss_nisplus/nisplus-alias.c
+++ b/nis/nss_nisplus/nisplus-alias.c
@@ -297,10 +297,10 @@  _nss_nisplus_getaliasbyname_r (const char *name, struct aliasent *alias,
       return NSS_STATUS_UNAVAIL;
     }
 
-  char buf[strlen (name) + 9 + tablename_len];
+  char buf[tablename_len + 9];
   int olderr = errno;
 
-  snprintf (buf, sizeof (buf), "[name=%s],%s", name, tablename_val);
+  snprintf (buf, sizeof (buf), "[name=],%s", tablename_val);
 
   nis_result *result = nis_list (buf, FOLLOW_PATH | FOLLOW_LINKS, NULL, NULL);

После этого toolchain собрался. После этого я попробовал собрать простой Hello, World с ним и запустить. На macOS не запустилось (zsh: exec format error: ./CrossWorld) - это хорошо. Собираем простейший контейнер на ubuntu 16.04 и запускаем - работает. Отлично!