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
// 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:
// features/feature.go
type Feature interface {
common.HasType // Type() interface{}
common.Runnable // Start() error, Close() error
}Feature types are identified by sentinel pointer values:
func ManagerType() interface{} { return (*Manager)(nil) }
func ClientType() interface{} { return (*Client)(nil) }Initialization Sequence
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()
endKey Steps in initInstanceWithConfig()
App features: Iterate
config.App(dispatcher, router, DNS, policy, stats, etc.), create objects viaCreateObject(), register as features.Essential defaults: If DNS, Policy, Router, or Stats weren't configured, register default (no-op) implementations:
goessentialFeatures := []struct{ Type, Instance }{ {dns.ClientType(), localdns.New()}, {policy.ManagerType(), policy.DefaultManager{}}, {routing.RouterType(), routing.DefaultRouter{}}, {stats.ManagerType(), stats.NoopManager{}}, }System dialer init: Configure the global system dialer with DNS client and outbound manager (for
freedomoutbound DNS resolution).Dependency check: If any
pendingResolutionsremain unresolved, fail with error.Handlers: Add all inbound and outbound handlers.
Dependency Resolution
The RequireFeatures() mechanism enables lazy dependency injection:
// 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:
- Check if all parameter types are already registered
- If yes, invoke the callback immediately
- If no, store as a
resolutioninpendingResolutions - Each time
AddFeature()is called, re-check all pending resolutions
The resolution uses reflection to match parameter types:
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:
// 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:
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:
- You need a feature registry mapping feature types to instances
- A dependency resolver that triggers callbacks when all deps are met
- A config deserializer that maps config types to constructors
- 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