Go Interop

Maggie runs on a Go runtime, and Go interop lets you reach into the Go ecosystem directly from Maggie code. You can wrap any Go package -- standard library or third-party -- to expose its types and functions as Maggie classes and methods. The toolchain handles introspection, code generation, and binary compilation automatically.

Go interop is useful when you need performance-critical operations, access to operating system APIs, network protocols, or any of the thousands of packages in the Go ecosystem. The generated bindings are type-safe, and the marshaling layer converts between Go and Maggie types transparently.

This chapter explains how Go interop works, from running mag wrap to using wrapped types in your Maggie code.

When and Why to Use Go Interop

Maggie is a self-contained language -- you can build entire applications without ever touching Go. But there are situations where reaching into Go makes sense:

- Performance: Go code runs as compiled native code. For CPU-bound algorithms, cryptographic operations, or data compression, calling into Go avoids interpreter overhead entirely.

- Ecosystem access: The Go standard library and third-party packages cover networking, file formats, databases, image processing, and much more. Wrapping an existing Go package is faster than reimplementing it in Maggie.

- System APIs: Low-level OS operations (sockets, file descriptors, process management) are best handled through Go's os, net, and syscall packages.

- Existing Go code: If your organization has Go libraries, you can wrap them and call them from Maggie without rewriting anything.

The tradeoff is that Go interop requires a custom binary build step. The standard mag binary does not include your wrapped packages -- you use mag build to produce a custom binary that bakes in whatever Go packages you need. Once built, the custom binary works exactly like the standard mag command, with your Go types available as Maggie classes.

A good rule of thumb: start in pure Maggie. When you find a bottleneck or need a capability that only Go provides, wrap that specific package. Keep the boundary narrow -- a thin Go layer with a rich Maggie layer on top.

The GoObject Wrapper Concept

When a Go value crosses into Maggie, the VM wraps it in a GoObject. A GoObject is an opaque handle -- Maggie code cannot inspect the raw Go memory, but it can send messages to the object, and those messages dispatch to the generated Go glue code.

Each Go type that you wrap gets a corresponding Maggie class. For example, wrapping net/http creates classes like Go::Http::Server and Go::Http::Request. Instances of these classes are GoObjects that hold a pointer to the underlying Go struct.

The key pieces of the GoObject system:

- GoObjectWrapper: Holds the Go value and a type ID that links it to its registered Maggie class.

- GoTypeRegistry: A thread-safe registry that maps Go reflect.Type values to Maggie classes. Each Go type gets a unique 16-bit type ID.

- ObjectRegistry: Stores live GoObjects and assigns each one a NaN-boxed Value that Maggie code can pass around, store in variables, and send messages to.

When you call a method on a GoObject in Maggie, the VM looks up the primitive method on the class, extracts the Go value from the wrapper, and calls the generated Go function with the unwrapped value as the receiver. The return value is marshaled back into a Maggie Value.

You never interact with GoObjectWrapper or GoTypeRegistry directly from Maggie code. They are internal VM machinery. From Maggie's perspective, a wrapped Go type is just another class with methods.

Example
"After wrapping encoding/json, you get a Go::Json class."
"Package-level functions become class methods:"
result := Go::Json marshal: myObject.

"Struct types become classes with instance methods:"
decoder := Go::Json::Decoder new: inputStream.
value := decoder decode.

Type Marshaling: Go Types to Maggie Values

The marshaling layer converts between Go types and Maggie values automatically. When a Go function returns a value, it is converted to a Maggie Value. When Maggie passes an argument to a Go function, it is converted to the expected Go type.

Go to Maggie (GoToValue):

| Go Type | Maggie Type | |-------------------|-----------------| | nil | nil | | bool | True / False| | int, int64... | SmallInteger | | uint, uint64 | SmallInteger | | float32/64 | Float | | string | String | | []byte | String | | []T | Array | | map[string]T | Dictionary | | *SomeStruct | GoObject |

Maggie to Go (ValueToGo):

| Maggie Value | Go Type | |-------------------|-----------------| | nil | nil | | True / False | bool | | SmallInteger | int64 | | Float | float64 | | String | string | | GoObject | original Go ptr |

