Implementing a solver interface

This guide outlines the basic steps to implement an interface to MathOptInterface for a new solver.


Implementing an interface to MathOptInterface for a new solver is a lot of work. Before starting, we recommend that you join the Developer chatroom and explain a little bit about the solver you are wrapping. If you have questions that are not answered by this guide, please ask them in the Developer chatroom so we can improve this guide!


Decide if MathOptInterface is right for you

The first step in writing a wrapper is to decide whether implementing an interface is the right thing to do.

MathOptInterface is an abstraction layer for unifying constrained mathematical optimization solvers. If your solver doesn't fit in the category, i.e., it implements a derivative-free algorithm for unconstrained objective functions, MathOptInterface may not be the right tool for the job.


If you're not sure whether you should write an interface, ask in the Developer chatroom.

Find a similar solver already wrapped

The next step is to find (if possible) a similar solver that is already wrapped. Although not strictly necessary, this will be a good place to look for inspiration when implementing your wrapper.

The JuMP documentation has a good list of solvers, along with the problem classes they support.


If you're not sure which solver is most similar, ask in the Developer chatroom.

Create a low-level interface

Before writing a MathOptInterface, you first need to be able to call the solver from Julia.

Wrapping solvers written in Julia

If your solver is written in Julia, there's nothing to do here! Go to the next section.

Wrapping solvers written in C

Julia is well suited to wrapping solvers written in C.


This is not true for C++. If you have a solver written in C++, first write a C interface, then wrap the C interface.

Before writing a MathOptInterface wrapper, there are a few extra steps.

Create a JLL

If the C code is publicly available under an open-source license, create a JLL package via Yggdrasil. The easiest way to do this is to copy an existing solver. Good examples to follow are the COIN-OR solvers.


Building the solver via Yggdrasil is non-trivial. please ask the Developer chatroom for help.

If the code is commercial or not publicly available, the user will need to manually install the solver. See Gurobi.jl or CPLEX.jl for examples of how to structure this.

Use Clang.jl to wrap the C API

The next step is to use Clang.jl to automatically wrap the C API. The easiest way to do this is to follow an example. Good examples to follow are Cbc.jl and HiGHS.jl.

Sometimes, you will need to make manual modifications to the resulting files.

Solvers written in other languages

Ask the Developer chatroom for advice. You may be able to use one of the JuliaInterop packages to call out to the solver.

For example, SeDuMi.jl uses MATLAB.jl to call the SeDuMi solver written in MATLAB.

Structuring the package

Structure your wrapper as a Julia package. Consult the Julia documentation if you haven't done this before.

MOI solver interfaces may be in the same package as the solver itself (either the C wrapper if the solver is accessible through C, or the Julia code if the solver is written in Julia, for example), or in a separate package which depends on the solver package.


The JuMP core contributors request that you do not use "JuMP" in the name of your package without prior consent.

Your package should have the following structure:

    gen.jl  # Code to wrap the C API
  • The /.github folder contains the scripts for GitHub actions. The easiest way to write these is to copy the ones from an existing solver.
  • The /gen and /src/gen folders are only needed if you are wrapping a solver written in C.
  • The /src/MOI_wrapper folder contains the Julia code for the MOI wrapper.
  • The /test folder contains code for testing your package. See Setup tests for more information.
  • The .JuliaFormatter.toml and .github/workflows/format_check.yml enforce code formatting using JuliaFormatter.jl. Check existing solvers or JuMP.jl for details.

Setup tests

The best way to implement an interface to MathOptInterface is via test-driven development.

The MOI.Test submodule contains a large test suite to help check that you have implemented things correctly.

Follow the guide How to test a solver to set up the tests for your package.


Run the tests frequently when developing. However, at the start there is going to be a lot of errors! Comment out all the test_ functions but one, run the tests, implement any missing methods until the test passes, then uncomment another test and repeat.

Initial code

By this point, you should have a package setup with tests, formatting, and access to the underlying solver. Now it's time to start writing the wrapper.

The Optimizer object

The first object to create is a subtype of AbstractOptimizer. This type is going to store everything related to the problem.

By convention, these optimizers should not be exported and should be named PackageName.Optimizer.

import MathOptInterface
const MOI = MathOptInterface

struct Optimizer <: MOI.AbstractOptimizer
    # Fields go here

Optimizer objects for C solvers


This section is important if you wrap a solver written in C.

Wrapping a solver written in C will require the use of pointers, and for you to manually free the solver's memory when the Optimizer is garbage collected by Julia.

