Обновить
57
69.7

Пользователь

Отправить сообщение

то никакого спора бы не получилось.

Почему не получилось бы?

Бейсик — понятие размытое. У него куча диалектов и разных реализаций. Поэтому нужно говорить о конкретном диалекте и конкретном продукте. Я не говорю про все Бейсики на свете, особенно про совсем допотопные «нечто» из 60-х годов, где даже блочных IF не было и их приходилось эмулировать с помощью GOTO (убеждён, что Дейкстра именно по этой причине наезжал на Бейсик).

Но меня всегда задевают наезды на два диалекта: Visual Basic (особенно сильно) и QuickBasic (в меньшей степени, но тоже).

Для этих двух диалектов выражение «не существует компиляторов в код с нативными инструкциями» — не верны, потому что и QuickBasic-овский компилятор компилирует в нативный код для x86 (для него это вообще единственный режим), и Visual Basic тоже это делает (для него это основной режим, режим по умолчанию, но есть альтернативный режим с генерацией в код собственной виртуальной машины).

Тем не менее, даже после того, как продемонстирован код на QB/VB и непосредственно машинный код, в который его превратил соответсвующий компилятор, находятся люди, которые пишут «вы всё врёти, там виртуальный машина и вообще построчная интерпретация». Я не понимаю, что с этими людьми — наверное тяжело разрушить в голову догму, с которой жил очень долго.

Да, он там вам, вероятно, не хуже С++ оформит фрейм вызова подпрограммы, но на том всё и закончится. Как только речь зайдёт о связывании программы с библиотеками, VB будет куда более медленным.

Это опять клевета на VB. С чего бы ему быть более медленным?

VB будет ровно настолько медленным, насколько вы его заставите. Если сама библиотека, с которой вы намерены работать, предлагает изначально медленный интерфейс взаимодействия, то взаимодействие через этот интерфейс будет естественно медленным. Но оно таким будет хоть из Delphi, хоть из C++.

То есть ли библиотека предполагает, что взаимодействовать с объектами можно только через позднее связывание (по терминологии COM) через IDispatch, то естественно это будет медленно: потому что нужно имена методов/свойств транслировать в DISPID, все аргументы упаковывать в VARIANT, если используются именованные аргументы — ещё их их dispid резолвить.

Если же вы работаете с библиотекой через вызовы по типу раннего связывания (по терминологии COM), то и код вызовов не будет «куда более медленным» (потому что с чего бы ради ему таким быть?), чем эквивалентный код на Delphi или на C++.

Либо если библиотека предлагает оба способа, но вы насильно со стороны VB-кода выбираете более медленный (но более гибкий) способ, то это будет медленнее, но это же были целиком вашим решением.

Я ещё раз предлагаю посмотреть на пример взаимодействия с Direct3D:
https://habr.com/ru/companies/ruvds/articles/971476/#comment_29219988

Какие излишества тут сгенерировал компилятор VB? В каком месте и за счёт чего Dephi или C++ обыграли бы его, сгенерировав более компактный и быстрый код?

По сравнению с чем?

Если этот вопрос относится к фразе «компилятор у Борланда генерировал более убогий машинный код», то ответ — по сравнению со здравым смыслом.

Я много раз в машинном коде, сгенерированном Борландом, замечал идеому в духе:

mov eax, dword [xxxxx]
mov ecx, eax
mov edx, ecx
mov eax, edx
; дальше используется eax, копии того же самого 
; значения в регистрах ecx и edx тут же перезатираются чем-то ещё

или

mov ecx, 0
mov eax, ecx

вместо просто xor eax, eax

Microsoft-овский компилятор такими глупостями не страдал, а вот борландовский — да. Неужели никогда не замечали?

 Так что объединение вполне корректное. :)

Нет, не корректное.

У QuickBasic в скомпилированном виде не было никакого Pcode и никакой виртуальной маины. Статья это чётко демонстрирует. У Visual Basic виртуальная машина в скомпилированном виде была, но во-первых, не в EXE, а в msvbvm?0.dll, а во-вторых, она не крутилась, а лежала там мёртвым грузом, если проект был скомпилирован в Native Code.

А это и не работает эффективно: это очень сильно зависит от кода, от того, что там делается и от того, как он написан — какой-то код будет быстрее при компиляции в native code, какой-то в P-код, плюс ещё от системы, на которой он выполняется и на которой происходит замер. В общем, русская рулетка и вопрос удачи.

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

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

Вы ошибаетесь по поводу Pcode

Я ни в чём не ошибаюсь насчёт Pcode. Если есть что возразить, пишите конкретно, в чём именно я ошибаюсь.

 в Quick/PDS/QBasic/VBDos/VBWin

Всё смешали в одну кучу.

Не надо «ЕМНИП», я в качестве реакции на «бесподобные» комментарии под данной статьёй написал свою статью: https://habr.com/ru/articles/973594

 чем это с точки зрения кеша процессора может настолько отличаться от выполнения этого же алгоритма, но уже сразу в нативных инструкциях?

