Native Classes

Create Go classes that can be instantiated and used from Scriptling using the Native API.

Basic Class

A class is an *object.Class structure containing methods:

import (
    "context"
    "fmt"
    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/object"
)

var PersonClass = &object.Class{
    Name: "Person",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                // args[0] is always 'self' (the instance)
                instance := args[0].(*object.Instance)

                name, _ := args[1].AsString()
                age, _ := args[2].AsInt()

                instance.Fields["name"] = object.NewString(name)
                instance.Fields["age"] = object.NewInteger(age)

                return object.None
            },
            HelpText: "__init__(name, age) - Initialize Person",
        },
        "greet": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                name, _ := instance.Fields["name"].AsString()
                return object.NewString("Hello, " + name + "!")
            },
            HelpText: "greet() - Return greeting with person's name",
        },
        "birthday": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                age, _ := instance.Fields["age"].AsInt()
                instance.Fields["age"] = object.NewInteger(age + 1)

                return object.NewString(fmt.Sprintf("Happy birthday! You're now %d", age+1))
            },
            HelpText: "birthday() - Increment age and return birthday message",
        },
        "get_info": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                name, _ := instance.Fields["name"].AsString()
                age, _ := instance.Fields["age"].AsInt()

                return object.NewStringDict(map[string]object.Object{
                    "name": object.NewString(name),
                    "age":  object.NewInteger(age),
                })
            },
            HelpText: "get_info() - Return person info as dict",
        },
    },
}

func main() {
    p := scriptling.New()

    // Register class
    p.SetVar("Person", PersonClass)

    // Use from Scriptling
    p.Eval(`
person = Person("Alice", 30)
print(person.greet())      # "Hello, Alice!"
print(person.birthday())   # "Happy birthday! You're now 31"
info = person.get_info()
`)
}

Creating Instances from Go

func main() {
    p := scriptling.New()
    p.SetVar("Person", PersonClass)

    // Create instance from Go
    instance, err := p.CreateInstance("Person", "Bob", 25)
    if err != nil {
        log.Fatal(err)
    }

    // Store instance in variable
    p.SetObjectVar("bob", instance)

    // Call methods
    greeting, _ := p.CallMethod(instance, "greet")
    fmt.Println(greeting.Inspect())  // "Hello, Bob!"

    // Use from script
    p.Eval(`
print(bob.greet())
bob.birthday()
`)
}

The init Method

The __init__ method is the constructor, called when creating a new instance:

var RectangleClass = &object.Class{
    Name: "Rectangle",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                if len(args) < 3 {
                    return &object.Error{Message: "__init__ requires instance, width, and height"}
                }
                instance := args[0].(*object.Instance)
                width, _ := args[1].AsFloat()
                height, _ := args[2].AsFloat()

                instance.Fields["width"] = object.NewFloat(width)
                instance.Fields["height"] = object.NewFloat(height)
                return object.None
            },
            HelpText: "__init__(width, height) - Initialize Rectangle",
        },
        "area": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                width, _ := instance.Fields["width"].AsFloat()
                height, _ := instance.Fields["height"].AsFloat()
                return object.NewFloat(width * height)
            },
            HelpText: "area() - Calculate area",
        },
        "perimeter": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                width, _ := instance.Fields["width"].AsFloat()
                height, _ := instance.Fields["height"].AsFloat()
                return object.NewFloat(2 * (width + height))
            },
            HelpText: "perimeter() - Calculate perimeter",
        },
    },
}

Inheritance

Base Class

var AnimalClass = &object.Class{
    Name: "Animal",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := args[1].AsString()
                instance.Fields["name"] = object.NewString(name)
                return object.None
            },
            HelpText: "__init__(name) - Initialize Animal",
        },
        "speak": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                return object.NewString("Generic animal sound")
            },
            HelpText: "speak() - Make animal sound",
        },
        "info": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := instance.Fields["name"].AsString()
                return object.NewString("Animal: " + name)
            },
            HelpText: "info() - Return animal info",
        },
    },
}

