- PVSM.RU - https://www.pvsm.ru -

Пишем расширение для Adobe Air на PureBasic

На волне растущей здесь популярности PureBasic [1], предлагаю ознакомится с еще одной областью применения этого языка.

Начиная с третьей версии Air, появилась возможность [2] компенсировать ограниченность SDK за счет расширений (Flash Runtime Extensions). Расширения можно писать на С/С++/Java(Android) и на любом другом языке, позволяющем создавать нативные библиотеки под соответствующие платформы.

Скорость вычислений, многопоточность, взаимодействие с операционной системой — все это доступно для расширений. Некоторое время назад я написал раcширение на С для Mac и Windows — обертку для кроссплатформенной библиотеки HIDAPI [3]. Несмотря на некоторые сложности, удобство заключалось в кроссплатформенности библиотеки, что позволило написать код расширения один раз и собрать под каждую платформу с минимальными усилиями.

PureBasic, в свою очередь, предоставляет возможность создавать приложения (dll, so, драйвера [4]) для нескольких систем. Присутствует возможность оптимизации, поддержка юникода, готовые объекты (List, Map), набор кроссплатформенных функций (алгоритмы сжатия, обработки изображений, шифрование), под Windows импортирована большая часть WINAPI, макросы и даже ООП. Все это существенно ускоряет разработку (для тех, у кого нет за плечами опыта С/С++), по сравнению с голым С.

Пример — системный модальный диалог

Исходники и демо-приложение для Windows доступны на code.google.com [5].

Первым шагом стал импорт [6] функций из билиотеки FlashRuntimeExtensions.lib (Windows, {AIR SDK}libwin), это делается сравнительно просто, при наличии FlashRuntimeExtensions.h ({AIR SDK}include):

ImportC "../lib/FlashRuntimeExtensions.lib"
;returns FRE_OK
;        FRE_WRONG_THREAD
;        FRE_INVALID_ARGUMENT If nativeData is null.
;FREResult FREGetContextNativeData( FREContext ctx, void** nativeData );
;-FREGetContextNativeData
FREGetContextNativeData.l (ctx.l, nativeData.l)
...
EndImport

В настройках компилятора необходимо указать формат Shared Dll, включить при необходимости поддержку юникода (в меню File/Format выбрать кодировку исходных файлов UTF8), потокобезопасность или поддрежку ассемблера.

Единственное неудобство — отсутствие типа unsigned long в PureBasic, для это пришлось написать дополнительные функции.

В дескрипторе расширения указываем платформу, библиотеку расширения и 2 единственные экспортируемые функции (initializer и finalizer):

<?xml version="1.0" encoding="UTF-8"?>
<extension xmlns="http://ns.adobe.com/air/extension/3.1">
    <id>com.pure.Extension</id>
    <name>pureair</name>
    <copyright>compile4fun 2012</copyright>
    <description>Extenion for Adobe AIR</description>
    <versionNumber>1.0.0</versionNumber>
    <platforms>
        <platform name="Windows-x86">
            <applicationDeployment>
                <nativeLibrary>pureair.dll</nativeLibrary>
                <initializer>initializer</initializer>
                <finalizer>finalizer</finalizer>
            </applicationDeployment>
        </platform>
    </platforms>
</extension>

Id должен соответствовать указанному в манифесте приложения и пакету расширения(ActionScript), профиль приложения — extendedDesktop.

Обязательная часть расширения это функции AttachProcess(Instance), DetachProcess(Instance), AttachThread(Instance) and DetachThread(Instance), а также:

...
;CDecl
ProcedureC contextInitializer(extData.l, ctxType.s, ctx.l, *numFunctions.Long, *functions.Long)
  *loginfo("create context: " + Str(ctx) + "=" + Utf8ToUnicode(ctxType))
  Define result.l
  ;exported extension functions count:
  Define size.l = 1 
  
  ;Array of FRENamedFunction:
  Dim f.FRENamedFunction(size - 1)
  
  ;there is no unsigned long type in PB
  setULong(*numFunctions, size)
  
  ;If you want to return a string out of a DLL, the string has to be declared as Global before using it.
  ;method name
  f(0)name = asGlobal("showDialog")
  ;function data example
  f(0)functionData = asGlobal("showDialog")
  ;function pointer
  f(0)function = @showDialog()
  *functionsl = @f()
  
  ;some additional data can be stored
  extData = #Null
  
  ;native data example
  result = FRESetContextNativeData(ctx, asGlobal("FRESetContextNativeData"))
  *logDebug(ResultDescription(result, "FRESetContextNativeData"))
  *loginfo("create context complete");
