Skip to content

Creating Libraries

Kameron Brooks edited this page Aug 17, 2019 · 1 revision

Creating Libraries to Extend Functionality

Libraries can be added to the CCL assembly that add new datatypes and functions. Creating a library can be a bit of work but can be really rewarding if you need custom data types in your CCL scripts. This would mean that your script has the ability to declare variables of custom datatypes and maintain compile-time type with custom objects. What does that mean?

Why Would I Want to Make a Library?

Here is a quick example:

Vanilla (no custom types):

object matrix = GetMatrix4x4();     // Gets an object reference from a method on the context called GetMatrix4x4
float x = (float)matrix.a0 + (float)matrix.a0; // have to be cast to float, compiler does not know the type

Custom Matrix4x4 Type

Matrix4x4 matrix;                 // The matrix4x4 type is recognized as part of the language and the constructor is called
float x = matrix.a0 + matrix.a0;  // Does not need to be cast, compiler knows type at compile-time

In the first example above, you can create a object by using a method on the context and then store it in a var reference. This is alright, but at compile-time, the compiler has no idea what type of object is stored in the reference. That means that you have to cast references to the type that you think they are supposed to be.

In the second example, the compiler knows how to directly work with the Matrix4x4 type, including any custom operators that work on matrix types. All the datatypes of the Matrix4x4 class are known at compile-time, no casts are needed. So this is going to be faster performance-wise and easier to work with in your scripts.


Example Library

For this example, lets say that I want to use the IntVector2 class. An IntVector2 is basically just 2 ints x and y

First, create the Library object

public class IntVector2Lib {

}

We are off to a good start.

Custom Type

Next, we need to a public sub-type to the IntVector2Lib class, this sub-type needs to inherit from the TypeDef class

public class IntVector2Lib {
   // ...
   
   // Custom Type Definition
   public class IntVector2TypeDef : TypeDef {

      // Return the system.type of the custom data type
      public override Type type
      {
         get
         {
             return typeof(IntVector2);
         }
      }

      // Return the name of the type
      public override string name
      {
          get
          {
              return "IntVector2";
          }
      }

      public override object Cast(object arg, CompilerData _internal)
      {
          // Create a function that lets you cast other types to this type
          //..
      }

      // specify a function that casts Func<IntVector2> to Func<object>
      public override Func<object> ToGenericFunction(object arg, CompilerData _internal)
      {
         if (arg.GetType() == typeof(Func<IntVector2>))
         {
            return () =>
            {
               return ((Func<IntVector2>)(arg))();
            };
         }
         return () =>
         {
            return arg;
         };

      }

      // Create the default constructor for the type
      public override Func<object> DefaultConstructor()
      {
         return () => { return new IntVector2(); };
      }     
   }

   //...

}

Now we have a custom type. When the library is loaded into the CCL assembly, all Sub-Types that implement the TypeDef class are added to the assembly. The type must specify how it behaves in the script. Each custom TypeDef must specify a default constructor, a Cast to Generic Function, a name, a type, and other cast functions to cast other objects to this type.

Great, we have the custom type IntVector2 in our CCL assembly. What now?

Now, lets create custom operators that operate on that type.


Unary Operators

We are going to create a Unary Operation and a Binary Operation. To do this, we will be using C# Attributes. Look at the syntax for creating a Unary Operation

[UnaryOperator(System.Type, Token.Type)]
public static object Function(object arg0, CompilerData cdata) {
   // ...
}

Lets examine this.

I created a static method in the IntVector2Lib class that has the signature object func(object, CompilerData) . Unary Operations must follow that format to be accepted as the correct delegate type. Above the method, there is a [UnaryOperator()] attribute. The UnaryOperator attribute takes 2 arguments, the first is a System.Type indicating the type that the operator operates on. The second, is a Token.Type argument that indicates which type of token invokes this operator.

So now lets actually make some functions. We will start with the negate function. This function will be added to the IntVector2Lib class

[UnaryOperator(typeof(IntVector2), Token.Type.Subract)]
public static object Negate(object arg0, CompilerData cdata) {

   // returns a function from of a CCL.Reference object if arg is a CCL.Reference
   // if arg0 is a function return that function
   // otherwise, if arg0 is a value, arg0Func is null
   Func<IntVector2> arg0Func = CompilerUtility.ForceGetFunction<IntVector2>(arg0);

   if(arg0Func == null) {
      IntVector2 temp = (IntVector2)arg0;
      return (Func<IntVector2>)(() => {
         return new IntVector2(-temp.x, -temp.y);
      });
   }
   else {
      return (Func<IntVector2>)(() => {
         IntVector2 temp = arg0Func();
         return new IntVector2(-temp.x, -temp.y);
      });
   } 
}

Now, you will be able to use the negate operator - on your custom type IntVector2. Once this library is loaded into the assembly, this is perfectly valid CCL code:

IntVector2 ivec; 
ivec.x = 1;
ivec.y = 1;
return -ivec;      // will return (-1,-1)

Binary Functions

That look pretty good, it is still a bit awkward that the components of the IntVector2 Have to be set independently. Lets change that by adding a custom assignment operator. I want to be able to assign an Int32[] array literal to an IntVector2 to assign both elements at once.

Look at the syntax for creating a Binary Operation

[BinaryOperator(System.Type, Token.Type, System.Type)]
public static object Function(object arg0, object arg1, CompilerData cdata) {
   // ...
}

It is the same as the UnaryOperator but the attribute is BinaryOperator. The BinaryOperator attribute takes an extra System.Type argument at the end. Again, the method must be static and the signature must be object func(object, object, CompilerData).

So lets add a new BinaryOperator to our library:

[BinaryOperator(typeof(IntVector2), Token.Type.Assign, typeof(Int32[])]
public static object Assign_Array(object arg0, object arg1, CompilerData cdata) {
   Func<IntVector2> arg0Func = CompilerUtility.ForceGetFunction<IntVector2>(arg0, cdata);
   Func<int[]> arg1Func = CompilerUtility.ForceGetFunction<int[]>(arg1, cdata);

   if(arg0Func == null && arg1Func == null) {
      IntVector2 temp0 = (IntVector2)arg0;
      int[] temp1 = (int[])arg1;
      return (Func<IntVector2>)(() => {
         temp0.x = temp1[0];
         temp0.y = temp1[1];
         return temp0;
      });
   }
   else if(arg0Func == null && arg1Func != null) {
      IntVector2 temp0 = (IntVector2)arg0;

      return (Func<IntVector2>)(() => {
         int[] temp1 = arg1Func();
         temp0.x = temp1[0];
         temp0.y = temp1[1];
         return temp0;
      });
   }
   else if(arg0Func != null && arg1Func == null) {
      int[] temp1 = (int[])arg1;
      return (Func<IntVector2>)(() => {
         IntVector2 temp0 = arg0Func();
         temp0.x = temp1[0];
         temp0.y = temp1[1];
         return temp0;
      });
   }
   else {

      return (Func<IntVector2>)(() => {
         IntVector2 temp0 = arg0Func();
         int[] temp1 = arg1Func();
         temp0.x = temp1[0];
         temp0.y = temp1[1];
         return temp0;
      });
   }
}

That was a bit verbose, but now we have added an assignment operator to the CCL assembly that takes a IntVector2 type on the left-hand side and an Int32[] on the right hand side:

IntVector2 ivec = [1, 1]; 
return -ivec;      // will return (-1,-1)