Interfaces
Syntax
Interface declaration:
[
modifier-list]
'interface'identifier[generic-params-decl]
[':'bases-clause]
('where'where-clause)*
'{'member-list'}'
Associated named type declaration:
associated-type-decl=
'associatedtype'identifier
[':'bases-clause]
('where'where-clause)*
';'
Parameters
modifier-listis an optional list of modifiers (TODO: link)identifieris the name of the declared interface typegeneric-params-declis an optional generic parameters declaration. See Generics (TODO).bases-clauseis an optional list of inherited interfaces.where-clauseis an optional generic constraint expression. See Generics (TODO).member-listis a list of interface members. A member is one of:var-declis astaticconstmember variable declaration of type int or bool. See Variables (TODO)associated-type-declis an associated type declaration. See below.function-declis a member function declaration. See Functions (TODO)constructor-declis a constructor declaration.property-declis a property declaration.subscript-op-declis a subscript operator declaration.function-call-op-declis a function call operator declaration.
Description
An interface specifies a set of members that a conforming type must provide. An interface can then
be used in place of a concrete type, allowing for the same code to use different concrete types via the
methods defined by the interface.
An interface consists of:
- Any number of member function prototypes, constructors, or function call operator declarations without a body. A concrete type inheriting from the interface must provide compatible member function implementations with the same names.
- Any number of member functions or function call operators with the
implementation body, which are added to the inheriting type (either a concrete
structor anotherinterface). Constructors are not allowed.- Inheriting types may override member functions by using the
overridemodifier with a compatible member function declaration.
- Inheriting types may override member functions by using the
- Any number of property or subscript operator
declarations. An interface may declare either
getorsetor both methods without the implementation body. A concrete type inheriting from the interface must provide implementations for the property and subscript operator declarations.- A property or a subscript operator declaration may be implemented with compatible
getandsetmethods as required by the interface. - Alternatively, a property may be implemented by declaring a compatible variable with a matching name.
- A property or a subscript operator declaration may be implemented with compatible
- Any number of associated named types, which a concrete inheriting type must provide. An associated named type may be provided by:
- Any number of
staticconstdata members without initializers. A concrete inheriting type must provide compatible static data members with the same names and types.- The type of a
staticconstmember must be eitherintorbool.
- The type of a
The interface member functions may be static or non-static.
An object of a type conforming to an interface can be converted to the interface type.
An interface may also inherit from another interface. The inherited members add to the inheriting interface.
A member function implementation is compatible with an interface member function when:
- The implementation function can be called with the parameter types of the interface; AND
- The implementation function return type can be converted to the interface function return type.
A member property (or variable) is compatible with an interface member property when the implementation property (or variable) is convertible to the interface property and vice versa.
Interface members may be declared with access control specifiers public or internal. The default member
visibility is the same as the visibility of the interface. See access control (TODO) for further
information.
When a structure implements an interface member requirement, the visibility of the member may not be higher than the requirement. However, it can be lower.
Example:
interface IReq
{
}
interface ITest
{
// Static data member requirement
static const int staticDataMember;
// Static member function requirement
static float staticMethod(int a);
// Property requirement
property testProp : float
{
get; // must be readable
set; // must be writable
}
// Constructor requirement
__init(float f);
// Non-static member function requirement
float someMethod(int a);
// Overridable non-static member function
// with default implementation.
float someMethodWithDefaultImplementation(int a)
{
return testProp + float(a);
}
// Function call operator requirement
float operator () (uint x, uint y);
// Subscript operator requirement
__subscript (uint i0) -> float { get; set; }
// Associated type requirement
associatedtype AssocType;
// Associated type requirement, provided type must
// conform to IReq
associatedtype AssocTypeWithRequirement : IReq;
}
struct TestClass : ITest
{
// Required data member
static const int staticDataMember = 5;
// Required static member function
static float staticMethod(int a)
{
return float(a) * float(a);
}
float propUnderlyingValue;
float arr[10] = { };
// Required constructor
__init(float f)
{
propUnderlyingValue = f + 1.0f;
}
// Required property
//
// Note that alternatively, a data member
// "float testProp;" could have also been provided.
property testProp : float
{
get
{
return propUnderlyingValue - 1.0f;
}
set(float newVal)
{
propUnderlyingValue = newVal + 1.0f;
}
}
// Required non-static member function
//
// Note that the parameters and the return value
// are not required to match as long as they are
// compatible.
float someMethod(int64_t a)
{
return float(a) * propUnderlyingValue;
}
// Required function call operator
float operator () (uint x, uint y)
{
return float(x * y);
}
// Required subscript operator
__subscript (uint i0) -> float
{
get
{
return arr[i0];
}
set
{
arr[i0] = newValue;
}
}
// Required associated type provided by using a
// type alias.
typealias AssocType = int;
// Required associated type provided by a nested type.
struct AssocTypeWithRequirement : IReq
{
}
}
📝 Remark 1: The test for an inheriting member function compatibility is equivalent to whether a wrapper function with the interface member function signature may invoke the inheriting member function, passing the parameters and the return value as is.
For example:
interface IBase { // a member function that a concrete type // must provide int32_t someFunc(int8_t a, int16_t b); } struct Test : IBase { // Implementation of IBase.someFunc(). This is // compatible because the corresponding wrapper // is well-formed (see below) int16_t someFunc(int a, int32_t b) { return int16_t(a + b); } } // A wrapper invoking Test.someFunc() with the // parameters and the return value of the interface // member function declaration int32_t IBase_wrapper_Test_someFunc( Test obj, int8_t a, int16_t b) { return obj.someFunc(a, b); }
📝 Remark 2: An interface can also be parameterized using generics.
For example:
interface ITypedReq<T> { T someFunc(T param); } struct TestClass : ITypedReq<uint> { uint someFunc(uint param) { return 123 + param; } } [shader("compute")] void main(uint3 id : SV_DispatchThreadID) { TestClass obj = { }; obj.someFunc(id.x); }See Generics (TODO) for further information on generics.
Interface-Conforming Variants
⚠️ Warning: This language feature is experimental and subject to change.
A variable declared with an interface type is an interface-conforming variant–or interface variant for short. An interface variant may have any type conforming to the interface. When an interface variant is instantiated, the following restrictions apply:
- The types conforming to the interface type may not have data members with opaque types such as
Texture2D - The types conforming to the interface type may not have data members with non-copyable types
- The types conforming to the interface type may not have data members with unsized types
Further, invoking a member function of an interface variant has performance overhead due to dynamic dispatching.
📝 Remark 1: Function parameters with interface types do not impose the above restrictions when invoked with variables with types known at compile time.
📝 Remark 2: Initializing an interface variant using the default initializer is deprecated in Slang 2025 and no longer allowed in Slang 2026. In Slang 2025, invoking a default-initialized interface variant is undefined behavior.
📝 Remark 3: In
slangc, an interface variant is said to have an existential type, meaning that its type exists such that it conforms to the specified interface.
Example
RWStructuredBuffer<int> outputBuffer;
interface IBase
{
// a member function that concrete types
// must define
int getA();
// a static const data member that concrete types
// must define
static const int8_t bias;
}
interface ITest : IBase
{
// Note: concrete types inheriting from this interface
// must define getA()
// a member function defined by the interface,
// to be overridden by ConcreteInt16
int someFunc()
{
return getA() + bias;
}
// a static member function that ConcreteInt16 and
// ConcreteInt32 must provide.
static int getUnderlyingWidth();
}
struct ConcreteInt32 : ITest
{
static const int8_t bias = 3;
int32_t a;
int32_t getA()
{
return a;
}
static int getUnderlyingWidth()
{
return 32;
}
}
struct ConcreteInt16 : ITest
{
static const int8_t bias = 1;
int16_t a;
int getA()
{
return a;
}
// override default implementation of someFunc()
override int someFunc()
{
return a + 5;
}
static int getUnderlyingWidth()
{
return 16;
}
}
// This function accepts any object conforming to
// interface ITest
int getValSquared(ITest i)
{
return i.getA() * i.getA();
}
// This function creates a concrete object and returns
// it as an interface variant.
ITest createConcrete(bool is32, uint initialValue)
{
if (is32)
return ConcreteInt32(initialValue);
else
return ConcreteInt16(initialValue);
}
[shader("compute")]
[numthreads(16, 16, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
int ret;
// Pass an object to a function
// with interface-typed argument
if ((id.x & 1) == 1)
{
ConcreteInt32 val = { id.y };
// A copy of getValSquared() is specialized for
// ConcreteInt32.
ret = getValSquared(val);
}
else
{
ConcreteInt16 val = { id.y };
// A copy of getValSquared() is specialized for
// ConcreteInt16.
ret = getValSquared(val);
}
outputBuffer[id.x] = ret;
// An interface variant. Declaring this imposes
// restrictions on the types conforming to the
// interface.
ITest iobj;
if ((id.x & 1) == 1)
{
// create and assign a ConcreteInt32 object to the
// interface variant
iobj = createConcrete(true, id.y);
}
else
{
// create and assign a ConcreteInt16 object to the
// interface variant
iobj = createConcrete(false, id.y);
}
// Note: Invoking a member function of an interface variant
// has the overhead of dynamic dispatching.
outputBuffer[id.x] += iobj.someFunc();
// Dynamic dispatch overhead also here:
outputBuffer[id.x] += iobj.getUnderlyingWidth();
}
Memory Layout and Dispatch Mechanism
The memory layout of an interface-conforming variant is unspecified. Type-based dynamic dispatching of a
member function invocation is unspecified. Both are subject to change in future versions of slangc.
Non-Normative Description of Interface-Conforming Variants
📝 Remark: The contents of this section are informational only and subject to change.
In the current implementation, the layout of an interface-conforming variant is a tagged union, conceptually as follows:
// Note: unions do not exist in Slang
union InterfaceConcreteObjectTypes
{
ConcreteType1 obj1;
ConcreteType2 obj2;
ConcreteType3 obj3;
// ...
}
struct InterfaceImplementationType
{
uint32_t typeTag;
InterfaceConcreteObjectTypes tuple;
}
However, since Slang does not have union types where the underlying data is reinterpreted as one of the union types, union types are emulated. Emulation is performed by packing/unpacking the data for a concretely-typed object to/from the underlying representation. This involves memory copies.
When the type is not known at compile-time, dynamic dispatch based on the type tag is performed to invoke member functions. Internally, this is performed as follows:
- A
switchstatement based on the type tag selects the correct type-specific implementation for the subsequent steps. - The concretely-typed object from the union is unpacked (non-static member functions only)
- The member function of the concrete type is invoked
- The concretely-typed object is packed back to the union (non-static mutating member functions only)
When the type is known at compile-time, the code using an interface type is specialized for the concrete type. This avoids the performance overhead of dynamic dispatching and union types.
Non-Normative Description of Interface-typed Function Parameters
📝 Remark: The contents of this section are informational only and subject to change.
In the current implementation, when a function with an interface-typed parameter is invoked with a type known at compile time, the function is specialized for the concrete type. This essentially creates a copy of the function with the interface-typed parameter replaced with a concrete-typed parameter.
See also Generic Functions.