EndProcedure 

;CDecl
ProcedureC contextFinalizer(ctx.l)
  *loginfo("dispose context: " + Str(ctx))
EndProcedure 

;CDecl
ProcedureCDLL initializer(extData.l, *ctxInitializer.Long, *ctxFinalizer.Long)
  *ctxInitializerl = @contextInitializer()
  *ctxFinalizerl = @contextFinalizer()
EndProcedure 

;CDecl
;this method is never called on Windows...
ProcedureCDLL finalizer(extData.l)
  ;do nothing
EndProcedure 

Массив FRENamedFunction содержит методы нашего расширения (в данном случае всего один — showDialog), также есть возможность привязать к функции или экземпляру расширения какие-либо данные. Следует обратить внимание на тип вызова CDecl и функцию asGlobal, которая специально выделяет память для строк передаваемых из асширения в основную программу, это упоминается в справке PureBasic по работе с dll.

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

Structure MessageParameters
  text.s
  title.s
  dwFlags.l
  ctx.l
EndStructure

Procedure ModalMessage(*params.MessageParameters)
  Define result.l, code.l
  code = MessageRequester(*paramstitle, *paramstext, *paramsdwFlags)
  result = FREDispatchStatusEventAsync(*paramsctx, asGlobal("showDialog"), asGlobal(Str(code)))
  *logDebug (ResultDescription(result, "FREDispatchStatusEventAsync"))
EndProcedure