Derived Class

var DogClass = &object.Class{
    Name: "Dog",
    BaseClass: AnimalClass,  // Inherit from Animal
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := args[1].AsString()
                breed, _ := args[2].AsString()

                // Call parent __init__
                animalInit := AnimalClass.Methods["__init__"].(*object.Builtin)
                animalInit.Fn(ctx, nil, instance, object.NewString(name))

                instance.Fields["breed"] = object.NewString(breed)
                return object.None
            },
            HelpText: "__init__(name, breed) - Initialize Dog",
        },
        "speak": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                return object.NewString("Woof!")
            },
            HelpText: "speak() - Bark",
        },
        "fetch": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                name, _ := instance.Fields["name"].AsString()
                return object.NewString(name + " fetches the ball!")
            },
            HelpText: "fetch() - Fetch something",
        },
    },
}

Using Inheritance

func main() {
    p := scriptling.New()
    p.SetVar("Animal", AnimalClass)
    p.SetVar("Dog", DogClass)

    p.Eval(`
dog = Dog("Rex", "German Shepherd")
print(dog.speak())   # "Woof!" (overridden method)
print(dog.fetch())   # "Rex fetches the ball!"
print(dog.info())    # "Animal: Rex" (inherited method)
`)
}

Special Methods

Full Dunder Method Reference

Method Purpose
__init__ Constructor called when creating instances
__str__ String representation — used by str() and f-strings
__repr__ Debug representation — used by repr()
__len__ Length — used by len()
__bool__ Truthiness — falls back to __len__ if absent
__iter__ Return an iterator object
__next__ Return next value; raise StopIteration when done
__contains__ Membership test — used by in operator
__eq__ Equality (==)
__ne__ Inequality (!=)
__lt__ Less-than (<) — also used by sorted()
__gt__ Greater-than (>)
__le__ Less-than-or-equal (<=)
__ge__ Greater-than-or-equal (>=)
__enter__ Context manager entry — called by with
__exit__ Context manager exit — always called; return truthy to suppress exceptions
__getitem__ Custom indexing — used by obj[key]

All dunder methods are inherited through the class hierarchy.

__getitem__(key) - Custom Indexing

counterClass := &object.Class{
    Name: "Counter",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                instance.Fields["data"] = object.NewStringDict(map[string]object.Object{})
                return object.None
            },
        },
        "__getitem__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key := args[1].Inspect()
                data := instance.Fields["data"].(*object.Dict)
                if pair, ok := data.GetByString(key); ok {
                    return pair.Value
                }
                return object.NewInteger(0)  // Default for missing keys
            },
            HelpText: "__getitem__(key) - Get count for key",
        },
        "set": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key := args[1].Inspect()
                value := args[2]
                data := instance.Fields["data"].(*object.Dict)
                data.SetByString(key, value)
                return object.None
            },
            HelpText: "set(key, value) - Set a count",
        },
    },
}

// Enables: c[key] syntax
p.Eval(`
c = Counter()
c.set("apples", 5)
print(c["apples"])   # 5
print(c["oranges"])  # 0 (default)
`)

Note: Use object.NewStringDict() to create dicts and GetByString()/SetByString() for access. Never manipulate the internal Pairs map keys directly — they use a type-prefixed canonical format.

Properties and Static Methods

Wrap methods in object.Property or object.StaticMethod to get the same behaviour as @property and @staticmethod in Scriptling scripts.

object.Property

The Getter is called with self as the only argument when the attribute is accessed (no call parens needed from the script). Add a Setter to allow assignment:

var CircleClass = &object.Class{
    Name: "Circle",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                r, _ := args[1].AsFloat()
                instance.Fields["radius"] = object.NewFloat(r)
                return object.None
            },
        },
        "radius": &object.Property{
            Getter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    return instance.Fields["radius"]
                },
            },
            Setter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    instance.Fields["radius"] = args[1]
                    return &object.Null{}
                },
            },
        },
        "area": &object.Property{  // read-only: no Setter
            Getter: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    r, _ := instance.Fields["radius"].AsFloat()
                    return object.NewFloat(3.14159 * r * r)
                },
            },
        },
    },
}

// c = Circle(5.0)
// print(c.radius)  # 5  — no parens
// c.radius = 10    # calls setter
// print(c.area)    # read-only, assignment raises error

object.StaticMethod

The Fn is called without self. Callable on both the class and instances:

var MathClass = &object.Class{
    Name: "Math",
    Methods: map[string]object.Object{
        "square": &object.StaticMethod{
            Fn: &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    v, _ := args[0].AsFloat()
                    return object.NewFloat(v * v)
                },
            },
        },
    },
}

// Math.square(4)  # 16
// m = Math()
// m.square(4)     # 16

Classes in Libraries

Add classes to libraries via the constants map:

myLib := object.NewLibrary("counters",
    map[string]*object.Builtin{
        "create_counter": {
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                // Factory function
                return &object.Instance{
                    Class: counterClass,
                    Fields: map[string]object.Object{
                        "data": object.NewStringDict(map[string]object.Object{}),
                    },
                }
            },
            HelpText: "create_counter() - Create a new Counter",
        },
    },
    map[string]object.Object{
        "Counter": counterClass,           // Expose for direct instantiation
        "VERSION": object.NewString("1.0.0"),
    },
    "Counter utilities library",
)

p.RegisterLibrary(myLib)

// Use in Scriptling
p.Eval(`
import counters

# Use factory
c = counters.create_counter()

# Or use class directly
c2 = counters.Counter()
`)

Complete Example: HTTP Client Class

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/paularlott/scriptling"
    "github.com/paularlott/scriptling/object"
)

var HTTPClientClass = &object.Class{
    Name: "HTTPClient",
    Methods: map[string]object.Object{
        "__init__": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)

                // Get optional parameters
                baseURL, _ := kwargs.GetString("base_url", "")
                timeout, _ := kwargs.GetInt("timeout", 30)

                instance.Fields["base_url"] = object.NewString(baseURL)
                instance.Fields["timeout"] = object.NewInteger(int64(timeout))
                instance.Fields["headers"] = object.NewStringDict(map[string]object.Object{})

                return object.None
            },
            HelpText: "__init__(base_url='', timeout=30) - Create HTTP client",
        },
        "set_header": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                key, _ := args[1].AsString()
                value, _ := args[2].AsString()

                headers := instance.Fields["headers"].(*object.Dict)
                headers.SetByString(key, object.NewString(value))

                return object.None
            },
            HelpText: "set_header(key, value) - Set default header",
        },
        "get": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                path, _ := args[1].AsString()

                // Build URL
                baseURL, _ := instance.Fields["base_url"].AsString()
                url := baseURL + path

                // Get timeout
                timeoutSec, _ := instance.Fields["timeout"].AsInt()

                // Create client with timeout
                client := &http.Client{
                    Timeout: time.Duration(timeoutSec) * time.Second,
                }

                // Create request
                req, err := http.NewRequest("GET", url, nil)
                if err != nil {
                    return &object.Error{Message: err.Error()}
                }

                // Add headers
                headers := instance.Fields["headers"].(*object.Dict)
                for _, pair := range headers.Pairs {
                    key := pair.Key.Inspect()
                    valStr, _ := pair.Value.AsString()
                    req.Header.Set(key, valStr)
                }

                // Execute
                resp, err := client.Do(req)
                if err != nil {
                    return &object.Error{Message: err.Error()}
                }
                defer resp.Body.Close()

                body, _ := io.ReadAll(resp.Body)

                return object.NewStringDict(map[string]object.Object{
                    "status":  object.NewInteger(int64(resp.StatusCode)),
                    "body":    object.NewString(string(body)),
                    "headers": object.NewStringDict(map[string]object.Object{}),
                })
            },
            HelpText: "get(path) - Make GET request",
        },
    },
}