For function parameters, the generated glue code uses more specific conversions based on the Go function signature. If a Go function expects an int32, the glue code calls int32(args[0].SmallInt()) rather than going through the generic ValueToGo path. This means you do not need to worry about type widths -- the generated code handles the conversion.

Error handling: When a Go function returns an error as its last result, the generated glue checks the error. If non-nil, it raises a Maggie exception with the message GoError: <error text>. You can catch these with on:do: in Maggie:

Example
[Go::Json marshal: badValue]
    on: Error
    do: [:ex | 'JSON encoding failed: ', ex message].

Multiple return values: Go functions that return multiple values (other than the error convention) produce a Maggie Array. A Go function returning (string, int) yields a two-element Array in Maggie:

Example
result := Go::SomePkg doWork.
name := result at: 0.
count := result at: 1.

The mag wrap Command

The mag wrap command introspects a Go package and generates two files: a Go glue file (wrap.go) and a Maggie stub file (stubs.mag). These are the bridge between Go and Maggie.

Basic usage -- wrap a single Go package:

Example
mag wrap encoding/json
mag wrap strings
mag wrap net/http

This introspects the package using Go's go/packages API, discovers all exported types, functions, and constants, and writes output to .maggie/wrap/<package>/.

Custom output directory:

Example
mag wrap encoding/json --output ./wrappers

Wrap all packages from the manifest:

Example
mag wrap

When run without arguments, mag wrap reads the [go-wrap] section from maggie.toml and wraps all configured packages.

What mag wrap generates:

For each wrapped package, two files are created:

1. wrap.go -- Go source code that registers primitives with the VM. Contains a RegisterPrimitives(v *vm.VM) function that creates Maggie classes for each Go type and wires up method dispatch.

2. stubs.mag -- Maggie class definitions with <primitive> method bodies. These are loaded at startup so the Maggie compiler knows the classes and selectors exist.

Naming conventions:

Go import paths map to Maggie namespaces under the Go:: prefix:

| Go Package | Maggie Namespace | |------------------|------------------| | strings | Go::Strings | | encoding/json | Go::Json | | net/http | Go::Http | | crypto/sha256 | Go::Sha256 |

Go function names become camelCase selectors. Go type names keep their PascalCase and are namespaced under the package:

| Go Name | Maggie Name | |----------------------|--------------------------| | json.Marshal(v) | Go::Json marshal: v | | strings.Contains() | Go::Strings contains: | | http.Server | Go::Http::Server | | http.ListenAndServe| Go::Http listenAndServe:|

Parameter limit: Functions with more than four parameters are skipped with a comment in the generated code. This is a practical limit -- Maggie keyword messages become unwieldy beyond four parts. If you need to call such a function, write a thin Go wrapper that accepts a struct or fewer arguments.

The mag build Command

After wrapping, mag build compiles a custom Maggie binary that includes the wrapped Go packages.

Basic usage (entry-point-only binary):

Example
mag build

This reads maggie.toml, runs mag wrap for all configured packages (if not already generated), then creates a custom binary at ./mag-custom. The binary loads the embedded image and runs your entry point.

Full-system binary:

Example
mag build --full

With --full, the resulting binary is a complete Maggie system -- REPL, fmt, doctest, help, LSP, and all other mag subcommands work. Your project's classes are pre-loaded in the embedded image. When invoked with no arguments, it runs your entry point:

Example
./myapp                  # Runs Main.start (your entry point)
./myapp -i               # REPL with all your classes loaded
./myapp fmt src/          # Format your source files
./myapp doctest           # Run your docstring tests
./myapp help MyClass      # Show help for your classes

This is useful for distributing a self-contained Maggie system that includes your application code. The recipient gets a single binary that can run your app, explore it interactively, or use any of the standard Maggie development tools.

Custom output path:

Example
mag build -o myapp
mag build --full -o myapp

What happens under the hood:

1. mag wrap runs for each package in [go-wrap.packages], generating wrap.go and stubs.mag files.

2. Project sources are compiled into an image (stdlib + your code).

3. Without --full: a temporary Go module is created with a minimal main.go that loads the image and runs the entry point.

