В жизни каждого мужчины наступает момент, когда, окинув взглядом свежую мировую статистику по использованию операционных систем, он понимает, что пришло время больших перемен. Дом, работу и жену при этом менять вовсе не обязательно, а вот попробовать охватить аудиторию, которая заметно выросла за последний десяток лет, все же стоит. Речь пойдет о разработке на Delphi для macOS (в девичестве OS X) и о том, как мы в компании TamoSoft выбирали инструменты, осваивали новое, учились, подрывались на минах и получали удовольствие от процесса.
Задача
Точка отправления: наш главный продукт TamoGraph Site Survey – инструмент для инспектирования Wi-Fi-сетей, который позволяет строить карты покрытия, оптимизировать размещение access points, создавать виртуальные модели распространения сигнала и делать еще много полезных вещей для инженеров, работающих в этой области. TamoGraph работает под Windows. Точка назначения: ну вы уже догадались. TamoGraph, который бы работал под macOS.
Продукт написан большей частью на Delphi, отдельные модули написаны на С++. Почему именно Delphi? (Варианты вопроса: Он еще не умер? Вы больные? Язык X на порядок лучше, а вы ретрограды, неспособные освоить новое!) Друзья, причин, почему мы используем не самый модный и популярный язык/среду (если верить Tiobe, сегодня Object Pascal 11-ый по популярности язык) много. Это и отличная продуктивность, и да, сила привычки, и быстрый компилятор, но самая главная причина лежит совершенно не в сфере технологий. Нам просто нравится писать на Delphi, мы получаем от этого кайф. А когда продукт написан с удовольствием и любовью, он, как правило, хорошо работает. Так что не будем заниматься религиозной полемикой, а перейдем непосредственно к делу.
Итак, из точки отправления (Windows, Delphi) мы должны кратчайшим путем попасть в точку назначения (macOS, пока неизвестный язык/среда). Были рассмотрены следующие основные варианты:
1. Переделать всё на Xcode, используя Swift или Objective-C.
2. Переделать бОльшую часть на Xcode с использованием части существующего Delphi-кода в виде динамических библиотек.
3. Переделать бОльшую часть на Delphi, используя фреймворк FMX (FireMonkey), а небольшую часть кода написать на Objective-C и использовать в виде динамических библиотек.
4. Переделать всё на RemObjects Elements, используя Oxygene, их разновидность Object Pascal.
У каждого варианта, естественно, нашлось много преимуществ и недостатков. Xcode – это полная нативность GUI, отсутствие каких-либо проблем при взаимодействии с операционной системой, масса sample code и библиотек. Но, и это очень большое «но», со всем этим «в комплекте» идет необходимость переписать очень много кода на другой язык. RemObjects Elements – также полная нативность GUI, при этом очень близкий к Object Pascal язык, что означает, что существующий код, не связанный с GUI, можно было бы использовать с относительно небольшими изменениями. Однако, этот инструмент никто из нас на тот момент еще не опробовал. И, наконец, Delphi FMX. Из плюсов – использование существующего отлаженного кода на полную катушку, знакомая среда, но при этом ненативные контролы (хотя, как оказалось, это не совсем так, подробнее ниже), возможные сложности при взаимодействии с macOS API, и много других сомнений.
Неспешно посовещавшись и проведя кое-какие тесты, мы, как вы догадались по заголовку этой статьи, остановились на варианте (3), т.е. Delphi FMX. Уж очень привлекательной была возможность не переписывать значительную часть кода. И, признаться, уж очень не понравился RemObjects Elements, к которому я изначально склонялся. Итак, выбор сделан, засучили рукава и поехали…
Арт-подготовка
Часть команды уже как минимум имела опыт тесного общения с macOS и хорошо представляла ее устройство. Часть же была совсем новичками, которым потребовалась теоретическая подготовка. Для этих целей неплохо подошла книга Mac OS X and iOS Internals: To the Apple's Core. Что касается практики, то всем нуждающимся были куплены MacBook'и, а на виртуальных машинах были развернуты разные версии macOS, от 10.9 до самой последней 10.12.
Процесс отладки программы для macOS на Delphi отличается от привычного процесса для Windows, где, как правило, вы запускаете отлаживаемую программу на том же компьютере, где работает среда Delphi. C macOS все несколько сложнее: для начала вы устанавливаете на машине с macOS Platform Assistant, т.е. вспомогательное приложение (часть Delphi), которое обеспечивает deployment и отладку приложения, а со стороны Delphi, уже под Windows, вы указываете IP-адрес машины, на которой запущен Platform Assistant:
Дальше вы просто запускаете свою программу, которая тут же начинает работать на Маке. Естественно, ее можно отлаживать ровно так же, как все мы привыкли отлаживать Windows-программы.
Контролы FMX
Итак, все настроено, можно запустить свой первый «Hello World» на Маке. GUI делается в привычном визуальном редакторе Delphi с помощью визуальных компонентов FMX. Фреймворк FMX появился в Delphi еще в 2011 году, в версии Delphi XE2. Надо сказать, что вначале он был крайне глючен, но за эти шесть лет его основательно переписали, заметно снизив количество проблем. Сейчас это вполне пригодный к использованию набор компонентов, начиная от простейшей TButton и заканчивая grids, listview, и прочими привычными контролами. Поэтому делать на FMX интерфейсы на сегодня вполне реально и комфортно, однако здесь есть некоторые особенности.
Во-первых, FMX-контролы не нативны. Это не обертка вокруг системных контролов, как это сделано в VCL, где, к примеру, TButton – это системный контрол, который рисует Windows, а не Delphi. Тут контролы рисует Delphi, задействовав свой стилевой движок, который использует стиль, соответствующий стилю той версии macOS, на которой запущена программа.
Пример диалога на Yosemite (10.10):
Ниже тот же диалог на Mavericks (10.9). Стили элементов GUI автоматически адаптировались под «родной» стиль Mavericks и выглядят уже иначе:
В принципе это работает неплохо, хотя некоторое вещи в стилях приходится подправлять (или использовать нативные контролы, о чем ниже). Например, «графитовый» стиль macOS, который появился в Yosemite, в Delphi отсутствует, и его пришлось сделать самостоятельно. На это ушло два человеко-дня.
Вторая проблема – «детские болезни». Ребеночку (фреймворку FMX), как я уже говорил, шесть лет, и несмотря на усилия Embarcadero, он еще не до конца переболел всем, чем нужно. Например, в главном меню приложения событие OnClick срабатывает для всех айтемов, кроме айтемов верхнего уровня. Т.е. если у вас меню File > Open, File > Save и так далее, то ивент OnClick случится при клике на Open и Save, но не случится при клике на File, когда произойдет выпадение списка сабайтемов. Или возьмем стандартные диалоги Open и Save. Совершенно неожиданно показ диалога полностью «затыкает» event loop приложения, и у вас перестает что-либо происходить (включая тики таймеров), пока диалог открыт. Все это, на мой взгляд, результат слишком слабого тестирования in-house и слишком медленного реагирования Embarcadero на баг-репорты.
Эти болезни лечатся в run-time, без патчинга системных юнитов. Отсутствие OnClick мы вылечили перехватив вызов 'menuWillOpen:' класса TFMXMenuDelegate, показ системных диалогов мы вообще переписали целиком, но чтобы исправить баг, надо сначала на нем подорваться. Будьте бдительны, не пренебрегайте тестированием, и не забывайте сообщать о багах на quality.embarcadero.com.
Наконец, закрывая тему FMX-контролов, советую взглянуть на TMS FMX UI Pack, который включает в себя много очень неплохо написанных визуальных компонентов, в том числе отличный TreeView, умеющий работать в виртуальном режиме. Это как раз то, чего нет в стандартных компонентах FMX.
Run-Time Library
Использование Delphi RTL ожидаемо оказалось наиболее беспроблемной частью при портировании кода на macOS. RTL уже давно «заточена» под мультиплатформенность, поэтому вы совершенно смело можете использовать любые функции и невизуальные классы без изменений. Нужно лишь следить за такими мелочами как, например, использование платформонезависимой IncludeTrailingPathDelimiter вместо hard-coded разделителя "\".
macOS API
Когда вы пишите что-либо чуть более сложное, чем калькулятор, вам рано или поздно придется использовать native API. Обойтись одними только RTL и фреймворком FMX совершенно нереально, равно как под Windows нереально обойтись лишь одними RTL и VCL. Нужно узнать системную локаль? Реализовать interprocess communications? Узнать размер virtual memory процесса? Шифрование? Синтез речи? Все это, естественно, native API. Но это совершенно не должно пугать, как нас не пугает вызов какой-нибудь FindWindow или GetLocaleInfo под Windows. А если что в Delphi не задекларировано, то можно задекларировать, добавить и переделать все что угодно.
Сам по себе API состоит из нескольких компонентов (BSD, Mach, Carbon, Cocoa и т.д.), но для наших целей главный интерес представляет собой Cocoa. Если говорить упрощенно, то Cocoa – это набор классов, что довольно непривычно для тех, кто привык использовать Windows API. Например, если вам нужно узнать смещение временной зоны компьютера относительно UTC, то в Windows это просто функция GetTimeZoneInformation. А вот в macOS это уже класс NSTimeZone. К этому со временем привыкаешь, покуривая на досуге Apple API Reference, ровно так же, как почти все мы когда-то в начале пути покуривали MSDN. Но вот от чего реально поначалу взрывается мозг, так это от синтаксиса «мостика» между Delphi и классами Cocoa. Это очень непривычно.
Class functions вызываются через волшебное слово OCClass:
TNSTimeZone.OCClass.localTimeZone
Возвращают они как правило указатели, но не все не так просто. Эти указатели на объекты нельзя использовать напрямую; указатели представляют из себя то, что называется id в Objective-C, и чтобы преобразовать такой указатель в объект, нужно сделать волшебный Wrap:
TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone)
И вот теперь уже мы уже можем вызвать функцию экземпляра класса и получить наконец требуемое смещение:
TimeZoneShift:= TNSTimeZone.Wrap(TNSTimeZone.OCClass.localTimeZone).secondsFromGMT;
Еще примерчик? Пожалуйста! Проверяем доступность сервера:
function BlockingGetTestURL: boolean;
var
URL: NSURL;
URLRequest: NSURLRequest;
aData: NSData;
Response: Pointer;
Policy: NSURLRequestCachePolicy;
TimeOut: NSTimeInterval;
const
URL_TO_CHECK = 'http://open.mapquestapi.com';
begin
URL := TNSURL.Wrap(TNSURL.OCClass.URLWithString(StrToNSStr(URL_TO_CHECK)));
Policy:= NSURLRequestReloadIgnoringLocalCacheData;
TimeOut:= 10;
URLRequest := TNSURLRequest.Wrap(TNSURLRequest.OCClass.requestWithURL(URL, Policy, TimeOut ));
aData := TNSURLConnection.OCClass.sendSynchronousRequest(URLRequest, @Response, nil);
result:= (aData <> nil) and (aData.length > 0);
end;
Когда функция Objective-C-класса хочет от нас указатель на объект, мы опять же не можем просто взять и передать @MyDelphiObject, мы должны исполнить ритуальный танец по преобразованию этого указателя в id с помощью функции GetObjectID:
function GetUserDefaultMeasureUnit : TSSMeasureUnitType;
var
p: pointer;
ns: NSString;
const
AppKitFwk: string = '/System/Library/Frameworks/AppKit.framework/AppKit';
begin
ns:= CocoaNSStringConst( AppKitFwk, 'NSLocaleUsesMetricSystem');
p := TNSLocale.Wrap(TNSLocale.OCClass.currentLocale).objectForKey((NS as ILocalObject).GetObjectID);
if TNSNumber.Wrap(p).boolValue
then Result := msMeters else result:= msFeet;
end;
В общем, к синтаксису вполне можно привыкнуть, изучив примеры. Советую прочесть статью Using OS X APIs directly from Delphi, в которой эта тема хорошо раскрыта.
Если же говорить непосредственно про API (не ограничиваясь только Cocoa), то он оставляет довольно приятное ощущение. Каких-то вещей, имеющихся в Windows API, в macOS API попросту нет, и наоборот. Какие-то вещи в macOS делаются сложнее, чем в Windows, какие-то проще. Взять, к примеру, AES-шифрование. В Windows, чтобы зашифровать массив байт, нужно использовать пяток функций и пару дюжин строк кода, тогда как в macOS это можно сделать практически в одну строку функцией CCCrypt. И это уже не часть Cocoa.
Милый, милый POSIX
POSIX тоже не является частью Cocoa, но, черт возьми, большое ему спасибо, что он есть на macOS! Это делает жизнь намного проще. Многое, что можно сделать через классы, на высоком уровне, гораздо проще сделать на низком уровне через POSIX. Например, как реализовать interprocess communications? Distributed Objects и класс NSProxy? NSConnection? Забудьте, все решается в пару строк кода через memory-mapped files и функции POSIX. Нам нужны shm_open, shm_unlink и mmap. Первые две, кстати, в Delphi не задекларированы, но это не проблема. Внимательно читаем описание, декларируем:
function shm_open(__name: PAnsiChar; __oflag: integer; __mode: mode_t): integer; cdecl; external libc name _PU + 'shm_open';
function shm_unlink(__name: PAnsiChar): integer; cdecl; external libc name _PU + 'shm_unlink';
А дальше все просто, вызываем:
fd := shm_open( PAnsiChar(UTF8Encode(ID)), O_RDWR or O_CREAT, S_IRUSR or S_IWUSR or S_IRGRP or S_IROTH );
ftruncate(fd, aSize);
mmap(nil, aSize, PROT_READ or PROT_WRITE, MAP_SHARED, fd, 0);
Все, мы создали маппинг, доступный по имени из других процессов.
Зачем нам еще может быть нужен POSIX? Да для многих вещей. Например, вот:
function GetPhysicalCoreCount: Cardinal;
var
CoreCount: Cardinal;
Size: Integer;
begin
Size:= SizeOf(Cardinal);
if sysctlbyname('hw.physicalcpu',@CoreCount, @Size, nil, 0) = 0
then result:= CoreCount else result:= System.CPUCount;
end;
Работа с сокетами, с COM-портами и многое другое – для всего этого годится простой и привычный POSIX, почти с тем же синтаксисом, что и в Windows. Среди прочего, нам нужно было портировать Delphi-класс для работы с COM-портами, который мы использовали под Windows для работы с GPS-приемниками. Кода там примерно 1500 строк. Сложно? Нет, не очень. День работы и примерно 50 IFDEF'ов такого вида:
function TGPSReceiver.ClearInputBuffer: Boolean;
begin
Result := False;
if Assigned(ComThread) and ((ComThread as TComThread).ComDevice <> GPS_INVALID_HANDLE_VALUE) then
begin
try
{$IFDEF MSWINDOWS}
Result := PurgeComm((ComThread as TComThread).ComDevice, PURGE_RXCLEAR);
{$ENDIF}
{$IFDEF MACOS}
result:= tcflush((ComThread as TComThread).ComDevice, TCIFLUSH) = 0;
{$ENDIF}
except
Result := False;
end;
end;
end;
Портировали, протестировали, к концу рабочего дня получили весело мигающий изображениями спутников модуль для работы с GPS.
Нативные контролы
Если вас не устраивает стандартный набор FMX-контролов, то это не беда. Никто не запрещает использовать нативные визуальные классы и даже смешивать их с FMX-контролами, соблюдая определенные правила. Собственно говоря, никто не запрещает даже совсем не использовать фреймворк FMX в вашем приложении Delphi (хотя это уже слегка экстремально).
Нативные классы стоит использовать ради производительности. Мы, например, столкнулись с тем, что viewport, выполненный на FMX-компонентах заметно тормозил при zoom'е и scroll'е больших битмапов, мы заменили его на нативный NSScrollView c NSImageView внутри. Чтобы получить доступ к событиям нативных классов, их надо сабклассить и/или использовать delegates. Это довольно тривиально кодируется в Delphi, и в результате вы получаете доступ к любым событиям. Нужно событие magnifyWithEvent класса NSImageView? Не проблема. Наследуем интерфейс:
NSImageViewEx = interface(NSImageView)
['{3E4F87DA-0577-4F21-A1CF-8BCA774FA903}']
procedure magnifyWithEvent(event: NSEvent); cdecl;
end;
Делаем класс-имплементатор:
TExtendedNSImageView = class(TOCLocal)
…
public
procedure magnifyWithEvent(event: NSEvent); cdecl;
…
end;
И делаем все что хотим, когда вызывается метод класса-имплементатора:
procedure TExtendedNSImageView.magnifyWithEvent(event: NSEvent);
begin
// Do whatever you want
end;
Чтобы это работало, нужно еще некоторое количество кода при создании класса; примеры можно легко найти в интернете. Сабклассинг – не единственный способ перехвата событий, можно также использовать method swizzling, и я даже приведу пример ниже.
Вот так примерно мы и живем, смешивая нативные и FMX-контролы.
Что (пока) не может Delphi на macOS
С большой бочкой меда зачастую идет некоторое количество не столь прекрасной субстанции. Поговорим для разнообразия о недостатках.
Из нерешаемых проблем пока есть одна, но довольно важная. Это 64-битный компилятор для macOS, который есть в roadmap, но пока не сделан. Это, конечно, позор для Idera/Embarcadero, которые увлечены, на наш взгляд, гораздо менее важными вещами, пренебрегая Mac-веткой продукта. Так что, ждем с нетерпением.
Из решаемых – code blocks, языковая фича С++ и Objective-C, которая не поддерживается в Delphi. Точнее, Delphi имеет свой аналог code blocks, но он несовместим с теми code blocks, которые ожидает от наc macOS API. Дело в том, что многие классы имеют функции, в которых используются code blocks в качестве handler'ов завершения. Самый простой пример — beginWithCompletionHandler классов NSSavePanel и NSOpenPanel. Передаваемый сode block выполняется в момент закрытия диалога:
- (IBAction)openExistingDocument:(id)sender {
NSOpenPanel* panel = [NSOpenPanel openPanel];
// This method displays the panel and returns immediately.
// The completion handler is called when the user selects an
// item or cancels the panel.
[panel beginWithCompletionHandler:^(NSInteger result){
if (result == NSFileHandlingPanelOKButton) {
NSURL* theDoc = [[panel URLs] objectAtIndex:0];
// Open the document.
}
}];
}
На Delphi такой «трюк ушами» исполнить, видимо, пока крайне проблематично (по крайней мере, нам это не удалось). Иными словами, нормальным путем мы не можем узнать о закрытии диалога. Но нормальный путь – это даже скучно! Кто нам мешает пойти ненормальным путем? Извращенных подходов к решению таких проблем несколько, но в данном случае, например, хорошо сработает следующий. Для начала мы можем получить список всех, как документированных, так и недокументированных, функций класса NSSavePanel. Делается это примерно так:
function ListMethodsForClass(const aClassName: string): string;
var
aClass: Pointer;
OutCount, i: integer;
Arr: PPointerArray;
p: PAnsiChar;
begin
result:= 'Instance methods for class ' + aClassName + ':' + #13#10;
aClass := objc_getClass(PAnsiChar(ansistring(aClassName)));
if aClass <> nil then
begin
Arr:= class_copyMethodList(aClass, OutCount);
if Arr <> nil then
begin
for i := 0 to OutCount - 1 do
begin
p:= sel_getName(method_getName(Arr^[i]));
result:= result + string(p) + #13#10;
end;
Posix.Stdlib.free(Arr);
end;
result:= result + 'Class methods:' + #13#10;
Arr:= class_copyMethodList(object_getClass(aClass), OutCount);
if Arr <> nil then
begin
for i := 0 to OutCount - 1 do
begin
p:= sel_getName(method_getName(Arr^[i]));
result:= result + string(p) + #13#10;
end;
Posix.Stdlib.free(Arr);
end;
end;
end;
Получили список и ищем что-нибудь вкусненькое… Ага, нашли: "_didEndSheet:returnCode:contextInfo:". Очень похоже на то, что нам нужно. Надо проверить теорию, вызывается ли этот селектор при закрытии диалога. Можно сделать сабкласс NSSavePanel, а можно грубо и беспардонно поставить хук на этот селектор, подменив имплементацию метода (method sizzling):
const
END_SHEET_SELECTOR : ansistring = '_didEndSheet:returnCode:contextInfo:';
SAVE_PANEL_CLASS : ansistring = 'NSSavePanel';
var
endSheetOld: procedure (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl;
procedure endSheetNew (self: pointer; _cmd: pointer; sheet: pointer; returncode: NSinteger; contextinfo: pointer); cdecl;
begin
endSheetOld(self, _cmd, sheet, returncode, contextinfo);
FDialogClosed:= ReturnCode;
end;
procedure DoDialogHooks();
var
FM1, aClass: pointer;
begin
aClass := objc_getClass(PAnsiChar(SAVE_PANEL_CLASS));
if aClass <> nil then
begin
FM1 := class_getInstanceMethod(aClass, sel_getUid(PAnsiChar(END_SHEET_SELECTOR)));
if FM1 <> nil then
begin
@endSheetOld := method_getImplementation(FM1);
method_setImplementation(FM1, @endSheetNew);
end
else raise Exception.Create('Failed to hook NSSavePanel');
end;
end;
Проверяем – и о чудо, в момент закрытия диалога по Cancel или OK мы попадаем в хукнутую функцию и, соответственно, узнаем, что диалог закрыт, а также и сам результат закрытия.
Мины
Наверное, никому не удавалось создать продукт, не подорвавшись на минах, но, подчеркну это специально еще раз, количество подрывов можно минимизировать, если вы будете побольше смотреть на чужой код, читать книги и API reference. Нет, правда, лучше прочесть о каком-нибудь App Nap на developer.apple.com, чем не прочесть и потом долго гадать, почему все таймеры в вашем приложении стали вдруг тикать в 10 раз реже. И лучше узнать заранее, что строковые параметры в POSIХ-функциях должны передаваться в кодировке UTF-8, а не ANSI или UTF-16. И тестируйте, тестируйте, тестируйте… Причем «и за себя, и за того парня». Да, мины будут и в Delphi тоже, Idera/Embarcadero не очень любит тестировать Mac-часть продукта. Ну не падала бы у них Macapi.Foundation.NSMakeRect, если бы нормально было организовано тестирование.
Итоги
Надеюсь, для тех, кто раздумывает о том, как сделать продукт для macOS, первое знакомство с Delphi + Cocoa оказалось познавательным. Связка вполне рабочая, позволяющая делать серьезный софт. А мои пожелания Idera/Embarcadero – не забывайте про macOS. Я понимаю, что мобильная разработка – это очень модно, но разработка десктопного софта – весьма приличный рынок, в чем вы могли убедиться на примере Windows за последние лет 20. У вас есть почти все для отличного продукта для macOS, нужно только еще немного потрудиться. Выкатывайте скорее 64-битный компилятор и исправляйте то, о чем вам поведали на quality.embarcadero.com.
Комментарии (25)
dima_1st
19.01.2017 13:50Если не поддерживаются блоки на уровне языка, то как например предполагается работа с GDC (Grand Dispatch Central)? Собственно работа с потоками в iOS это в 90% GDC + block.
NightFlight
19.01.2017 14:14+2Ок, на эту тему могу сказать следующее:
- Для iOS (но не для macOS) блоки реализованы в юните Macapi.OCBlocks.pas. Я лично не проверял, как это работает, поскольку для iOS мы пока ничего не пишем, но, теоретически, должно работать.
- GDC вполне «юзабелен», особенно с помощью обертки, о которой идет речь тут: https://ridingdelphi.blogspot.ru/ . Ссылка на сорс там есть внизу. Хотя, замечу, что в macOS у нас пока не возникло необходимости использовать этот код.
- Для работы с потоками нас пока вполне устраивает родной дельфийский TThread (ну и плюс анонимные треды, что тоже очень удобно).
Woit
19.01.2017 16:56-1Асинхронщину можно писать не юзая GCD и блоков вообще. Но без блоков грустно, да.
Не хочется холиварить, но это наверное тяжело? Я имею ввиду забивать гвозди отверткой?
Не имею ничего против дельфей, сам писал некоторое время на них, но ведь разумнее брать инструмент под задачу? Это не претензия, но как мне кажется, если человек очень хорошо знает дельфи/другой хардкор язык, то по времени дешевле разучить бегло obj-c, раз уж приходится писать под osx. Тем более, что на стаке все, с чем вы можете столкнуться на разных уровнях понимания, уже решено и разобрано
upd: извините, промахнулся уровнемNightFlight
19.01.2017 17:25+1Ощущения забивания гвоздей отверткой совсем нет. Наоборот, приятно и интересно. Мне кажется, что это все же преувеличение. Я как раз писал о том, что инструмент адекватен задаче.
По поводу изучения obj-c — да, это один из возможных путей, и для некоторых из нас даже учить ничего не надо, уже всё выучено, но я объяснил в статье, что нам представилось более разумным решением не переписывать горы кода, а использовать большую часть as is. Так что никаких священных войн, чистая целесообразность.Woit
19.01.2017 17:30Что ж, тогда могу только пожелать удачи, т.к. поддержание актуальности ПО для продуктов apple это то еще приключение )
NightFlight
19.01.2017 17:37+1Спасибо! Мы привыкли к приключениям:) В Windows тоже много приключений было за последние 20 лет.
ireg
19.01.2017 18:18+1А вариант с Lazarus не рассматривался?
NightFlight
19.01.2017 18:40+2Рассматриваля на самом начальном этапе, но был отброшен. Кроме 64-битного компилятора, других преимуществ мы в Lazarus не нашли. Source code пришлось бы править в той или иной степени. И с визуальными контролами там не так чтобы всё хорошо было.
ElectroGuard
19.01.2017 22:07+1Спасибо за статью! Очень познавательно и интересно.
если человек очень хорошо знает дельфи/другой хардкор язык,
Хардкор язык — это брейнфак. А делфи — отличный язык, позволяющий писать один код для многих платформ сразу, не распыляясь на кучу языков и сред.
Zapped
23.01.2017 16:42День работы и примерно 50 IFDEF'ов такого вида:...
слышали про Design Patterns? ;)NightFlight
23.01.2017 22:56Да, конечно, только не очень понял, к чему вы в данном случае клоните… Есть Delphi-класс (кстати, написанный не нами) для работы с GPS через COM-порты. Класс для одной платформы, Windows. Мы его сделали мультиплатформенным, используя несколько десятков conditional defines, чтобы под Windows остался WinAPI, а под macOS стал использоваться POSIX. Нам мог бы облегчить жизнь какой-нибудь из design patterns? Если да — расскажите, буду благодарен, и в следующий раз мы пойдем другим путем.
Zapped
24.01.2017 01:17собственно, к тому и клоню, что conditional defines в приведённом Вами виде — это лапша, ведь согласитесь?
представьте (просто в качестве мысленного эксперимента), что вам надо будет портировать этого класса подLinuxдаже лучше так — Android (Delphi ж как раз умеет)
Вам опять придётся, уверен, ползать по всему коду и вставлять $IFDEF ANDROID… код станет ещё развестистей… вместо добавления модуля для работы с COM-портом в Android (а это та ещё песня, скажу я вам)
а рассказать — рассказано уже всё давно в книжках (как раз сегодня дочитал подаренную на НГ вышеупомянутую «Design Patterns») — паттерны «Фабричный метод» + «Cтратегия», например, КМК…
посмотрите, как реализована работа с Bluetooth или буфером обмена в исходниках самого Delphi:
как Вы понимаете, реализации работы с ними под каждой платформой — свои, но клиентский (ваш) код — не содержит никаких IFDEF'ов, один код на все платформы
всё сделано в коде FMX, причём «красиво» и понятно: под каждую платформу — в своём модуле.
а чтобы добавить, например, поддержку BT в Linux (ну, это я так, абстрактно), им только надо будет добавить модуль реализации для этой платформы, а в вашем коде менять ничего не придётся…
З.Ы. Я сейчас как раз закончил полугодовой марафон по портированию одного нашего (небольшого, в общем-то) проекта под Linux (на Free Pascal). Но код был и без тестов, и лапшой, и с копи-пастой, так что я очень в теме )
прежде, чем менять что-то, надо покрыть тестами, чтобы хоть как-то покрыть тестами, надо мало-мальски отрефакторить… заодно понаходил кучу ошибок…
З.З.Ы. а у вас в этом проекте автотесты есть? ;)
З.З.З.Ы. и кстати,
if sysctlbyname('hw.physicalcpu',@CoreCount, @Size, nil, 0) = 0 then result:= CoreCount else result:= System.CPUCount;
это продакшн-код? я имею в виду форматирование
NightFlight
24.01.2017 11:33Спасибо за разъяснения!
собственно, к тому и клоню, что conditional defines в приведённом Вами виде — это лапша, ведь согласитесь?
Да, примерно в той же степени, что и System.Sysutils или System.Classes.
всё сделано в коде FMX, причём «красиво» и понятно: под каждую платформу — в своём модуле.
а чтобы добавить, например, поддержку BT в Linux (ну, это я так, абстрактно), им только надо будет добавить модуль реализации для этой платформы, а в вашем коде менять ничего не придётся…
Согласен, этот подход неплох. Просто дело в том, что иногда приходится выбирать между красотой и производительностью труда. Можно сделать красиво, как те же platform services в FMX, а можно быстро, с IFDEFs, причем, на мой взгляд, вероятность наделать ошибок во втором случае существенно ниже, потому что не будет такого объема рефакторинга. Когда есть много людей и много времени — это одно, когда людей мало и когда вы торопитесь выкатить продукт быстрее конкурентов — это другое. У нас был сотрудник, который мог потратить несколько дней на то, чтобы написать супер-эффективную function IPv4ToStr(const IP: Cardinal): string, которая работала быстрее станадртной винсоковской в десятки раз. Проблема была только в том, что она вызывалась крайне редко, и ее скорость работы ни на что не влияла… В конце концов мы расстались:-)
Про автотесты — нет. Про форматирование — ну слушайте, давайте не будем обсуждать личные преференции конкретного программиста в части форматирования:-) Опять же, в идеальном мире все форматируют идеально. В реальном — как привыкли. Я никого за это не ругаю, хотя сам часто бурчу под нос, когда читаю код с нестандартным форматированием.Zapped
24.01.2017 17:58да я, можно сказать, «в курсе» таких «оправданий» )
я называю их «технический долг»
З.Ы. тот код, который я портировал, писался лет 7 назад по такому же принципу: «да зачем заморачиваться — ведь только под Win32 будет работать»… а потом появилась необходимость в Win64 (а работа с преобразованием Pointer в Integer и обратно не менялась (и как оно работало?) )))))… и под Linux (тут во весь рост copy-paste вместо декомпозиции)
и в итоге я потратил ПОЛГОДА(sic!), чтобы только, по большому счёту, заменить реализацию TCP-сервера с Windows-only компонентов OverbyteICS на кроссплатформенные Indy, хотя весь остальной код в общем и целом был практически кроссплатформенный… только вот беда — неоднородный, с дублированием (а в одном месте — так и вообще затроенный(!)), нетестируемый… ))) но «работал же! надо было быстро»
ладно, это пустое (опыт показывает, что мне не убедить даже коллег, которые говорят «да, да! надо!», но продолжаютговнокодитьделать по-своему )))
Вы мне скажите, как у вас релизы выпускаются? чем собираются?NightFlight
24.01.2017 19:25и в итоге я потратил ПОЛГОДА(sic!), чтобы только, по большому счёту, заменить реализацию TCP-сервера с Windows-only компонентов OverbyteICS на кроссплатформенные Indy
Кстати ICS неплохо работает на macOS, мы его используем в нашей бесплатной утилите TamoSoft Throughput Test (маковская версия тоже на FMX сделана). Жаль, что не работает на Linux и iOS.
Вы мне скажите, как у вас релизы выпускаются? чем собираются?
Не очень понял, о каких именно релизах речь. Windows? macOS? На macOS продукт пока не готов. На Windows — всё билдится из командной строки, собирается в сетап, подписывается, и.п. Или речь о чем-то другом? Единственный «ручной» этап, спасибо новым политикам Microsoft, это подписывание драйверов для Windows 10 на их портале. Это конечно дико неудобно.Zapped
25.01.2017 15:11Не очень понял, о каких именно релизах речь. Windows? macOS?
а я и не уточнял каких именно )) я обо всех и спрашивал )
всё билдится из командной строки
ну уж я надеялся, что не в самой Delphi )))
а чем билдится? что/кто эту командную запускает?ElectroGuard
25.01.2017 15:49Мы у себя, например, собираем всё тупо батником. Запускается вручную, собирается под 30 exe, потом упаковывается и отправляется на фтп.
Zapped
25.01.2017 16:17Hint: посмотрите в сторону MSBuild'а ))
родные проекты Delphi 2007 и выше — это проекты MSBuild (.dproj-файлы)
*хотя, работает — не трогай ))))
NightFlight
25.01.2017 16:08Windows: bat-файл, который:
- Делает MSBuild проекта.
- Делает дистрибутив с помощью SetupBuilder, который в процессе сам подписывает Authenticode.
- Зипует.
Bat-файл запускает специально обученный homo sapiens.
Mac (предварительно, как я сказал, коммерческих релизов пока нет): bat-файл, который:
- Делает MSBuild проекта.
- Деплоит релиз на Мак с помощью embdeploy.
- Подписывает релиз на Маке с помощью того же embdeploy (хотя можно через paclient.exe).
- Делает DMG-файл на Маке с помощью CreateDMG.
Bat-файл запускает специально обученный homo sapiens, иногда тот же самый.Zapped
25.01.2017 16:22Делает MSBuild проекта.
ага! а как версию файла выставляете? или она у вас в исходниках (в .dproj-файле) меняется?
подозреваю, что последнее, судя поBat-файл запускает специально обученный homo sapiens.
… т.е. CI-сервера нет…
*не лень же вам…NightFlight
25.01.2017 16:44ага! а как версию файла выставляете? или она у вас в исходниках (в .dproj-файле) меняется?
подозреваю, что последнее, судя по
В исходниках меняется.
т.е. CI-сервера нет…*не лень же вам…
Зато есть к чему стремиться:) Нет предела совершенству.
Zapped
24.01.2017 18:01а про форматирование: в приведённом мной куске кода даже не столько дело вкуса (хотя я считаю такой код некрасивым (да и он не Borland-style)), сколько даже неудобно: нельзя поставить точку останова на одну ветку условия
darked
Большое спасибо за статью. Интересно было почитать ещё что — нибудь техническое и вкусное про подводные камни на пути при портировании. Ну, и маркетинговую часть интересно послушать, насколько успешен был выход на данную платформу.
NightFlight
Про техническое и вкусное — буду писать еще. Про маркетинговую часть — пока еще рано, продукт в стадии beta, а вот после релиза поделюсь наблюдениями. Спасибо!