func main() {
    p := scriptling.New()
    p.SetVar("HTTPClient", HTTPClientClass)

    p.Eval(`
client = HTTPClient(base_url="https://api.example.com", timeout=10)
client.set_header("Authorization", "Bearer token123")
client.set_header("Content-Type", "application/json")

response = client.get("/users")
if response["status"] == 200:
    print("Success!")
    print(response["body"])
else:
    print("Error:", response["status"])
`)
}

Best Practices

1. Always Handle self

First argument is always the instance:

Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    instance := args[0].(*object.Instance)  // self
    // ... rest of implementation
}

2. Use instance.Fields for User-Visible State

Store data that scripts can read as fields:

instance.Fields["name"] = object.NewString(name)
instance.Fields["count"] = object.NewInteger(count)

3. Use instance.NativeData for Go-Only State

When your class wraps a Go value (a connection, file handle, parsed template, etc.) that scripts should never access directly, store it in NativeData instead of Fields. This avoids polluting the field namespace and removes the need for a wrapper type that implements object.Object:

type myConn struct {
    conn net.Conn
    id   string
}

var MyClass = &object.Class{
    Name: "MyClient",
    Methods: map[string]object.Object{
        "send": &object.Builtin{
            Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                instance := args[0].(*object.Instance)
                c, ok := instance.NativeData.(*myConn)
                if !ok {
                    return errors.NewError("invalid client")
                }
                msg, _ := args[1].AsString()
                c.conn.Write([]byte(msg))
                return &object.Null{}
            },
        },
    },
}

// Create instance with NativeData
func newClientInstance(conn net.Conn) *object.Instance {
    return &object.Instance{
        Class:      MyClass,
        Fields:     map[string]object.Object{},
        NativeData: &myConn{conn: conn},
    }
}

Use Fields for data scripts can read; use NativeData for internal Go state. Shallow and deep copies of an instance do not copy NativeData, so native-backed objects should be treated as handles rather than copyable data containers.

4. Return object.None for Void Methods

Methods without return values should return None:

Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    // ... implementation
    return object.None
}

5. Use Type Assertions Safely

Check types before casting:

// Safe type handling
func safeMethod(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
    if len(args) < 2 {
        return &object.Error{Message: "method requires at least 1 argument"}
    }

    instance, ok := args[0].(*object.Instance)
    if !ok {
        return &object.Error{Message: "invalid instance"}
    }

    value, err := args[1].AsString()
    if err != nil {
        return &object.Error{Message: "argument must be a string"}
    }

    // Safe to use instance and value
    instance.Fields["data"] = object.NewString(value)
    return object.None
}

Testing Classes

func TestClass(t *testing.T) {
    p := scriptling.New()

    // Create class
    counterClass := &object.Class{
        Name: "Counter",
        Methods: map[string]object.Object{
            "__init__": &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    instance.Fields["count"] = object.NewInteger(0)
                    return object.None
                },
            },
            "increment": &object.Builtin{
                Fn: func(ctx context.Context, kwargs object.Kwargs, args ...object.Object) object.Object {
                    instance := args[0].(*object.Instance)
                    count, _ := instance.Fields["count"].AsInt()
                    instance.Fields["count"] = object.NewInteger(count + 1)
                    return object.NewInteger(count + 1)
                },
            },
        },
    }

    // Register class
    p.SetVar("Counter", counterClass)

    // Test the class
    result, err := p.Eval(`
c = Counter()
c.increment()
c.increment()
result = c.increment()
`)
    if err != nil {
        t.Fatalf("Eval error: %v", err)
    }

    if value, objErr := result.AsInt(); objErr == nil {
        if value != 3 {
            t.Errorf("Expected 3, got %d", value)
        }
    }
}

See Also