4. With --full: the actual cmd/mag/ source files are copied into a temporary module, with the project image replacing the stock maggie.image. A generated project_config.go sets the default entry point and registers any wrapped Go packages.

5. go build compiles everything into a single binary.

6. The temporary module is cleaned up.

Requirements:

Generated Glue Code Walkthrough

Understanding the generated code helps when debugging or extending bindings. Here is what mag wrap strings produces (simplified).

The Go glue file (wrap.go):

Example
" -- Go side (wrap.go) -- simplified --"

"The RegisterPrimitives function creates Maggie classes and methods:"

func RegisterPrimitives(v *vm.VM) {
    "Register the namespace class for package-level functions:"
    nsClass := v.RegisterGoType("Go::Strings", reflect.TypeOf((*struct{})(nil)))

    "Register a package function as a class method:"
    nsClass.AddClassMethod(v.Selectors, "contains:substr:", ...)

    "The method body extracts Go args, calls the real function, and"
    "marshals the result back to a Maggie Value:"
    func(vmPtr interface{}, receiver vm.Value, args []vm.Value) vm.Value {
        v := vmPtr.(*vm.VM)
        arg0 := v.ValueToGo(args[0]).(string)
        arg1 := v.ValueToGo(args[1]).(string)
        result := pkg.Contains(arg0, arg1)
        return v.GoToValue(result)
    }
}

The Maggie stub file (stubs.mag):

Example
" -- Maggie side (stubs.mag) --"

namespace: 'Go'

Strings subclass: Object
  classMethod: contains: s substr: t [ <primitive> ]
  classMethod: hasPrefix: s prefix: t [ <primitive> ]
  classMethod: hasSuffix: s suffix: t [ <primitive> ]
  classMethod: join: elems sep: s [ <primitive> ]
  classMethod: repeat: s count: c [ <primitive> ]
  classMethod: replace: s old: o new: n [ <primitive> ]
  classMethod: split: s sep: t [ <primitive> ]
  classMethod: toLower: s [ <primitive> ]
  classMethod: toUpper: s [ <primitive> ]
  classMethod: trimSpace: s [ <primitive> ]

The stubs declare <primitive> methods -- the compiler sees these as method definitions but the bodies are implemented in Go. At runtime, when Maggie sends Go::Strings contains: 'hello world' substr: 'world', the VM dispatches to the registered Go function which calls strings.Contains("hello world", "world") and returns true.

Struct type bindings work similarly but register instance methods instead of class methods. The generated code extracts the Go receiver from the GoObject wrapper:

Example
" -- Go side for a struct method --"

"For a type like http.Server with a method ListenAndServe():"
serverClass.AddPrimitiveMethod(v.Selectors, "listenAndServe", ...)

func(vmPtr interface{}, receiver vm.Value, args []vm.Value) vm.Value {
    v := vmPtr.(*vm.VM)
    goVal, ok := v.GetGoObject(receiver)
    if !ok { return vm.Nil }
    self := goVal.(*pkg.Server)
    err := self.ListenAndServe()
    if err != nil {
        panic(fmt.Sprintf("Maggie error: GoError: %v", err))
    }
    return vm.Nil
}

The pattern is consistent: unwrap the receiver, convert arguments, call the Go function, convert the result. Errors from Go become Maggie exceptions via panic with the Maggie error: prefix, which the VM catches and converts to a proper exception object.

Manifest Configuration: [go-wrap]

The [go-wrap] section in maggie.toml configures which Go packages to wrap and where to put the generated code.

Minimal configuration:

Example
[go-wrap]
[[go-wrap.packages]]
import = "encoding/json"

[[go-wrap.packages]]
import = "strings"

With include filters:

Most Go packages export more than you need. The include list lets you wrap only specific functions and types, keeping the generated code small and the API surface focused.

Example
[[go-wrap.packages]]
import = "net/http"
include = ["Get", "Post", "Server", "ListenAndServe"]

[[go-wrap.packages]]
import = "encoding/json"
include = ["Marshal", "Unmarshal", "Decoder", "Encoder"]

Without include, all exported functions, types, and constants are wrapped.

Custom output directory:

Example
[go-wrap]
output = "generated/wrappers"