Просто тем, что тот же самый алгоритм представляется малым количеством много раз повторяемых макро-блоков машинного кода.

Без всякой P-кодной виртуальной машины можно представить себе такую же ситуацию на примере программы на Си или C++: представьте, что вы задали такие ключики компиляции, что приказали заинлайнить абсолютно всё. Инлайнятся абсолютно все функции, маленькие и большие, а также, допустим, вы как-то заставили компилятор разворачивать все циклы, где число итераций известно в момент компиляции. И плюс ещё вы отключили COMDAT Folding у линкера. Каким будет результат?

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

  • Он раздувает размер машинного кода, и это сказывается на увеличении количества страниц, и если у вас включен включен и штатным образом работает механизм подкачки, то просто может оказаться, что время от время выполнение будет переходить на страницу, котороая нет в физической памяти, которая выгружена, и ОС придётся её подгружать с диска, а это медленно. Естественно, это происходит только при первом обращении к странице, но если в системе много одновременно работаюших приложений, большой объём суммарно выделенных страниц памяти, большой объём файла подкачки (или файлов подкачки — их может быть несколько), но по сравнению с этим малый объём физической памяти, то конкуренция за страницы ФП может быть большой и та же страница в скором времени опять может быть выгружена и всё повторится при следующем обращении. И

  • Он просто раздувает код и заставляет появляться там повторяющимся паттернам машинных инструкций, которые процессор вынужден декодировать каждый раз как в первый раз, хотя он мог бы взять это в uop-кеше. Когда я говорю «каждый раз как в первый раз», я не имею в виду повторные вызовы того же самого кода, я имею в виду что если у вас развернулся цикл из 1024 итераций в «китайский код», то тело цикла будет декодироваться 1024 раза. Опять же, когда дело дойдёт того следующего раз, когда этот код должен будет выполниться, не факт, что он к этому моменту сохранится в кешах процессора. В системе ведь может быть ещё куча других процессов, и все конкурируют за кеши.

Вот в статье я приводил в качестве примера функции GetMinAndMax и Fact, показывая, что при компиляции в машинный код их код достаточно рационально компактен и ничем не уступает аналогиному сишному коду. Естественно, эти примеры взяты не с потолка: когда я пишу код (хоть на VB, но на C и C++), у меня в голове уже есть представление, во что это скомпилируется или примерно может скомпилироваться. Точно так же, как я могу выбрать удачный пример, я могу выбрать и анти-пример:

Public Function Heavyproc(ByVal FirstName, ByVal LastName, _
                          ByVal Age, ByVal Occupation, _
                          ByVal Address, ByVal SSN)

End Function

Абсолютно пустая функция, ничего не делающая, но аргументы типов — Variant и они передаются ByVal, а под капотом Variant это 16-байтная структура, и всё усугубляется правилами владения и управления ресурсами, на которые есть «ссылки» изнутри VARIANT-структур.

Поэтому эта пустая абсолютно ничего не делающая функция компилируется вот в такой машинный код:

Heavyproc    .  55                 PUSH EBP
             .  8BEC               MOV EBP, ESP
             .  83EC 08            SUB ESP, 8
             .  68 E6104000        PUSH __vbaExceptHandler                         
             .  64:A1 00000000     MOV EAX, DWORD PTR FS:[0]
             .  50                 PUSH EAX
             .  64:8925 00000000   MOV DWORD PTR FS:[0], ESP
             .  83EC 64            SUB ESP, 64
             .  53                 PUSH EBX
             .  56                 PUSH ESI
             .  57                 PUSH EDI                                        
             .  8965 F8            MOV DWORD PTR SS:[EBP-8], ESP
             .  C745 FC C0104000   MOV DWORD PTR SS:[EBP-4], 004010C0
             .  8B35 70104000      MOV ESI, DWORD PTR DS:[<&MSVBVM60.__vbaVarDup>] 
             .  33C0               XOR EAX, EAX
             .  8D55 0C            LEA EDX, DWORD PTR SS:[EBP+C]
             .  8D4D D0            LEA ECX, DWORD PTR SS:[EBP-30]
             .  8945 E0            MOV DWORD PTR SS:[EBP-20], EAX
             .  8945 D0            MOV DWORD PTR SS:[EBP-30], EAX
             .  8945 C0            MOV DWORD PTR SS:[EBP-40], EAX
             .  8945 B0            MOV DWORD PTR SS:[EBP-50], EAX
             .  8945 A0            MOV DWORD PTR SS:[EBP-60], EAX
             .  8945 90            MOV DWORD PTR SS:[EBP-70], EAX
             .  FFD6               CALL ESI                                        
             .  8D55 1C            LEA EDX, DWORD PTR SS:[EBP+1C]
             .  8D4D A0            LEA ECX, DWORD PTR SS:[EBP-60]
             .  FFD6               CALL ESI
             .  8D55 2C            LEA EDX, DWORD PTR SS:[EBP+2C]
             .  8D4D E0            LEA ECX, DWORD PTR SS:[EBP-20]
             .  FFD6               CALL ESI
             .  8D55 3C            LEA EDX, DWORD PTR SS:[EBP+3C]
             .  8D4D C0            LEA ECX, DWORD PTR SS:[EBP-40]
             .  FFD6               CALL ESI
             .  8D55 4C            LEA EDX, DWORD PTR SS:[EBP+4C]
             .  8D4D 90            LEA ECX, DWORD PTR SS:[EBP-70]
             .  FFD6               CALL ESI
             .  68 7C174000        PUSH 0040177C
             .  EB 0A              JMP SHORT 0040175C
             .  8D4D B0            LEA ECX, DWORD PTR SS:[EBP-50]
             .  FF15 10104000      CALL DWORD PTR DS:[<&MSVBVM60.__vbaFreeVar>]    
             .  C3                 RETN
             >  8B35 10104000      MOV ESI, DWORD PTR DS:[<&MSVBVM60.__vbaFreeVar>]
             .  8D4D E0            LEA ECX, DWORD PTR SS:[EBP-20]
             .  FFD6               CALL ESI                                        
             .  8D4D D0            LEA ECX, DWORD PTR SS:[EBP-30]
             .  FFD6               CALL ESI
             .  8D4D C0            LEA ECX, DWORD PTR SS:[EBP-40]
             .  FFD6               CALL ESI
             .  8D4D A0            LEA ECX, DWORD PTR SS:[EBP-60]
             .  FFD6               CALL ESI
             .  8D4D 90            LEA ECX, DWORD PTR SS:[EBP-70]
             .  FFD6               CALL ESI
             .  C3                 RETN
             .  8B45 08            MOV EAX, DWORD PTR SS:[EBP+8]                   
             .  8B55 B0            MOV EDX, DWORD PTR SS:[EBP-50]
             .  8BC8               MOV ECX, EAX
             .  8911               MOV DWORD PTR DS:[ECX], EDX                     
             .  8B55 B4            MOV EDX, DWORD PTR SS:[EBP-4C]
             .  8951 04            MOV DWORD PTR DS:[ECX+4], EDX                   
             .  8B55 B8            MOV EDX, DWORD PTR SS:[EBP-48]
             .  8951 08            MOV DWORD PTR DS:[ECX+8], EDX                   
             .  8B55 BC            MOV EDX, DWORD PTR SS:[EBP-44]
             .  5F                 POP EDI                                         
             .  8951 0C            MOV DWORD PTR DS:[ECX+C], EDX                   
             .  8B4D F0            MOV ECX, DWORD PTR SS:[EBP-10]
             .  5E                 POP ESI                                         
             .  64:890D 00000000   MOV DWORD PTR FS:[0], ECX
             .  5B                 POP EBX                                         
             .  8BE5               MOV ESP, EBP
             .  5D                 POP EBP                                         
             .  C2 5400            RETN 54

Вот тебе и абсолютно пустая функция!

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

Пролог делает:

  1. Установка EBP для нового фрейма.

  2. Добавляет новый SEH-хендлер в цепочку.

  3. Резервирует место под семь локальных переменных типа VARIANT — шесть под копии аргументов и одну под переменную, хранящую возвращаемое значение функции до непосредственно самого возврата из функции (это одноимённая переменная Heavyproc с именем таким же, как у своей родительской функции).

  4. Зануляет первое поле .vt у каждой структуры, чтобы структура считалась инициализированной (и при этом пустой).

  5. Шесть раз вызывает __vbaVarDup для копирования переданного аргумента в локальную переменную-копию.

Эпилог делает обратное:

  1. Шесть раз вызывает __vbaFreeVar для зачистки VARIANT-структуры с освобождением ресурсов, которыми она могла владеть.

  2. Вручную перемещает начинку Heavyproc в предоставляемый вызывающей стороной приёмник возвращаемого значения.

  3. Восстанавливает неизменяемые регистры (EDI, ESI, EBX), убирает SEH-фрейм.

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

Компилятор догадался соптимизироваь вызов __vbaVarDup, поместив адрес вызываемой функции в ESI и делая потом только call esi, но аргументы для вызова (а __vbaVarDup использует fastcall) вычисляются (с помощью LEA) каждый раз в индивидуальном порядке.

И этого всего лишь 6 аргументов (а могло бы быть 26) на фоне абсолютно пустой функции.

Если же скомпилировать тот же VB-код в P-code, то функция Heavyproc будет состоять всего лишь из 7 P-кодных инструкций:

FDupVar ...
FDupVar ...
FDupVar ...
FDupVar ...
FDupVar ...
FDupVar ...
ExitProcCb ...

Здесь и проявляется разница: когда виртуальная машина 6-раз выполняет VM-ную инструкцию FDupVar, она шесть раз выполнят один и тот же макро-блок машинного кода, который уже давно сидит как просто в L1I-кеше (и эти инструкции не надо дёргать из памяти), так и в uop-кеше (и процессору даже не надо заново их декодировать). И поскольку это довольно часто используепмые P-кодные инструкции, вызываемые практически из любой процедуры VB-кода, то их машинные имлементации никогда не вылезают из кешей, и страницы памяти, на которые приходятся эти имплементации в машинном коде, тоже вряд ли выкидываются из working set'а.

