[БЕЗ_ЗВУКА] В этом видео мы рассмотрим возможность интеграции кода на C с кодом с Go, то есть вызовов C кода из Go. Этот механизм называется C Go, и далее мы рассмотрим, как это происходит технически, и все те подводные камни, которые вам встретятся. Итак, попробуем вызвать простую функцию из Go. Реализуется это при помощи псевдо-пакета C, import C. Псевдо-пакет, что это значит? Это значит, что это не какой-то исходник на Go, который вы можете прочитать. Нет, он реализуется компилятором. Для того чтобы им воспользоваться, нужно давить на import C, и после этого то, что идет в комментарии над ним, это будет являться уже кодом на C, и компилятор его будет воспринимать соответствующим образом. Это сишные комментарии, это сишная функция. У меня есть функция multiply, которая принимает два значения и возвращает их произведение. Теперь как я должен это вызывать? У меня есть две переменные a и b. И чтобы вызвать вообще любой сишный код, мне нужно добавить префикс C, то есть обращение к этому псевдо-пакету. Я вызываю, C плохо, назовем его лучше result, res так будет лучше. Я вызываю переменную res, я вызываю функцию multiply, то есть я добавляю вызов обращение к пакету и вызываю его функцию. А теперь первый подводный камень. Вы не можете передать гошные переменные в функцию на C, она про них ничего не знает. Они другие, они из другой вселенной, из другого рантайма. Поэтому вам нужно их конвертировать в сишные переменные. Для простых переменных это сделать просто. int можно конвертировать. Я обращаюсь к псевдо-пакету C и говорю в int и делаю int из этой переменной. Это будут уже сишные переменные, которые идут сюда, как раз в int a. Функция multiply тоже вернет мне сишную переменную res. Если я хочу ее привести обратно к гошной, мне нужно проделать обратное преобразование, привести ее к гошной. Давайте теперь это запустим. Значит, запустили, ждём какое-то время, все, мне вывелось. multiply, 2 на 3, получится 6, Вот тип, к которому оно привелось, main_Ctype_int, это тот тип, который нам вернула сишка. Получилось с шести. Теперь я покажу следующий подводный камень. Это должно быть довольно быстро. Запускаем, и вот он у меня пошёл, я не успел, ладно. Сишный код собирается при помощи GCC компилятора. Для того чтобы собирать код при помощи C Go, вам нужен дополнительный GCC поставить. Visual Studio компилятор не поддерживается. Другие компиляторы тоже не поддерживаются. Может быть, в будущем что-то изменится, но пока так. Раз вы устанавливаете C, то вы теряете возможность кросс-компиляции. Вам нужно будет тогда использовать GCC для целевой платформы, если вы решите собирать отличный от того, что он стал в базовом GCC. Например, я могу под виндой собрать бинарник под Linux и залить его, всё будет хорошо. Но в C Go мне уже нужно будет использовать линуксовый GCC для сбора. Это раз. Два — если вы будете использовать какие-то внешние библиотеки, которые подключаются вам каким-либо модулем, то есть DLL, или сошкой, то вы теряете всю прелесть статической компиляции, вы возвращаете ситуацию, возможного dependency hell, когда вам нужно будет эти библиотеки доставлять, и они будут конфликтовать с чем-то другим. Это первые минусы. Ещё код через GCC будет пересобираться каждый раз, потому что он не знает, что изменилось. Это будет ещё замедлять кросс-компиляцию. Ладно. Рассмотрим теперь, каким образом можно вызвать код из C, вызвать код на Go, то есть обратная ситуация. Мы вызвали сначала, у нас запускается гошная программа, мы вызвали сишный код, из сишного кода мы теперь хотим вызвать гошный код. Это тоже можно реализовать. Посмотрим следующую функцию. multiply, которая будет уже печатать результат при помощи гошной функции printResultGolang. Для того чтобы вызвать нам гошную функцию, нам ее нужно объявить как экспортируемой, чтобы компилятор там все нужные биндинги подготовил. Поскольку компилятор готовит биндинги и прочее в заголовочном файле, то, если я буду теперь, и там же объявлю реализацию, то компилятор может заругаться, что у вас двойное объявление. У вас есть объявление и там, и тут. Поэтому часто делается так, что тот код, который вы объявляете в гошном файле, то есть перед псевдо-пакетом C, там вы объявляете только заголовки, только объявления этих функций. Сами же эти функции будут реализованы уже в каком-то внешнем файле. Например, у меня есть файл на C. В C Go он бы объявил все в C Go, и у вас как раз был бы дубль. Но поскольку я объявил там только заголовок, то теперь у меня есть возможность в C объявить уже реализацию этой функции. Внешняя функция моя printResultGolang будет объявляться как extern void. Теперь это нужно собрать. [БЕЗ_ЗВУКА] Я не могу воспользоваться своим любимым go run, придётся собирать это полностью. Теперь запускаем, result-var internals Ctype int. Обратите внимание, что тут моя функция printResultGolang принимает сишный аргумент. Всё хорошо, и в main у меня тоже она объявлена, как принятие сишного аргумента. Однако в этом случае, если вы будете так делать, то вы будете несколько раз переключаться между рантаймами. Когда вы вызываете функцию main, у вас гошный runtime, потом вы вызываете функцию multiply, вы возвращаетесь опять в сишку, точнее, переходите в сишку. Когда вы вызываете printResult, вы опять вызываете гошку, опять переключаете runtime, после этого он всё-таки функцию отработал и опять вернется в сишный runtime, потому что multiply нужно доработать. Здесь что-то есть. return будет делать сишная функция. Поэтому вы вернетесь опять-таки ещё раз, и после уже, как multiply отработает, вы только тогда вернетесь в гошный runtime. Почему я говорю про переключение рантаймов как про что-то плохое? Это лучше показать на тесте. У меня вот есть команда. Тест, который будет вызывать нам просто пустую функцию из C, просто пустую сишную функцию и пустую гошную функцию, которая ничего не будет делать. Фактически мы меряем как раз переключение рантайма. Давайте посмотрим. Запускаем, C Go. Обратите внимание, в C Go одна операция занимает 119 наносекунд, при том что В обычной Go эта же операция, вызов пустой функции, которая ничего не делает, занимает три наносекунды. Почему так происходит? Дело в том, что вот у вас есть вселенная Go, где есть garbage collector, треды, горутины, асинхронщина, перекладывание горутин из одного системного треда в другой, а в C этого ничего нет, он про это ничего не знает. Поэтому когда вы вызываете код через CGO, то что происходит? Происходит лог треда на этот вызов. То есть вы лочите полностью системный тред, для того чтобы вызвать в нем эксклюзивно ваш C-код. В случае если бы это был чистый Go-код, то какая-то там была бы блокирующая операция, там могли бы выполняться какие-то другие горутины. В случае с Go такого не будет. Вы полностью залочите тред эксклюзивно на себя, и как раз все эти переключения рантаймов, туда-сюда, они как раз занимают много времени. То есть 119 наносекунд — это, в общем-то, очень мало, однако если у вас в процессе вашего запроса тысячи таких операций, то это уже может стать заметно, потому что оттуда же нужно другие горутины убрать. нужно в планировщике пометить, что туда не нужно пускать, он залочен, все такое. Вот. Это [НЕРАЗБОЧИВО]. Плюс еще раз скажу, что у вас во вселенной C нет никакого garbage collector. Поэтому за памятью придется следить. Рассмотрим следующий код. Вот у меня есть функция print, которая принимает Go-строку, и теперь я, смотрите, получил, сконвертировал это в C-строку, все хорошо, и теперь вот в этой функции конкретной я сразу же освобождаю этот pointer. То есть я занимаюсь ручным освобождением памяти. То есть если вы не будете это делать, у вас сразу начинаются утечки и все прочие прелести. Но я запущу эту программу. [БЕЗ_ЗВУКА] Так. И вот она начинает печатать. Найдем ее. Вот она тут есть. Вот у меня Performance. Performance Graph, память, вот она жрет 2,5 мегабайта, и нормально, никуда не течем, все хорошо. Теперь давайте я вам продемонстрирую утечку памяти. Так, эту мы закрываем, эту мы останавливаем. Теперь, допустим, я в пакете unsafe уберу освобождение памяти. То есть как раз таки тут используется unsafe, для того чтобы получить ссылку на ту область, где в реальности она хранится. И посмотрим, что получилось. Запускаем. Поехали. И вперед. 4, 4.5, 4.9, 5, в общем, мы растем. То есть у нас это даже уже не в кишках C-кода. Это просто я забыл освободить строковую переменную, под которую у меня выделилась память. И у меня сразу же началась утечка памяти, и она растет. В данном случае это понятно, где оно, это более-менее ясно, и это вы еще более-менее можете найти через хотя бы профилировщик pprof. Но еще один нюанс, еще один подводный камень, что как только вы уходите в другую вселенную, pprof перестает видеть, что там происходит дальше. То есть если Go-функцию я еще могу отследить, то на момент ухода в CGO, у меня просто в pprof будет: вызов в CGO. Всё. Дальше он там не знает. И если вы хотите это профилировать, если вы хотите искать там утечки памяти, вам придется использовать [НЕРАЗБОРЧИВО], либо какие-то другие инструменты, для того чтобы найти эти утечки. Поэтому тут стоит быть аккуратнее. Так, ладно, это мы завершим. Хорошо. То есть утечки памяти, надо с ними быть осторожными, чтобы их не допустить. То есть в простом Go вы от этого почти всегда избавлены, в большинстве случаев. Еще один момент, который стоит знать, как я уже говорил, в C не будет у вас никакой синхронщины, и тред будет лочиться эксклюзивно на ваш вызов. Рассмотрим следующий код. Что я тут делаю? 10 мало, давайте поставим 100. Здесь я запускаю 100 горутин, внутри которой я запускаю просто sleep. Все. Что произойдет, если я это запущу, сколько мне системных тредов понадобится? Так. Ага. Так, запустили программу, она ушла спать. Находим вот мои системные треды. Вот они все есть: раз, два, три, четыре, пять, шесть. Вот он мне создал шесть тредов системных, все такое, где он все это вертит, где все это живет. То есть не блокирующие операции и все такое. Что будет, если я попробую вызвать C-sleep, то есть симулировать какую-то блокирующую операцию? Стоп. Давайте я увеличу тут количество тоже до 100, чтобы было наглядно. Запускаем. Запустилось. Вот. Теперь смотрите, сколько у меня тредов появилось. То есть я запустил каждую горутину, у меня 100 горутин, и внутри этой горутины я вызвал C.sleep. А поскольку это вызов C-кода, то он лочит тред на себя. Это значит, что в этом треде больше ничего другого выполняться не сможет. Это значит, что для обслуживания этих 100 горутин мне потребуется 100 системных тредов. В случае, если вы будете ходить по сети с какими-то СИшными блокирующими вызовами, то вы как раз будете получать ту ситуацию, что ваш запрос висит, ничего не делает, ожидает ответа от внешней системы, хотя там могла бы выполняться какая-то горутина. То есть вот оверхед. Так. Остановим. Итак, какие выводы можно из этого сделать? Первое. Вызов CGO небесплатен. Второе. Вызов CGO, то есть C-кода из Go, может повлечь за собой неприятные последствия. Например, у вас начнет течь память, либо начнет сильно расти количество системных тредов. Также вы теряете возможность кросс-компиляции. Но когда вообще стоит использовать это? Во-первых, у вас может не быть выбора. То есть у вас есть какая-то уже скомпилированная библиотека, для которой у вас есть только заголовочные файлики. Поэтому выбора у вас нет, вам придется это подрубать и использовать. Надейтесь только, что там нет хождения по сети. Какие еще хорошие способы применения? То есть если у вас какая-то библиотека, которую вы будете вызывать, например, один раз, то есть оверхед на вызов будет маленький, но при этом она внутри очень оптимизирована, как числодробилка. Например, TensorFlow. В Go есть биндинги для TensorFlow, которые позволяют вам использовать его для расчетов модели. То есть у вас есть уже предпосчитанная модель, и вы просто вызываете ее через TensorFlow. Вот это как раз реализовано через CGO. То есть у вас есть какая-то мощная числодробилка, оптимизированная, которая не ходит по сети, и в этом случае вы можете хорошо выиграть. Еще из примеров использования CGO, например, это библиотека SQLite. Биндинги к SQLite реализованы сейчас через CGO. Когда использовать не стоит? Когда у вас будет очень много маленьких вызовов. То есть операция небольшая, короткая, вы будете терять на оверхеде на вызове. Еще, как я говорил, если вы ходите по сети, то лучше все-таки это попробовать переписать на Go. Есть трансплиттеры C-кода в Go, однако они еще неполноценно рабочие. И есть нюансы. Однако, возможно, в будущем ситуация исправится, у нас будет полноценный конвертер C-кода в Go-код. Но пока этого нет, приходится писать руками. Я надеюсь, что с CGO вы будете сталкиваться очень редко и не наступите на все те грабли, которые в нем есть.