[[go-wrap.packages]]
import = "strings"

The default output is .maggie/wrap/. The output directory structure is:

Example
.maggie/wrap/
  json/
    wrap.go        " Go glue code"
    stubs.mag      " Maggie class stubs"
  strings/
    wrap.go
    stubs.mag
  http/
    wrap.go
    stubs.mag

Full project example:

Example
[project]
name = "web-app"
namespace = "WebApp"
version = "0.1.0"

[source]
dirs = ["src"]
entry = "Main.start"

[go-wrap]
output = ".maggie/wrap"

[[go-wrap.packages]]
import = "net/http"
include = ["Get", "Post", "Server", "ListenAndServe", "HandleFunc"]

[[go-wrap.packages]]
import = "encoding/json"
include = ["Marshal", "Unmarshal"]

[[go-wrap.packages]]
import = "strings"

With this manifest, running mag build -o web-app produces a custom binary with HTTP server, JSON encoding, and string utilities all available as Maggie classes.

Putting It All Together

Here is a complete walkthrough of adding Go interop to a project, from manifest configuration to Maggie code.

Step 1: Configure maggie.toml

Example
[project]
name = "my-app"
namespace = "MyApp"

[source]
dirs = ["src"]
entry = "Main.start"

[[go-wrap.packages]]
import = "strings"

[[go-wrap.packages]]
import = "encoding/json"
include = ["Marshal", "Unmarshal"]

Step 2: Generate bindings

Example
mag wrap

This creates .maggie/wrap/strings/ and .maggie/wrap/json/ with the generated wrap.go and stubs.mag files.

Step 3: Build the custom binary

Example
mag build -o my-app           # entry-point-only binary
mag build --full -o my-app    # full mag system with your code

Step 4: Write Maggie code that uses the wrapped packages

Example
" src/Main.mag "
namespace: 'MyApp'
import: 'Go'

Main subclass: Object
  classMethod: start [
      "Use Go strings library"
      result := Go::Strings toUpper: 'hello maggie'.
      result printString.

      "Use Go JSON encoding"
      data := Go::Json marshal: #(1 2 3).
      data printString.
  ]

Step 5: Run with the custom binary

Example
./my-app -m Main.start

The wrapped Go functions are called through the generated glue code. From the Maggie side, they look and feel like any other class methods.

Ad-hoc wrapping (without a manifest) is also supported. You can wrap a single package on the fly:

Example
mag wrap crypto/sha256 --output ./wrappers

This is handy for exploration -- see what a package looks like as Maggie classes before committing to a manifest entry.

Limitations and Tips

Go interop is powerful but has constraints worth knowing about:

Functions with more than four parameters are skipped. Maggie keyword messages with many parts are hard to read. If you need a Go function with five or more parameters, write a thin Go wrapper function that accepts a struct or fewer arguments, and wrap that instead.

Only exported Go names are wrapped. Unexported functions, types, and fields (those starting with a lowercase letter in Go) are not visible to the introspection layer and cannot be wrapped.

Pointer receivers only. For struct methods, only pointer-receiver methods are wrapped. Value-receiver methods on large structs would require copying the entire struct on each call, so they are excluded.

No callback support yet. You cannot currently pass a Maggie block as a Go callback (e.g., http.HandleFunc). Go functions that expect function-typed parameters are skipped or require manual glue code.

Custom binary required. Unlike reflection-based FFI in some languages, Go interop works by generating and compiling Go code. You must run mag build to produce a binary that includes your wrapped packages. This is a one-time step per build -- the resulting binary is self-contained.

Debugging tips:

- Run mag wrap with -v (verbose) to see what was discovered: number of functions, types, and constants found.

- Read the generated stubs.mag to see what selectors are available. This is the authoritative list of what you can call from Maggie.

- If a wrapped method returns nil unexpectedly, check the Go glue code -- the GetGoObject extraction may be failing because the receiver is not a valid GoObject.

- Go errors become GoError exceptions. Use on:do: to catch and inspect them rather than letting them crash your program.

- Use include filters in the manifest to keep the generated code focused. Wrapping an entire large package like net/http produces a lot of bindings, most of which you probably do not need.