Custom Native Functionality

From Plasmaworks

Jump to: navigation, search

Contents

Overview

There are three ways to add additional native functionality and interoperability to your Plasmacore projects:

  1. By defining native methods that are called from Slag and implemented in C++ (or the native language of the current platform).
  2. By calling Slag methods from native code.
  3. By storing native data in Slag objects.

Native functionality can be "hooked in" by modifying the perform_custom_setup() method in the main file of a Plasmacore platform implementation - for example, in platforms/ios/main.mm or platforms/windows/main.cpp. These main files are intended for your use and modification - the GoGo build system will create them if they are missing, but it will never overwrite them while upgrading a project. And yes, Mac and Windows users will need to recompile Plasmacore to take advantage of custom functionality.

Preparation

Edit the main native file (main.cpp, main.mm, etc.), #include or #import whatever files you need, add the appropriate .cpp/.m/.mm files (concerned with what functionality you're adding) to the project.

perform_custom_setup()

In perform_custom_setup() you will want to do any mix of the following:

1. Call appropriate library initialization routines.

2. Add custom shutdown callbacks to clean up your libraries at program exit (not usually required):

   void my_shutdown_fn() { ... }
 
   void perform_custom_setup()
   {
     slag_add_custom_shutdown( my_shutdown_fn );
     ...
   }

3. Hook up native method implementations (see below).

Native Methods

Say that you augmented Math with the following method:

 augment Math
   native method hypotenuse( Real64 width, Real64 height ).Real64
 endAugment

You would write your native implementation like this, making sure your signature conforms to the same signature pattern so that a cross-compiled build will work with your method:

 void Math__hypotenuse__Real64_Real64()
 {
   // pop the parameters
   SlagReal64 height = SLAG_POP_REAL64();
   SlagReal64 width  = SLAG_POP_REAL64();
   SLAG_POP_REF();  // Discard the Math singleton
 
   // push the result
   SLAG_PUSH_REAL64( sqrt(width*width + height*height) );
 }

Then hook it up in perform_custom_setup():

 slag_hook_native( "Math", "hypotenuse(Real64,Real64)", Math__hypotenuse__Real64_Real64 );

Caveats

There is one common mistake that is often made while implementing native layer methods.

Forgetting to pop a parameter or push a result

Make sure that you pop each parameter off the stack, including the object context itself - so if you call a method on the Math singleton that takes an integer parameter, be sure to pop off both the integer and the Math object reference.

Forgetting to pop a parameter won't affect the VM build but it will tend to crash the cross-compiled builds.

Likewise be sure that you always push a result (if required) onto the stack before returning from the native layer.

Native Data

Slag has a special mechanism for keeping native data (such as file handles or texture info) associated with Slag objects. On the Slag-side you define a reference property of type "NativeData" which is an opaque "black box". On the native side you call SlagNativeData::create(arbitrary_pointer,delete_fn), where delete_fn is a function pointer that takes a void* as a parameter and is called when the native data object is collected. This returns a SlagObject* reference to the native data.

An easy way to use this system is to use the built-in "SlagResource" system as shown below. As a toy example we're going to implement a native data struct that stores a C string and its length.

First, define a C++ struct in the main file or a file included from the main file that extends the existing SlagResource struct, giving it any arbitrary methods and member variables you like:

 struct CStringData : SlagResource
 {
   char* data;
   int count;
 
   CStringData( const char* st )
   {
     count = strlen(st);
     data = new char[count+1];
     strcpy(data,st);
   }
 
   ~CStringData() { delete data; }
 };

Second, create a Slag wrapper class containing a NativeData reference:

 class CString
   PROPERTIES
     native_data : NativeData
 
   METHODS
     native method init( String data )
     native method get( int index ).Char
     native method count.Int32
 endClass

Next, here's a native implementation of CString::init(String) that creates a CStringData object and stores it in native_data:

 void CString__init__String()
 {
   SlagString* param_obj = (SlagString*) SLAG_POP_REF();
   SlagObject* this_ref = SLAG_POP_REF();
 
   // Retrieve the String as an ASCII string and create the CStringData.
   char* st = param_obj->to_new_ascii();
   CStringData* data = new CStringData(st);
   delete st;
 
   // Create the native data.
   SlagNativeData* native_data_obj = SlagNativeData::create( data, SlagNativeDataDeleteResource );
   
   // Set the native_data reference of the Slag object.
   SLAG_SET_REF( this_obj, "native_data", native_data_obj );
 }

And finally, here's a native implementation of CString::get(Int32) that fetches the native_data reference while casting its data to a CStringData pointer and returns the requested character:

 void CString__get__Int32()
 {
   SlagInt32 index = SLAG_POP_INT32();
   SlagObject* this_obj = SLAG_POP_REF();
 
   SLAG_GET_NATIVE_DATA( CStringData*, cstringdata, this_obj, "native_data" );
   if ( cstringdata && index >= 0 && index < cstringdata->count )
   {
     SLAG_PUSH_CHAR( cstringdata->data[index] );
   }
   else
   {
     SLAG_PUSH_CHAR(0); 
   }
 }

Add a similar implementation of CString::count() and you're done! The CStringData will be automatically deleted by the end of the program.