Never pass a pointer directly to a Julia ccall function.

Instead, store the pointer as a field in your Optimizer, and implement Base.cconvert and Base.unsafe_convert. Then you can pass Optimizer to any ccall function that expects the pointer.

In addition, make sure you implement a finalizer for each model you create.

If newsolver_createProblem() is the low-level function that creates the problem pointer in C, and newsolver_freeProblem(::Ptr{Cvoid}) is the low-level function that frees memory associated with the pointer, your Optimizer() function should look like this:

struct Optimizer <: MOI.AbstractOptimizer

    function Optimizer()
        ptr = newsolver_createProblem()
        model = Optimizer(ptr)
        finalizer(model) do m
        return model

Base.cconvert(::Type{Ptr{Cvoid}}, model::Optimizer) = model
Base.unsafe_convert(::Type{Ptr{Cvoid}}, model::Optimizer) = model.ptr

Implement methods for Optimizer

Now that we have an Optimizer, we need to implement a few basic methods.


For this and all future methods, read the docstrings to understand what each method does, what it expects as input, and what it produces as output. If it isn't clear, let us know and we will improve the docstrings! It is also very helpful to look at an existing wrapper for a similar solver.

You should also implement, ::Optimizer) to print a nice string when some prints your model. For example

function, model::Optimizer)
    return print(io, "NewSolver with the pointer $(model.ptr)")

Implement attributes

You also need to implement the model and optimizer attributes in the following table.

For each attribute

  • get gets the current value of the attribute
  • set sets a new value of the attribute. Not all attributes can be set. For example, the user can't modify the SolverName.
  • supports returns a Bool indicating whether the solver supports the attribute.

Each column in the table indicates whether you need to implement the particular method for each attribute.


For example:

function MOI.get(model::Optimizer, ::MOI.Silent)
    return # true if MOI.Silent is set

function MOI.set(model::Optimizer, ::MOI.Silent, v::Bool)
    if v
        # Set a parameter to turn off printing
        # Restore the default printing

MOI.supports(::Optimizer, ::MOI.Silent) = true

Define supports_constraint

The next step is to define which constraints and objective functions you plan to support.

For each function-set constraint pair, define supports_constraint:

function MOI.supports_constraint(
    return true

To make this easier, you may want to use Unions:

function MOI.supports_constraint(
    return true

Only support a constraint if your solver has native support for it.

The big decision: copy-to or incremental modifications?

Now you need to decide whether to support incremental modification or not.

Incremental modification means that the user can add variables and constraints one-by-one without needing to rebuild the entire problem, and they can modify the problem data after an optimize! call. Supporting incremental modification means implementing functions like add_variable and add_constraint.

The alternative is to accept the problem data in a single copy_to function call, afterwhich it cannot be modified. Because copy_to sees all of the data at once, it can typically call a more efficient function to load data into the underlying solver.

Good examples of solvers supporting incremental modification are MILP solvers like GLPK.jl and Gurobi.jl. Examples of copy_to solvers are AmplNLWriter.jl and SCS.jl

It is possible to implement both approaches, but you should probably start with one for simplicity.


Only support incremental modification if your solver has native support for it.

In general, supporting incremental modification is more work, and it usually requires some extra book-keeping. However, it provides a more efficient interface to the solver if the problem is going to be resolved multiple times with small modifications. Moreover, once you've implemented incremental modification, it's usually not much extra work to add a copy_to interface. The converse is not true.


If this is your first time writing an interface, start with copy_to.

The copy_to interface

To implement the copy_to interface, implement the following function:

The incremental interface


Writing this interface is a lot of work. The easiest way is to consult the source code of a similar solver!

To implement the incremental interface, implement the following functions:


Solvers do not have to support AbstractScalarFunction in GreaterThan, LessThan, EqualTo, or Interval with a nonzero constant in the function. Throw ScalarFunctionConstantNotZero if the function constant is not zero.

In addition, you should implement the following model attributes:


Variable-related attributes:


Constraint-related attributes:


If your solver supports modifying data in-place, implement:

Variables constrained on creation

Some solvers require variables be associated with a set when they are created. This conflicts with the incremental modification approach, since you cannot first add a free variable and then constrain it to the set.

If this is the case, implement:

By default, MathOptInterface assumes solvers support free variables. If your solver does not support free variables, define:

MOI.supports_add_constrained_variables(::Optimizer, ::Type{Reals}) = false

Incremental and copy_to

If you implement the incremental interface, you have the option of also implementing copy_to. If you don't want to implement copy_to, e.g., because the solver has no API for building the problem in a single function call, define the following fallback:

function MOI.copy_to(dest::Optimizer, src::MOI.ModelLike; kwargs...)
    return MOI.Utilities.automatic_copy_to(dest, src; kwargs...)

function MOI.Utilities.supports_default_copy_to(
    # If you support names...
    return true
    # Otherwise...
    return !copy_names

See Utilities.supports_default_copy_to for more details.


Regardless of which interface you implement, you have the option of implementing the Name attribute for variables and constraints:


If you implement names, you should also implement the following three methods:

function MOI.get(model::Optimizer, ::Type{MOI.VariableIndex}, name::String)
    return # The variable named `name`.

function MOI.get(model::Optimizer, ::Type{MOI.ConstraintIndex}, name::String)
    return # The constraint any type named `name`.

function MOI.get(
) where {F,S}
    return # The constraint of type F-in-S named `name`.

These methods have the following rules:

  • If there is no variable or constraint with the name, return nothing
  • If there is a single variable or constraint with that name, return the variable or constraint
  • If there are multiple variables or constraints with the name, throw an error.


Implement optimize! to solve the model:

At a minimum, implement the following attributes to allow the user to access solution information.


You only need to implement get for solution attributes. Don't implement set or supports.


Solver wrappers should document how the low-level statuses map to the MOI statuses. Statuses like NEARLY_FEASIBLE_POINT and INFEASIBLE_POINT, are designed to be used when the solver explicitly indicates that relaxed tolerances are satisfied or the returned point is infeasible, respectively.


Attributes like VariablePrimal and ObjectiveValue are indexed by the result count. Use MOI.check_result_index_bounds(model, attr) to throw an error if the attribute is not available.

If your solver returns dual solutions, implement:

For integer solvers, implement:

If applicable, implement:

If your solver uses the Simplex method, implement:

If your solver accepts primal or dual warm-starts, implement:

Other tips

Unsupported constraints at runtime

In some cases, your solver may support a particular type of constraint (e.g., quadratic constraints), but only if the data meets some condition (e.g., it is convex).

In this case, declare that you support the constraint, and throw AddConstraintNotAllowed.

Dealing with multiple variable bounds

MathOptInterface uses SingleVariable constraints to represent variable bounds. Defining multiple variable bounds on a single variable is not allowed.

Throw LowerBoundAlreadySet or UpperBoundAlreadySet if the user adds a constraint that results in multiple bounds.

Only throw if the constraints conflict. It is okay to add SingleVariable-in-GreaterThan and then SingleVariable-in-LessThan, but not SingleVariable-in-Interval and then SingleVariable-in-LessThan,

Expect duplicate coefficients

Solvers should expect that functions such as ScalarAffineFunction and VectorQuadraticFunction may contain duplicate coefficents.

For example, ScalarAffineFunction([ScalarAffineTerm(x, 1), ScalarAffineTerm(x, 1)], 0.0).

Use Utilities.canonical to return a new function with the duplicate coefficients aggregated together.

Don't modify user-data

All data passed to the solver should be copied immediately to internal data structures. Solvers may not modify any input vectors and should assume that input vectors may be modified by users in the future.

This applies, for example, to the terms vector in ScalarAffineFunction. Vectors returned to the user, e.g., via ObjectiveFunction or ConstraintFunction attributes, should not be modified by the solver afterwards. The in-place version of get! can be used by users to avoid extra copies in this case.

Column Generation

There is no special interface for column generation. If the solver has a special API for setting coefficients in existing constraints when adding a new variable, it is possible to queue modifications and new variables and then call the solver's API once all of the new coefficients are known.

Extra: solver-specific attributes

You don't need to restrict yourself to the attributes defined in the MathOptInterface.jl package.

Solver-specific attributes should be specified by creating an appropriate subtype of AbstractModelAttribute, AbstractOptimizerAttribute, AbstractVariableAttribute, or AbstractConstraintAttribute.

For example, Gurobi.jl adds attributes for multiobjective optimization by defining:

struct NumberOfObjectives <: MOI.AbstractModelAttribute end

function MOI.set(model::Optimizer, ::NumberOfObjectives, n::Integer)
    # Code to set NumberOfOBjectives

function MOI.get(model::Optimizer, ::NumberOfObjectives)
    n = # Code to get NumberOfobjectives
    return n

Then, the user can write:

model = Gurobi.Optimizer()
MOI.set(model, Gurobi.NumberofObjectives(), 3)