Сама имплементация P-кодной инструкции FDupVar в виртуальной машине выглядит вот так:

По сути тут тот же самый вызов __vbaVarDup, который шесть раз делался из native code в примере выше. Только там это было 6 раз разных, подряд идущих вызовов, а здесь это один и тот же вызов, повторяемый шесть раз как бы в цикле.

Цикл образуется последней инструкцией (JMP, подсвеченный жёлтеньким). Во это вот mov al, [esi+4] и jmp [tblByteDisp + eax*4] это фетч новой P-код инструкции и переход на её хендлер. Когда этот кусочек машинного кода закончит обработку первой P-кодной инструкции FDupVar, он дойдёт до конца и вот этот жёлтый JMP перенесёт нас обратно на lblEX_FDupVar — и так 6 итераций в общей сложности.

И да, у инструкции FDupVar два операнда размером WORD (откуда и куда копировать Variant-значени в виде смещений относительно базы текущего стекового фрейма) и поэтому в хендлере этой инструкции (т.е. в этом кусочке машинного кода, что на картинке выше) есть два обращения к памяти (а в native коде их как бы нет, те же самые смещения вшиты в операнды LEA-инструкций). Однако, по всей видимости, эти обращения обходятся супер-дёшево, потому что тут операнды идут вплотную к опкодам FDupVar-инструкции, при «фетчинге» P-кода сразу вся кеш-линия попадает в кеш данных.

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

Но если совсем невмоготу, можно было перевести как «Сборка». А так, очевидно, перевод на картинке вообще был сделан тупо засовыванием строк в ПРОМТ без понимания им контекста, в котором употребляются фразы.

Все так, не обращайте внимание. Нынче и за длинное тире линчуют, потому что якобы так делают только LLM.

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

Это просто у меня депрессия, или статья состоит из двух типов новостей: печальных новостей и якобы хороших новостей, от которых гнетущий эффект не меньше, чем от первых?

В байт-коде не нужно пересчитывать все переходы внутри программы,

С чего это ради? Эти слова косвенно говорят о том, что вы не понимаете или неправильно понимаете то, как устроен P-код VBVM и его выполнение.

Точно так же, как и в машинном коде, в P-коде все условные и безусловные переходы внутри процедуры нужно пересчитывать, если что-то в ней перекраивалось. С P-кодом даже больше нужно сделать: если в x86-инструкциях условных и безусловных переходов операнд закодирован как относительное смещение целевого адреса (относительно начала следующей инструкции — следующей по отношению к Jxx), так что целый блок x86-инструкций можно двигать, и все Jxx инсрукции в нём будут position independant, если не ведут за пределы блока), что в P-code все jump destination указываются не относительно следующей инструкции, а относительно начала процедуры (первой P-code инструкции процедуры). И VB IDE всё это делает при перекраивании: пересчитывает, правит.

А если вы переменную добавили в выполняющейся подпрограмме в машинном коде - ещё и фрейм стека перестраивать, и всё над ним двигать?

Всё то же самое актуально и для P-кода. Виртуальная машина VB стековая, буквально для всех действий сначала нужно что-то положить на стек: для вычисления выражений, сколько угодно сложным бы оно ни было, операнды кладутся на стек а затем вызываются инструкции, соответствующие операциям (и результат после выполнения такой инструкции тоже остаётся на вершине стека), при вызове процедур параметры складываются на стек и так далее. При этом какой-то свой отдельный обособленный стек не используется. Используется основной стек = процессорный стек, с верхушкой в ESP.

И таким образом добавление новой локальной переменной в уже выполняющейся в данной момент процедуре потребовалало бы перекраивания фрейма для вклинивания туда новой локальной переменной.

И тем не менее, VB IDE позволяет остановиться прямо внутри процедуры, добавить несколько новых локальных переменных, вставив их объявления в код, и продолжить выполнение (помимо добавления новых переменных можно добавить и код, произвольным образом их использующий).

Делать это можно и в том числе, если, например, мы стопнулись внутри рекурсии на каком-нибудь 10-ом уровне вложенности. Так что у нас не только процедура «Test» прямо сейчас находится в середине своего выполнения, но и вызвана она из Test, а Test вызвана из Test — и так 10 уровней. И добавлять новые локальные переменные — можно. И они появятся в каждом фрейме этой рекурсии с 10 уровнями вложенности.

Для этого VB IDE не перекраивает стековый фрейм (или стековые фреймы, в случае с рекурсией). Всё гораздо проще. Для этого просто изначально в фрейме есть резервное место на случай добавления новых переменных в процессе отладки. Примерно 150 байт на случай, если внезапно захочется приостановить выполнение и понавставлять везде новых локальных переменных. Это очень большой запас: 30 с чем-то локальных переменных типа Long/String/Object/Single. Я ни разу при правке кода по живому не упирался в этот лимит: обычно за раз хочется добавить переменных никак не больше, чем пальцев на руке.