;CDecl
ProcedureC.l showDialog(ctx.l, funcData.l, argc.l, *argv.FREObjectArray)
  *loginfo("Invoked showDialog")
  Define result.l
  
  ;ActionScriptData example
  Define actionScriptObject.l, actionScriptInt.l, type.l
  result = FREGetContextActionScriptData(ctx, @actionScriptObject)
  *logDebug(ResultDescription(result, "FREGetContextActionScriptData"))
  
  result = FREGetObjectType(actionScriptObject, @type)
  *logDebug("result=" + ResultDescription(result, "FREGetObjectType"))
  *loginfo("ContextActionScriptData: type=" + TypeDescription(type))
  
  result = FREGetObjectAsInt32(actionScriptObject, @actionScriptInt)
  *logDebug("result=" + ResultDescription(result, "FREGetObjectAsInt32"))
  
  *loginfo("ContextActionScriptData: actionScriptInt=" + Str(actionScriptInt))
  
  ;function data example
  Define funcDataS.s
  funcDataS = PeekS(funcData, -1, #PB_Ascii)
  *loginfo("FunctionData: " + funcDataS)

  *loginfo("Method args size: " + Str(fromULong(argc)))

  Define resultObject.l, length.l, booleanArg.l, dwFlags.l, message.s, *string.Ascii
  
  result = FREGetObjectAsBool(*argvobject[0], @booleanArg)
  *logDebug("result=" + ResultDescription(result, "FREGetObjectAsBool"))
  
  result = FREGetObjectAsInt32(*argvobject[1], @dwFlags)
  *logDebug("result=" + ResultDescription(result, "FREGetObjectAsInt32"))
  
  result = FREGetObjectAsUTF8(*argvobject[2], @length, @*string)
  *logDebug("result=" + ResultDescription(result, "FREGetObjectAsUTF8"))
  message = PeekS(*string, fromULong(length) + 1)
  
  *loginfo("Argument: booleanArg=" + Str(fromULong(booleanArg)))
  *loginfo("Argument: dwFlags=" + Str(dwFlags))
  *loginfo("Argument: message=" + Utf8ToUnicode(message))
  
  ;native data example
  Define native.l, nativeData.s
  result = FREGetContextNativeData(ctx, @native)
  *logDebug(ResultDescription(result, "FREGetContextNativeData"))
  nativeData = PeekS(native, -1, #PB_Ascii)
  *loginfo("FREGetContextNativeData: " + nativeData)
    
  Define *params.MessageParameters = AllocateMemory(SizeOf(MessageParameters))
  *paramsctx = ctx
  *paramstitle = "PureBasic"
  *paramstext = Utf8ToUnicode(message)
  *paramsdwFlags = dwFlags
  CreateThread(@ModalMessage(), *params)
  
  ;return Boolean.TRUE
  result = FRENewObjectFromBool(toULong(1), @resultObject)
  *logDebug(ResultDescription(result, "FRENewObjectFromBool"))
  
  ProcedureReturn resultObject
EndProcedure

Со стороны Air это выглядит так:

package com.pure
{
    import flash.events.StatusEvent;
    import flash.external.ExtensionContext;
    import mx.logging.ILogger;
    import mx.logging.Log;

    /**
     * Wrapper for PureBasic extension
     */
    public class Extension
    {
        /**
         * Extension id, must be specified in air-manifest.xml and extension.xml
         */
        public static const CONTEXT:String = "com.pure.Extension";

        private static const log:ILogger = Log.getLogger(CONTEXT);

        /**
         * @private
         */
        private var _context:ExtensionContext;

        /**
         * @private
         */
        private var contextType:String;

        /**
         * Creates context
         * @param contextType default value is "PureAir"
         * @param actionScriptData any number
         */
        public function Extension(contextType:String = "PureAir", actionScriptData:int = 4)
        {
            //random type
            this.contextType = contextType + Math.round(Math.random() * 100000);
            try
            {
                log.debug("Creating context: {0}, contextType: {1}", CONTEXT, this.contextType);

                _context = ExtensionContext.createExtensionContext(CONTEXT, this.contextType);

                if (_context == null)
                {
                    //creation failed
                    log.error("Failed to create context: {0}, contextType: {1}", CONTEXT, this.contextType);
                }
                else
                {
                    log.debug("Context was created successfully");

                    //listen for extension events
                    _context.addEventListener(StatusEvent.STATUS, onStatusEvent);

                    //set actionScript data
                    _context.actionScriptData = actionScriptData;
                }
            }
            catch(e:Error)
            {
                log.error("Failed to create context: {0}, contextType: {1}, stacktrace: {2}", CONTEXT, this.contextType, e.getStackTrace());
            }
        }

        private function get contextCreated():Boolean
        {
            return _context != null;
        }

        /**
         * Test method, shows YesNoCancel modal dialog
         * @param booleanArg boolean parameter
         * @param flags integer parameter, #PB_MessageRequester_YesNoCancel=3, #MB_APPLMODAL = 0
         * @param message string parameter
         * @return
         */
        public function showDialog(booleanArg:Boolean, flags:int, message:String):Boolean
        {
            if (!contextCreated)
                return false;

            var result:Boolean = false;

            try
            {
                result = _context.call('showDialog', booleanArg, flags, message) as Boolean;
                if (!result)
                {
                    log.error("Invocation error: test({0}, {1}, {2})", booleanArg, flags, message);
                }
            }
            catch (e:Error)
            {
                log.error("Invocation error: test({0}, {1}, {2}), stacktrace: {3}", booleanArg, flags, message, e.getStackTrace());
            }
            return result;
        }

        private function onStatusEvent(event:StatusEvent):void
        {
            log.info("Status event received: contextType={0} level={2}, code={1}", this.contextType, event.code, event.level);
        }

        /**
         * Performs clean-up
         */
        public function dispose():void
        {
            if (_context)
            {
                _context.dispose();
                //clean all references
                _context.removeEventListener(StatusEvent.STATUS, onStatusEvent);
                _context = null;
                log.info("Disposed {0}", this.contextType);
            }
            else
            {
                log.warn("Can not dispose {0}: Context is null", this.contextType);
            }
        }
    }
}

StatusEvent единственный тип события, которое может передавать расширение.

Результат работы можно увидеть на скриншоте:
image

Спасибо за внимание.

Автор: kemsky


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/10763

Ссылки в тексте:

[1] PureBasic: http://habrahabr.ru/post/64457/

[2] возможность: http://www.adobe.com/devnet/air/articles/extending-air.html

[3] HIDAPI: http://www.signal11.us/oss/hidapi/

[4] драйвера: http://habrahabr.ru/post/145926/

[5] code.google.com: http://code.google.com/p/purebasic-extension-for-adobe-air/

[6] импорт: http://code.google.com/p/purebasic-extension-for-adobe-air/source/browse/trunk/pureair/native/src/FlashRuntimeExtensions.pb