c++プラグインでSquirrelスクリプトとやり取りをする(Android)

この記事は2017年07月11日にqiitaに投稿した内容です。

環境

Unity5.6.2f1 Squirrel 3.1

概要

c++Likeなスクリプト言語Squirrelとやり取りをするテストです

Squirrelのダウンロード

以下のページで「Download」を押す https://sourceforge.net/projects/squirrel/

AndroidStudioとUnityの事前設定

以下を参照してプロジェクトを作成する プラグインでc++と連携する(Android) libunisquirrel.soとしてエクスポートする

Squirrel関連のファイル追加

AndroidのNativePluginを作成した時の「cpp」フォルダ内に「squirrel3」というフォルダを作成 ・ダウンロードしたtar.gzを展開する ・「squirrel」「sqstdlib」「include」フォルダを作成した「squirrel3」の中にフォルダ毎コピーする ・コピーしたフォルダ内の「.cpp」と「.h」以外を削除する

CMakeLists.txt

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             UniSquirrel.cpp
             #squirrel3
             squirrel3/squirrel/sqapi.cpp
             squirrel3/squirrel/sqbaselib.cpp
             squirrel3/squirrel/sqclass.cpp
             squirrel3/squirrel/sqcompiler.cpp
             squirrel3/squirrel/sqdebug.cpp
             squirrel3/squirrel/sqfuncstate.cpp
             squirrel3/squirrel/sqlexer.cpp
             squirrel3/squirrel/sqmem.cpp
             squirrel3/squirrel/sqobject.cpp
             squirrel3/squirrel/sqstate.cpp
             squirrel3/squirrel/sqtable.cpp
             squirrel3/squirrel/sqvm.cpp
             squirrel3/sqstdlib/sqstdaux.cpp
             squirrel3/sqstdlib/sqstdblob.cpp
             squirrel3/sqstdlib/sqstdio.cpp
             squirrel3/sqstdlib/sqstdmath.cpp
             squirrel3/sqstdlib/sqstdrex.cpp
             squirrel3/sqstdlib/sqstdstream.cpp
             squirrel3/sqstdlib/sqstdstring.cpp
             squirrel3/sqstdlib/sqstdsystem.cpp

)

# Specifies a path to native header files.
include_directories( Unity/ )
include_directories( squirrel3/include/ )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log
)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
                       native-lib
                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib}
)

プログラム

Squirrelの「.nut」ファイルはUTF8(bom無し)CR+LFでテストしました

UniSquirrel.cpp

#include "IUnityInterface.h"
#include <math.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
//#define SQUNICODE
#include "squirrel.h"
#include "sqstdio.h"
#include "sqstdaux.h"

static void printfunc( HSQUIRRELVM v, const SQChar* pFormat, ... );
static SQInteger print_args( HSQUIRRELVM v );
static SQInteger register_global_func( HSQUIRRELVM v, SQFUNCTION f, const char *fname );
static void CallSquirrelFunc( HSQUIRRELVM v );

extern "C"
{
    using CallbackOutputString = void(*)( const SQChar* );
    namespace
    {
        CallbackOutputString onCallbackOutputString = NULL;
    }

    UNITY_INTERFACE_EXPORT void SetCallbackOutputString( CallbackOutputString func )
    {
        onCallbackOutputString = func;
    }

    UNITY_INTERFACE_EXPORT void ExecuteScript( const SQChar* pPath )
    {
        if ( onCallbackOutputString == NULL )
            return;

        HSQUIRRELVM v;
        v = sq_open( 1024 );
        sqstd_seterrorhandlers( v );
        sq_setprintfunc( v, printfunc, printfunc );
        sq_pushroottable( v );
        //c/c++関数の登録
        register_global_func( v, print_args, "print_args" );

        if( SQ_SUCCEEDED( sqstd_dofile( v, pPath, 0, 1 ) ) )
        {
            printf("Call Failed!");
        }

        //squirrel関数の呼び出し
        CallSquirrelFunc( v );

        sq_close(v);
    }

    UNITY_INTERFACE_EXPORT void ExecuteScriptInMemory( const SQChar* pBuffer )
    {
        if ( onCallbackOutputString == NULL )
            return;

        HSQUIRRELVM v;
        v = sq_open( 1024 );
        sqstd_seterrorhandlers( v );
        sq_setprintfunc( v, printfunc, printfunc );
        sq_pushroottable( v );

        sq_compilebuffer( v, pBuffer,(int)strlen( pBuffer ) * sizeof( SQChar ), "compile", 1 );
        sq_pushroottable(v);
        sq_call(v,1,1,0);

        sq_close(v);
    }

    static void CallSquirrelFunc( HSQUIRRELVM v )
    {
        //SQInteger saveStack = sq_gettop( v );
        sq_pushroottable( v );
        sq_pushstring( v, _SC( "TestFunc" ), -1 );
        if( SQ_SUCCEEDED( sq_get( v, -2 ) ) )
        {
        }
        int n = 123;
        float f = 345.67f;
        const SQChar* s = _SC( "abcd" );
        sq_pushroottable( v );
        sq_pushinteger( v, n );
        sq_pushfloat( v, f );
        sq_pushstring( v, s, -1 );
        sq_call( v, 4, SQTrue, 0 );

        if( sq_gettype( v, -1 ) == OT_INTEGER )
        {
            SQInteger ret;
            sq_getinteger( v, -1, &ret );
            sq_pop( v, 1 );
        //    sq_settop( v, saveStack );
            char str[64] = "";
            snprintf( str, sizeof( str ) - 1, "%d", ret );
            onCallbackOutputString( str );
        }
    }

    static void printfunc( HSQUIRRELVM v, const SQChar* pFormat, ... )
    {
        va_list arglist;
        va_start( arglist, pFormat );
        SQChar buffer[ 1024 ] = _SC( "" );
        vsnprintf( buffer, sizeof(buffer), pFormat, arglist );
        va_end( arglist );

        onCallbackOutputString( buffer );
    }

    static SQInteger print_args( HSQUIRRELVM v )
    {
        SQInteger nargs = sq_gettop(v); //number of arguments
        for(SQInteger n=1;n<=nargs;n++)
        {
            printf("arg %d is ",n);
            switch(sq_gettype(v,n))
            {
                case OT_NULL:
                    onCallbackOutputString("null");
                    break;
                case OT_INTEGER:
                    onCallbackOutputString("integer");
                    break;
                case OT_FLOAT:
                    onCallbackOutputString("float");
                    break;
                case OT_STRING:
                    onCallbackOutputString("string");
                    break;
                case OT_TABLE:
                    onCallbackOutputString("table");
                    break;
                case OT_ARRAY:
                    onCallbackOutputString("array");
                    break;
                case OT_USERDATA:
                    onCallbackOutputString("userdata");
                    break;
                case OT_CLOSURE:
                    onCallbackOutputString("closure(function)");
                    break;
                case OT_NATIVECLOSURE:
                    onCallbackOutputString("native closure(C function)");
                    break;
                case OT_GENERATOR:
                    onCallbackOutputString("generator");
                    break;
                case OT_USERPOINTER:
                    onCallbackOutputString("userpointer");
                    break;
                default:
                    return sq_throwerror(v,"invalid param"); //throws an exception
            }
            onCallbackOutputString("\n");
        }
        sq_pushinteger(v,nargs); //push the number of arguments as return value
        return 1; //1 because 1 value is returned
    }

    static SQInteger register_global_func( HSQUIRRELVM v, SQFUNCTION f, const SQChar* fname )
    {
        sq_pushroottable(v);
        sq_pushstring(v,fname,-1);
        sq_newclosure(v,f,0); //create a new function
        sq_createslot(v,-3);
        sq_pop(v,1); //pops the root table
        return 0;
    }
}