И в Delphi тоже так могли бы сделать. Сколько держать резерва на случай появления новых локальных переменных в процессе работы программы могли бы сделать кастомизируемым — для тех, у кого в планах запускать оргромные по вложенности рекурсии и есть опасение, что стека не хватит.

Отдельно хочется сказать, что от такой локализации интерфейса VB кровь из глаз идёт.

Особенно мне нравится вкладка «Делать»...

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

Рынок именно что компонентов для формошлёпства действительно был. А Python-то выстрелил не формошлёпством.

ChatGPT не далёк от истины. Каждой P-code-ной инструкции соответствует кусочек машинного кода виртуальной машины, имплементирующий функциональность этой инструкции. Эти кусочки очень компактны, состоят всего из нескольких машинных инструкций, они написаны на MASM-е, то есть это даже не процедуры/функции — у них нет прологов, у них нет эпилогов, то есть нет сохранения регистров (которые соглашение о вызове предписывает оставлять нетронутыми), нет восстановления регистров в конце, нет резрвирования места в стеке под локальные переменные. В них есть только сама суть, так как они написаны на ассемблере. Поэтому они очень компактны.

И поскольку они очень компактны (а также из-за того, что для реально одинаковых поддействий из таких кусочков всё-таки вызываются классические процеды), реализация самых «популярных» P-кодных инструкций очень быстро попадает в кеш процессора. P-кодый код, если там выполняюся одни и те же процедуры VB-проекта, или много циклов, попадают в кеш данных процессора.

Таким образом, для процессора выполнение большей части VB-кода превращается в исполнение уже очень хорошо знакомых (с точки зрения кеша) макро-блоков.

 (иерархическая база данных без индексов) в 1995-м ключик и загрузить нужную библиотеку - это было быстро?

Почему это без индексов? Там хеш-таблиц. Поиск в реестре весьма быстрый.

И ресурсоёмкое, и неудобное, 

Что именно ресурсоёмкое?

Если ресурсоёмким был поиск в реестре (и на этом всё), то во-первых мы выяснили, что была альтернатива. То что альтернатива не была распространённой — это не аргумент, потому что это социальный фактор, а мы о технической стороне говорим. К социальным факторам его сводить не надо, потому что сам первоначальный вопрос взывает к социальному фактору: с чего ради один подход взлетел, а второй не взлетел.

Вдобавок к этому, если кому-то надо было создавать сотни тысяч объектов в секунду, то достаточно было просто 1 раз получить объект-фабрику и потом у неё вызывать метод CreateInstance, а не сотни тысяч раз искать в реестре. Более того, я почти уверен, что внутри OLEAUT32.DLL был кеш на этот случай.

Как минимум, там напрочь отсутствует взаимодействие с какими-либо сервисами ОС, кроме файловой системы.

Ха-ха, как будто файловая система это какая-то мелочь. Если бы файловая система была невесомо быстрой, никто бы не изобретал реестр, а все опции, настройки и прочее, что обычно хранится в реестре, просто распихали бы по микроскопическим файлам файловой системы, которая такая же иерархическая, как файлы. Но нет, сделали реестр.

Да файловая система куда большее зло.

VB тогда не умел в компиляцию, как я уже писал, и не будет уметь ещё шесть лет,

Он умел, просто это компиляция не в машинный код, а в бай-код. В самом слове компиляция нет никакого намёка на машинный код, поэтому самая настоящая манипуляция — уклоняться называть компиляцию в байт-код собственно компиляцией. Не нативность цепочки инструкций по отношению к CPU даёт процессу получения этой цепочки по праву называться компиляцией, а тот факт исходный код был разобран, его синтаксис проверен, его логическая структура (вложенность блоков) верифицирована, связанность идентификаторов верифицирована, замысел программиста был переведён в последовательность макро-операций, при этом выполнены все вычисления, которые можно выполнить на стадии компиляции, элиминированы пути исполнения, который никогда не достижимы, устранены избыточные макро-операции (вроде перекладываний данных туда сюда). Так что я не принимаю больше провокационных высказываний, что он не умел в компиляцию. Я люблю native code, но в контексте данного дискурса свет на нём клином не сошёлся.

Но ко всему прочему, можно подумать, что Python тогда умел в компилцию в native code?

Там это делалось просто "New ActiveX", и дальше просто как обычный элемент управления писать.

Попробуйте написать на Delphi ActiveX-сервер с классом, экземпляр которого должен олицетворять математическое выражение или математическую функцию (от одного или нескольких переменны) и притворяться этой функцией, и чтобы это можно было использовать из скриптового языка, например из VBScript, примерно вот так:

Set math_func = CreateObject("HabrDiscussionServer.MathFunc")
math_func.Expression = "f(g,x) = (sin(g) + sqr(x)) / (g*x)"

a = math_func(1,2)
b = math_func(3.141, 25)

Чтобы не тратить время и не смещать акцент на написание эвалюатора матвыражений, можете вообще не писать эту часть, а сделать заглушку, которая будет просто возвращать сумму всех аргументов вместо вычисления user-defined выражения. Вопрос не в том, где проще написать эвалюатор, а именно в ActiveX-специфичных вещах.

