Skip to content

Instance & Feature System

The core.Instance is the central object that holds all features (DNS, routing, proxy managers, etc.) and manages their lifecycle.

Instance Structure

go
// core/xray.go
type Instance struct {
    statusLock                 sync.Mutex
    features                   []features.Feature
    pendingResolutions         []resolution
    pendingOptionalResolutions []resolution
    running                    bool
    resolveLock                sync.Mutex
    ctx                        context.Context
}

The instance is essentially a dependency injection container. Features register themselves, and other features can declare dependencies that get resolved when all required features are available.

Feature Interface

Every major component implements the features.Feature interface:

go
// features/feature.go
type Feature interface {
    common.HasType   // Type() interface{}
    common.Runnable  // Start() error, Close() error
}

Feature types are identified by sentinel pointer values:

go
func ManagerType() interface{} { return (*Manager)(nil) }
func ClientType() interface{}  { return (*Client)(nil) }

Initialization Sequence

mermaid
sequenceDiagram
    participant Config
    participant Instance
    participant Feature
    participant InboundMgr
    participant OutboundMgr

    Config->>Instance: New(config)
    loop For each app config
        Instance->>Instance: CreateObject(settings)
        Instance->>Feature: AddFeature(feature)
        Feature-->>Instance: Resolve pending deps
    end
    Note over Instance: Auto-register defaults<br/>(DNS, Policy, Router, Stats)
    Instance->>Instance: InitSystemDialer(dns, obm)
    Instance->>Instance: Check: all deps resolved?
    loop For each inbound config
        Instance->>InboundMgr: AddHandler(handler)
    end
    loop For each outbound config
        Instance->>OutboundMgr: AddHandler(handler)
    end
    Note over Instance: Instance ready (not started)
    Instance->>Instance: Start()
    loop For each feature
        Instance->>Feature: Start()
    end

Key Steps in initInstanceWithConfig()

  1. App features: Iterate config.App (dispatcher, router, DNS, policy, stats, etc.), create objects via CreateObject(), register as features.

  2. Essential defaults: If DNS, Policy, Router, or Stats weren't configured, register default (no-op) implementations:

    go
    essentialFeatures := []struct{ Type, Instance }{
        {dns.ClientType(), localdns.New()},
        {policy.ManagerType(), policy.DefaultManager{}},
        {routing.RouterType(), routing.DefaultRouter{}},
        {stats.ManagerType(), stats.NoopManager{}},
    }
  3. System dialer init: Configure the global system dialer with DNS client and outbound manager (for freedom outbound DNS resolution).

  4. Dependency check: If any pendingResolutions remain unresolved, fail with error.

  5. Handlers: Add all inbound and outbound handlers.

Dependency Resolution

The RequireFeatures() mechanism enables lazy dependency injection:

go
// A component declares what it needs:
core.RequireFeatures(ctx, func(om outbound.Manager, router routing.Router,
    pm policy.Manager, sm stats.Manager, dc dns.Client) error {
    return d.Init(config, om, router, pm, sm)
})

When RequireFeatures() is called:

  1. Check if all parameter types are already registered
  2. If yes, invoke the callback immediately
  3. If no, store as a resolution in pendingResolutions
  4. Each time AddFeature() is called, re-check all pending resolutions

The resolution uses reflection to match parameter types:

go
type resolution struct {
    deps     []reflect.Type      // required feature types
    callback interface{}         // func(features...) error
}

Object Creation

CreateObject() is the factory function that converts protobuf configs into runtime objects:

go
// Called from core/config.go
func CreateObject(server *Instance, config interface{}) (interface{}, error) {
    ctx := context.WithValue(server.ctx, xrayKey, server)
    return common.CreateObject(ctx, config)
}

This calls into the config registry where each protobuf type maps to a constructor function registered via common.RegisterConfig().

The Config Registry Pattern

Every component follows this pattern:

go
func init() {
    common.Must(common.RegisterConfig((*Config)(nil),
        func(ctx context.Context, config interface{}) (interface{}, error) {
            c := config.(*Config)
            // Build runtime object from config
            return NewHandler(ctx, c)
        }))
}

The protobuf Config type is the key, and the factory function creates the runtime object. The TypedMessage wrapper (protobuf Any) provides type-safe deserialization.

Implementation Notes

To reimplement the Instance system:

  1. You need a feature registry mapping feature types to instances
  2. A dependency resolver that triggers callbacks when all deps are met
  3. A config deserializer that maps config types to constructors
  4. Feature lifecycle: registration → dependency resolution → start → close

The reflection-based approach can be replaced with explicit registration in statically-typed languages:

instance.addFeature(dispatcherFeature)
instance.addFeature(routerFeature)
// Router sees dispatcher is available, resolves its dependency

Technical analysis for re-implementation purposes.