Как создать портабельное GUI приложение на wxPerl

13 сентября 2011

Одна из проблем интерпретируемых языков заключается в зависимости скриптов от наличия у пользователя соответствующего интерпретатора и модулей, используемых в коде скрипта. В мире UNIX эти проблемы никого не беспокоят благодаря менеджерам пакетов. К сожалению, 90% наших потенциальных пользователей сидят не под UNIX.

***

Спрашивается — есть ли способ писать на скриптовых языках программы, рассчитанные на широкую аудиторию? То есть, аудиторию, большая часть которой не в состоянии скачать Strawberry Perl и поставить необходимые модули из CPAN (если речь о Perl). Очевидно, есть как минимум два возможных решения.

Первое решение заключается в том, чтобы транслировать скрипт в программу на каком-нибудь C/C++, а затем собрать ее с помощью GCC. Только в этом случае придется отказаться от некоторых возможностей языка. В Perl, например, этими возможностями будут eval(), а также модули типа Moose или Inline::*. Однако возникает вопрос — раз мы все равно не используем всей гибкости скриптовых языков, может тогда лучше сразу писать на C++ или, скажем, Haskell? У последнего, кстати, есть и компилятор, и интерпретатор.

Насколько мне известно, описанный выше подход в Perl вообще не используется. В Python напротив, соответствующих проектов в изобилии — см PyPy, Shedskin и Cython. Каждый из них заслуживает отдельной заметки, так что в рамках данного поста они не рассматриваются.

Второе возможное решение проблемы — таскать вместе с программой интерпретатор и все необходимые модули. Несмотря на свою топорность, это решение является довольно популярным и вполне рабочим. В частности, программы типа perl2exe и py2exe используют именного его. Вот об этом решении в контексте языка Perl и будет рассказано далее по тексту. Забегая вперед, скажу, что программа получится вполне приемлемого размера и будет стартовать безо всяких задержек.

***

Как нам с вами хорошо известно, пользователи Windows не сильно жалуют консольные программы. В связи с этим нам понадобятся биндинги какой-нибудь библиотеки для создания GUI. Вопросу выбора GUI библиотеки в свое время я уделил много внимания и остановился на wxWidgets. Ее и будем использовать.

Соответствующая библиотека для Perl называется wxPerl. Где ее взять? Самый простой способ — это использовать сборку Perl под названием Citrus Perl. Эта сборка отличается от Active Perl и Strawberry Perl тем, что она «из коробки» имеет очень много готовых модулей, включая wxPerl. Я пользуюсь Citrus Perl уже не первый месяц и полностью им доволен.

Устанавливается Citrus Perl не совсем обычным образом. Скачиваем установщик отсюда, запускаем cmd.exe и говорим:

citrusperl-msw-x86-5-12-r6 -d d:\coding-stuff\

Здесь d:\coding-stuff\ — это директория, куда вы хотите поставить Citrus Perl. После распаковки всех файлов говорим:

d:\coding-stuff\CitrusPerl\x86\5-12\bin\citrusreloc

Затем идем в «Компьютер → Дополнительные параметры системы → Переменные среды» и дописываем «d:\coding-stuff\CitrusPerl\x86\5-12\bin» в переменную окружения PATH. Согласен, нормальный инсталятор не помешал бы, но мы с вами программисты или где? Наконец, запускаем новый экземпляр cmd.exe и проверяем, все ли работает:

perl -v
d:\coding-stuff\CitrusPerl\x86\5-12\bin\wxperl_demo.bat

Лично я также скачал Padre и библиотеку GD:

cpan -i Padre
cpan -i PPM
ppm install http://www.bribes.org/perl/ppm/GD.ppd

Дополнение: В недавно вышедшем Citrus Perl Release 8 библиотеки GD и GD::Graph идут «из коробки».

Чтобы не писать код GUI вручную, можно воспользоваться wxGlade или wxFormBuilder. Последний не умеет генерировать Perl-код, но на CPAN доступен модуль FBP::Perl, компенсирующий этот недостаток. В wxGlade есть генератор Perl-кода, правда сама программа требует наличия Python.

***

Для этой заметки я написал небольшой скрипт, убирающий «мусор» из дизассемблерного листинга, получаемого с помощью IDA.

Пример программы на wxPerl

Код скрипта:

#!/usr/bin/perl -w --

# ida2code.pl v 0.1
# (c) Alexandr A Alexeev 2011 | http://eax.me/

use Wx 0.15 qw[:allclasses];
use strict;

package MyFrame;

use Wx qw[:everything];
use base qw(Wx::Frame);
use strict;

sub new {
  my( $self, $parent, $id, $title, $pos, $size, $style, $name ) = @_;
  $parent = undef              unless defined $parent;
  $id     = -1                 unless defined $id;
  $title  = ""                 unless defined $title;
  $pos    = wxDefaultPosition  unless defined $pos;
  $size   = wxDefaultSize      unless defined $size;
  $name   = ""                 unless defined $name;

# begin wxGlade: MyFrame::new

  $style = wxDEFAULT_FRAME_STYLE
    unless defined $style;

  $self = $self->SUPER::new( $parent, $id, $title, $pos, $size, $style, $name );
  $self->{textedit} = Wx::TextCtrl->new($self, -1, "", wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE|wxHSCROLL);
  $self->{button_1} = Wx::Button->new($self, -1, "Remove /.text:[0-9a-f]+/");

  $self->__set_properties();
  $self->__do_layout();

  Wx::Event::EVT_BUTTON($self, $self->{button_1}->GetId, \&onPressButton);

# end wxGlade
  return $self;

}


