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

в 8:18, , рубрики: adobe air, Flash-платформа, flex, native extension, PureBasic, Программирование, метки: , , ,

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

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

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

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

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

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

Первым шагом стал импорт функций из билиотеки 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js