Сравним компактность и простоту реализаций на VB и на Delphi.

Не было, т.к. это всё-таки не интерпретатор :Р.

Так и VB IDE не интерпретировала исходный текст, а выполняла заранее скомпилированный байт-код, но при внесении правок в исходный код на живую в процессе выполнения перекраивала этот байт код, если это было в принципе осуществимо.

При желании авторы Delphi могли бы сделать такое же перекраивание машинного кода. Например, если я взял и вырезать строчку с вызовом функции в процессе пошаговой отладки программы, взять и заNOP-ить её на уровне машинного кода. Или если я что-то добавил, пропатчить код с помщью jmp и внести недостающие инструкции.

Нечто с очень ресурсозатратным связыванием

Господи... ужас... шок... фейспалм.

Неужели вслед за статьёй что QB и VB не был интерпретируемым языком, а генерировали настоящий полноценный машинный код, нужно писать следующую, с разоблачением новой байки?

С другой стороны, у вашего ответа есть ещё и второй дно: даже если бы всё что вы пишите, было правдой (а оно не было), то удивительно, что в сравнении с Python у последнего всё так же, если не хуже.

Но вернёмся к очень ресурсозатратному связыванию. Я не уверен, что именно вы понимаете под связыванием, но, в любом случае, «очень ресурсозатратное связывание» существует только в вашем воображении.

  • Если вы про то связывание, которое имеет место в момент вызова метода у объекта, то вопреки тому, что вы там себе придумали, вызов любого метода COM-объекта НИЧЕМ НЕ ОТЛИЧАЕТСЯ от вызова метода C++-класса, объявленного с ключевым словом virtual, за исключением использования соглашения stdcall вместо thiscall (указатель на this передаётся не в регистре ECX, а первым параметром на стеке).

    То есть буквально Something.Foo() в VB ничем не отличается от Something->Foo в С++, при условии, что Foo объявлена как
    virtual __stdcall returntypename Foo() = 0;

    На выходе будет сгенерирован код типа
    mov eax, dword [Something]
    mov ecx, dword [eax] ; Получаем адрес vtbl
    push eax ; Пушим this неявным первым аргументом
    call [ecx+nnnnn] ; Вызываем метод foo, где nnnn — смещение в vtbl


    Ну и каким боком это связывание очень ресурсозатратное или какое-то сложное?

    Это называется поздним связыванием в мире C++ (в противовес раннему связыванию, происходящему при линковке, если метод класса был не виртуальным), но в мире COM это наоборот называется ранним связыванием. А есть ещё и позднее связывание в мире COM, и вы наверное имели в виду его сложность и ресурсоёмкость. Только вот использование позднего связывания — это фича, опция, а не обязанность. Не хотите: можете не использовать его со стороны вызывающего кода (на VB). Не хотите — можете не имплементировать его на стороне реализации COM-объекта. Не обязательно вообще реализовывать механизм позднего связывания. Это в технологии ActiveX/OLE Automation ваш coclass должен быть dual-ным и поддерживать IDispatch и disp-интерфейсы для обычных COM-интерфейсов. Но никто не заставляет вас делать поддержку всего этого, если вам не нужно или обременительно.

    Ярчайший пример: библиотеки DirectX, Direct3D того времени (6, 7, 8, 9). Условно возьмём библиотеки d3d9.dll —  всё взаимодействие с Direct3D осуществляется через COM-интерфейсы, через работу с этими объектами и вызовы их методов. Это библиотека написана вообще без прицела на то, чтобы её использовали из VB. Создатели Half-Life 2, например, писали не на VB и использовали Direct3D, равно как и создатели сотен других игр.

    Тем не менее, использовать её VB легко и просто, равно как и любую другую подобную библиотека, будь она написана в том же духе.

  • Если же под связыванием подразумевается то, что Microsoft называет термином «активация» — то есть установление связи между COM-клиентом (вызывающей стороной) и COM-серверов, то и здесь всё не так, как вам хочется думать.

    В рамках COM не установлено и не регламентируется, как вы найдёте DLL-библиотеку, реализующую COM-объект, как с ней провзаимодействуете и как получите указатель на COM-объект. Как хотите, так и делайте.

    Это в рамках технологии ActiveX, чтобы не было бардака и разнобоя, установили единый концепт и единый способ (чтобы серверы регистрировали в реестре свои классы по их уникальным идентификаторам CLSID, а также регистрировали маппинг между человеко-читаемыми идентификаторами ProgId и 128-битными числовыми CLSID-ами, а также редактировали бы для серверов местоположение TLB и т.д. Просто чтобы каждый программист не изобретал велосипед и чтобы все программы использовали единый отлаженный механизм (подразумевающий поиск в реестре, который вы конечно же делаете не сами).

    Но это, опять же, как и в случае с поздним связыванием через IDispatch, по вашему желанию. Не хотите — не надо. Вот библиотека d3d9.dll не зарегистрирована в реестре. Ей не нужно регистрироваться в реестре. Она просто лежит в system32 и экспортирует обычную функцию Direct3DCreate9(). Вы импортируете эту функцию, вызываете, и вуаля — увас указатель на COM-объект. Точнее указатель на COM-интерфейс IDirect3D9 некоего объекта, олицетворяющего всю библиотеку Direct3D. Работаете сним, вызывайте его методы, и на этом будет построено всё ваше взаимодействие с Direct3D.

    Для свой собственной библиотеки вы можете делать то же самое. Экспортируйте функцию, которая вернёт корневой объект вашей библиотеки, а вызыващая сторона пусть дёрнет эту функцию и дальше работает с корневым объектом. Вы можете назвать функцию как угодно. Вы можете назвать библиотеку как угодно. Вы можете положить её куда угодно (вовсе не обязательно класть её в system32). Лишь бы вызывающая сторона могла дёрнуть из неё экспортируемую функцию.

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

    Но и тут можно пойти дальше. Я не понимаю, с чего ради способ этой активации считается каким-то ресурсоёмким.

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

    Для этого системная реализация CoCreateInstance делает пару простых вещей:
    1. Вызывает CoGetClassObject, чтобы получить ссылку на объект, олицетворяющий сам класс (экземпляр которого вы намереваетесь создать)
    2. Запрашивает у этого объекта интерфейс IClassFactory и у него вызывает метод CreateInstance.

    Только и всего. Сама же CoGetClassObject должна как-то находить DLL-библиотеку, внутри которой реализован не только сам класс, но и отдельный вспомогательный объект, олицетворяющий этот класс. Для этого CoGetClassObject делает несколкьо простых вещей:
    1. Ищет в реестре по предоставленному уникальному идентификатору класса ключик, содержащий полный путь к DLL-файлу.
    2. Вызывает LoadLibrary(), чтобы загрузить этот DLL-файл.
    3. У загруженной DLL вызывает экспортируемую оттуда функцию DllGetClassObject — и возвращает ссылку на объект, который отдала DLL-библиотека.

    Ничего тут нет ресурсоёмкого. В отличие от работы с обычной DLL-библиотекой, пусть даже не COM-библиотекой, разница только в том, что имя/расположение DLL-файла не жёстко захардкожено, а ищется в реестре, а затем, вызывается конкретно определённая функция, чтобы через неё выйти на фабрику класса и запросить создания нового экземпляра нового класса.

    В итоге создание нового экземпляра это:
    1. Получение полного имени файла из реестра.
    2. hmod = LoadLibrary(..._
    3. funcptr_GetClassObject = GetProcAddress(hmod, "DllGetClassObject")
    4. IClassFactory* cf = funcptr_GetClassObject( ... CLSID ...)
    5. cf->CreateInstance(...)

    И всё это обычные вызовы в духе Си/Си++.

    Я уверен на миллион, что это менее ресурсоёмко, чем вещи, которые происходят под капотом Python, когда он встречает строку
    from fooo.baar.baaz import yetanothershit

    Так что слова о том, что там делается «нечто очень ресурсозатратное» вообще не в тему.

    Но второй упрёк был в том, что первый пункт этого списка требует, чтобы в реестре были соответствующие ключики (чтобы по CLSID найти DLL файл). В общем-то да, требует, и это логичная мера, придуманная Microsoft, чтобы побороть DLL hell. Не понятно, почему нужно бояться регистрации DLL в файле.

    НО ЕСЛИ ВАМ ПО КАКОЙ-ТО ПРИЧИНЕ НЕ НРАВИТСЯ РЕГИСТРАЦИЯ В РЕЕСТРЕ — не делайте её. Механизм порождения нового экземпляра класса полностью документирован. Все вот эти шаги из 5-пунктного списка выше полностью были документированы. Если вы хотите использовать CoCreateInstance для быстрого и лёгкого создания нового объекта, то ей нужно, чтобы в реестре был ключик. Но если вам религиозные соображения, внутренние страхи или другие объективные факторы не позволяют прибегнуть к этой регистрации в реестре, просто делайте сами вручную эти 5 действий, только в качестве первого пункта вместо поиска в реестре делайте что-то другое. Напишите свой вариант CoCreateInstance, которая вместо поиска нужной DLL в реестре ищет её где-то в ином месте. В ini-файле, в файле MyCoolAppLibrariesPaths.txt, либо пусть там будет захардкожено имя библиотеки, а библиотека лежит рядом с программой. И всё, проблема решена.

    Для вашего удобства и искоренения проблемы DLL hell вам предложили способ с реестром, с централизованным местом регистрации всех ActiveX-серверов, но если вам это не нравится, порождайте объекты по своему, как вам нравится. Механизм порождения объектов не засекречен и документирован.

    Но есть вариант ещё покруче. Сколько лет как вышла Windows XP? Начиная с Windows XP придуман способ показать COM, в каком же DLL-файле живёт тот или иной класс (по его CLSID) не прибегая к созданию каких бы то ни было ключей в реестре. А значит не нужен доступен в реестр, не нужна инсталляция, не нужны админские права (они в общем-то и для регистрации в реестре не особо нуждны, потому что в HKEY_CURRENT_USER есть свой под раздел profile-specific регистраций.

    Суть способа — манифесты. Вы просто делаете XML-манифест и либо кладёте его отдельным файлом рядом со своим приложением, либо прямо в ресурсы EXE-файла его вшиваете. В манифесте нехитрыми XML-нодами указываете, что вам известны вот такие-то CLSID-ы и что искать их нужно в таких-то DLL-файлах.

    Всё, начиная с Windows XP, вы можете использовать стандартный способ порождения экземпляров желаемого класса по его CLSID-у (либо ProgId-у), но при CoCreateInstance не пойдёт ничего искать в реестре, если она нашла это в манифесте.

    Без регистрации. Без установки. Без админских прав. Уже четверть века этот способ доступен. И это не пасхалка, это документированный способ.

И для примера небольшой сниппет, касающийся упомянутого ранее Direct3D 9.

Вот такой сниппет:

Public Function TestThisStuff() As Long
    Dim D3D_RootObject As IDirect3D9
    
    Set D3D_RootObject = Direct3DCreate9(32)
    TestThisStuff = D3D_RootObject.GetAdapterCount()
End Function

Здесь просто создаётся корневой объект объектной модели D3D и через него мы дёргаем метод GetAdapterCount(), чтобы узнать число видеоапаптеров в системе. Можно было и какую-то примитивную 3D-игру написать типа бильярда, но смотреть на это в дизассемблере было бы не весело. Поэтому просто такой наипростейший пример.

На C++ то же самое выглядело бы вот так:

long TestThisStuff()
{
    IDirect3D9* D3D_RootObject;

    D3D_RootObject = Direct3DCreate9(D3D_SDK_VERSION /* 32 */);
    return D3D_RootObject->GetAdapterCount();
}
Я знаю, что этот код содержит баг

С C++-коде выше недостаёт вызова D3D_RootObject->Release() перед возвратом и возникает нарушение подсчёта ссылок/утечка памяти. По феншую указатель на IDirect3D9* в шаблонный враппер _com_ptr_t<>

Если скомпилировать этот VB-сниппет, то получится исполняемый файл, который просто тупо импортирует 1 функцию из D3D9.DLL:

Если посмотреть дизасм функции TestThisStuff, то получим следующее:

Не сильно-то это отличается от того, чтобы сгенерировалось для аналогичного C++-кода (только там бы автоматически не было установки хендлера исключений и копирования/освобождения ссылки на объект (за которым стоит IUnknown::AddRef и IUnknown::Release для правильного подсчёта ссылок).

Ну и что здесь происходит? Сначала просто вызывается функция из DLL, точно так же, как была бы вызвана, к примеру, функция CreateProcessA из kernel32.dll — в ответ функция возвращается ссылку на корневой объект объектной модели D3D. И сразу же у этого объекта дёргается метод GetAdapterCount, через vtbl, метод является 4-м по счёту у интерфейса IDirect3D9, поэтому его смещение в vtbl — 0x10. поэтому тут мы видим CALL [ECX+10]

Где здесь пресловутое очень ресурсоёмкое связывание? Кто мешает использовать такое же в своих приложениях или для своих библиотек?

Выводы:

  • Не хотите для своих библиотек использовать единый для всех унифицированный способ с помощью которого система сможет находить вашу DLL и делать все манипуляции, требуемые для создания нужного вам объекта — не делайте. VB сможет работать с такой библиотекой.

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

  • Либо, если вы приняли для себя факт существования Windows XP или более поздних систем, вы можете ни на уровне библиотеки не делать ничего экстраординарного, ни на уровне клиентского приложения тоже не делать ничего экстраординарного, но просто вместо регистрации в реестре положить рядом с приложением саму библиотеку и манифест-файл.

А теперь что касается двойного дна у комментария, про котрое я сказал в самом начале. Изначально я в предшествующем комментарии неаписал: что мешало наплодить миллион библиотек, как это сделали в сообществе Python? На что был ответ, что всё это ресурсо-ёмкое и вообще надо регистрировать в реестре. Вроде бы как мы выяснили, что ничего там ресурсоёмкого нет, а регистрация в реестре не единственный вариант, а имеется альтернатива. Но даже если бы всё было так, хочется спросить: а разве в Python поиск библиотека не более ресурсоёмкий? А разве в Python вызов метода класса у объекта-экземпляра — не более ресурсоёмкий? А разве в Python не нужно так же инсталлировать/регистрировать библиотеку вызовом какого-нибудь pip install?

1
23 ...

Информация

В рейтинге
109-й
Откуда
Петропавловск, Северо-Казахстанская обл., Казахстан
Зарегистрирован
Активность

Специализация

Десктоп разработчик, Инженер встраиваемых систем
Pure C
Assembler
X86 asm
Win32 API
Visual Basic
MySQL
Git
ООП
Разработка электроники
Обратная разработка