/Assets/TestSquirrel.cs

using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
using System.Runtime.InteropServices;
using System.IO;
using System.Collections;

public class TestSquirrel : MonoBehaviour 
{
    [SerializeField] Text _text = null;

    [DllImport ("unisquirrel")]
    public static extern void SetCallbackOutputString( UnityAction<string> func );
    [DllImport ("unisquirrel")]
    public static extern void ExecuteScript( string path );
    [DllImport ("unisquirrel")]
    public static extern void ExecuteScriptInMemory( string buffer );

    private static TestSquirrel _instance = null;
    private void Awake()
    {
        _instance = this;
    }

    private void OnDestroy()
    {
        _instance = null;
    }

    private void OnApplicationQuit()
    {
        OnDestroy();
    }

    private void OnEnable()
    {
        SetCallbackOutputString( OnOutputString );
    }

    private void OnDisable()
    {
        SetCallbackOutputString( null );
    }

    private IEnumerator Start() 
    {
        yield return StartCoroutine( ExecuteScriptProcess( "test1.nut" ) );
        yield return StartCoroutine( ExecuteScriptInMemoryProcess( "test0.nut" ) );
    }

    private IEnumerator ExecuteScriptProcess( string fileName )
    {
        var filePath = Path.Combine(Application.streamingAssetsPath, fileName );
        var scriptData = "";

        if ( filePath.Contains( "://" ) == true )
        {
            WWW www = new WWW( filePath );
            yield return www;
            scriptData = www.text;
        }
        else
            scriptData = System.IO.File.ReadAllText( filePath );

        //persistentDataPathへコピー
        var outputPath = Path.Combine( Application.persistentDataPath, fileName );
        File.WriteAllText( outputPath, scriptData );
        while( File.Exists( outputPath ) == false )
            yield return null;

        Debug.Log( scriptData );
        ExecuteScript( outputPath );
        yield break;
    }

    private IEnumerator ExecuteScriptInMemoryProcess( string fileName )
    {
        var filePath = Path.Combine( Application.streamingAssetsPath, fileName );
        var scriptData = "";

        if ( filePath.Contains( "://" ) == true )
        {
            WWW www = new WWW( filePath );
            yield return www;
            scriptData = www.text;
        }
        else
            scriptData = File.ReadAllText( filePath );

        Debug.Log( scriptData );
        ExecuteScriptInMemory( scriptData );
        yield break;
    }

    private void OnOutputString( string message )
    {
        Debug.Log( message );
        _instance.SetText( message );
    }

    private void SetText( string message )
    {
        if( _text == null )
            return;
        _text.text = message;
    }
}

Assets/StreamingAssets/test0.nut

print("ハローワールド");

Assets/StreamingAssets/test1.nut

function TestFunc( n, f, s )
{
    print( "TestFunc:" + n + "/" + f + "/" + s );
    return 789;
}

function Main()
{
    print( "ハロー!ワールド" );
    print_args( 987, 654.32, "zxcv" );
}
Main();