sub __set_properties {
  my $self = shift;

# begin wxGlade: MyFrame::__set_properties

  $self->SetTitle("Ida2code v 0.1 | http://eax.me/");
  $self->SetSize(Wx::Size->new(400, 300));

# end wxGlade
}

sub __do_layout {
  my $self = shift;

# begin wxGlade: MyFrame::__do_layout

  $self->{sizer_1} = Wx::BoxSizer->new(wxVERTICAL);
  $self->{sizer_1}->Add($self->{textedit}, 1, wxEXPAND, 0);
  $self->{sizer_1}->Add($self->{button_1}, 0, wxALIGN_CENTER_HORIZONTAL, 0);
  $self->SetSizer($self->{sizer_1});
  $self->Layout();

# end wxGlade
}

sub onPressButton {
  my ($self, $event) = @_;
# wxGlade: MyFrame::onPressButton <event_handler>

  # warn "Event handler (onPressButton) not implemented";
  my $text = $self->{textedit}->GetValue();
  $text =~ s#\.text:[0-9a-f]+\s##gi;
  $text =~ s#\; CODE XREF\:[^\r\n]+##sgi;
  $text =~ s#\; \-\-\-\-\-[^\r\n]+##sgi;
  $self->{textedit}->SetValue($text);
  $event->Skip;

# end wxGlade
}


# end of class MyFrame

1;

1;

package main;

unless(caller){
  local *Wx::App::OnInit = sub{1};
  my $app = Wx::App->new();
  Wx::InitAllImageHandlers();

  my $frame_1 = MyFrame->new();

  $app->SetTopWindow($frame_1);
  $frame_1->Show(1);
  $app->MainLoop();
}

Весь функционал сосредоточен в функции onPressButton — взять текст из поля ввода, прогнать через регулярные выражения и запихнуть обратно. Остальной код был сгенерирован в wxGlade.

Демонстрационный скрипт, созданный с помощью FBP::Perl, можно скачать здесь. Его я выложил так, на всякий случай.

***

У меня не нашлось лишних 150$ на perl2exe и тем более 300$ на PerlApp, так что я решил попытать счастье с модулем PAR::Packer. Программа Cava Packager тоже выглядит неплохо, но до нее у меня руки не дошли.

cpan -i PAR::Packer
pp -o test.exe test.pl

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

cpan -i Wx::Perl::Packager

Согласно документации к Wx::Perl::Packager, от нас требуется просто прописать в нужном месте

use Wx::Perl::Packager;

… после чего команда:

wxpar -o ida2code.exe ida2code.pl

… магическим образом создаст exe-шник. К сожалению, на практике все оказалось не так просто. У меня программа, созданная с помощью wxpar, выдавала такую ошибку:

Can't locate Wx/DND.pm in @INC (@INC contains: ...

Решение проблемы удалось нагуглить. Оказывается, достаточно просто заменить в скрипте строчку:

use Wx 0.15 qw[:allclasses];

… на:

use Wx;

И действительно, exe’шник, созданный из пропатченного скрипта, уже не выводит сообщений об ошибках. Он ничего не выводит. Вообще. В смысле — даже окон.

Поковыряв exe’шник и немного погуглив, я так и не нашел решения проблемы. Как я понял, она заключается в том, что упакованный скрипт не может найти модуль Win32.pm, хотя он присутствует в пакете. Ну, раз проблему не удается решить, попробуем ее обойти.

Оказывается, полученный exe’шник представляет собой самораспаковывающийся архив и его можно открыть в любом современном архиваторе. Распаковываем содержимое архива в отдельную директорию. Динамические библиотеки из подкаталога shlib кладем в «корень». Туда же копируем perl.exe, perl512.dll и libgcc_s_sjlj-1.dll из CitrusPerl\x86\5-12\bin. Открываем файл script/ida2code.pl и удаляем из него строчку:

use Wx::Perl::Packager;

Затем создаем run.bat следующего содержания:

perl script/ida2code.pl

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

Для удобства пользователя вместо bat-файла можно положить специальную небольшую программку с красивой иконкой или сделать инсталятор. Что касается размера получившегося приложения, в zip-архиве оно весит 9 Мб, а в 7z — 5 Мб, что по нынешним меркам не так уж много. Я использовал параметры сжатия по умолчанию, на случай, если на компьютере пользователя мало оперативной памяти. Следует учесть, что мы можем распространять приложение в двух вариантах — «все в одном» и «поставь Perl и нужные CPAN модули сам».

***

PS. Стоит попробовать написать архиватор, заточенный для сжатия портабельных Perl-скриптов. Интересно, даст ли обфускация CPAN-модулей ощутимое улучшение коэффициента сжатия?

PPS. И кстати, с днем программиста!

Метки: , , .


Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.