"Yes, of course duct tape works in a near-vacuum. Duct tape works anywhere. Duct tape is magic and should be worshiped." ― Andy Weir, The Martian
Call Julia from MATLAB using a Julia daemon launched by DaemonMode.jl.
Download the MATLAB function jlcall.m from the api subfolder of the MATDaemon.jl github repository and run
>> jlcallin the MATLAB console.
The first time jlcall.m is invoked in a MATLAB session:
- A local Julia project
.jlcall/Project.tomlwill be created, if it does not already exist, to whichMATDaemon.jland dependencies are added. The folder.jlcallis stored in the same directory as the downloaded copy ofjlcall.m. - A Julia server will be started in the background using
DaemonMode.jlwhich loadsMATDaemon.jl.
All subsequent calls to Julia via jlcall.m are run on the Julia server.
The server will be automatically killed when MATLAB exits.
Use the MATLAB function jlcall.m to call Julia from MATLAB:
>> jlcall('sort', {rand(2,5)}, struct('dims', int64(2)))
ans =
0.1270 0.2785 0.6324 0.8147 0.9575
0.0975 0.5469 0.9058 0.9134 0.9649The positional arguments passed to jlcall.m are:
- The Julia function to call, given as a MATLAB
chararray. This can be any Julia expression which evaluates to a function. For example,'a=2; b=3; x -> a*x+b'. For convenience, the empty string''is interpreted as'(args...; kwargs...) -> nothing', returningnothingfor any inputs. Note: expressions are wrapped in aletblock and evaluated in the global scope - Positional arguments, given as a MATLAB
cellarray. For example,args = {arg1, arg2, ...} - Keyword arguments, given as a MATLAB
struct. For example,kwargs = struct('key1', value1, 'key2', value2, ...)
In the event that the Julia server reaches an undesired state, the server can be restarted by passing the 'restart' flag with value true:
>> jlcall('', 'restart', true) % restarts the Julia server and returns nothingSimilarly, one can shutdown the Julia server without restarting it:
>> jlcall('', 'shutdown', true) % shuts down the Julia server and returns nothingBefore calling Julia functions, it may be necessary or convenient to first set up the Julia environment. For example, one may wish to activate a local project environment, run setup scripts, import modules for later use, or set the number of threads for running multithreaded code.
This setup can be conveniently executed at the start of your MATLAB script with a single call to jlcall.m as follows:
>> jlcall('', ...
'project', '/path/to/MyProject', ... % activate a local Julia Project
'setup', '/path/to/setup.jl', ... % run a setup script to load some custom Julia code
'modules', {'MyProject', 'LinearAlgebra', 'Statistics'}, ... % load a custom module and some modules from Base Julia
'threads', 'auto', ... % use the default number of Julia threads
'restart', true ... % start a fresh Julia server environment
)See the corresponding sections below for more details about these flags.
The number of threads used by the Julia server can be set using the 'threads' flag:
>> jlcall('() -> Threads.nthreads()', 'threads', 8, 'restart', true)
ans =
int64
8The default value for 'threads' is 'auto', deferring to Julia to choose the number of threads.
Note: Julia cannot change the number of threads at runtime.
In order for the 'threads' flag to take effect, the server must be restarted.
Julia modules can be loaded and used:
>> jlcall('LinearAlgebra.norm', {[3.0; 4.0]}, 'modules', {'LinearAlgebra'})
ans =
5Note: modules are loaded using import, not using. Module symbols must therefore be fully qualified, e.g. LinearAlgebra.norm in the above example as opposed to norm.
By default, previously loaded Julia code is available on subsequent calls to jlcall.m.
For example, following the above call to LinearAlgebra.norm, the LinearAlgebra.det function can be called without loading LinearAlgebra again:
>> jlcall('LinearAlgebra.det', {[1.0 2.0; 3.0 4.0]})
ans =
-2Set the 'shared' flag to false in order to evaluate each Julia call in a separate namespace on the Julia server:
% Restart the server, setting 'shared' to false
>> jlcall('LinearAlgebra.norm', {[3.0; 4.0]}, 'modules', {'LinearAlgebra'}, 'restart', true, 'shared', false)
ans =
5
% This call now errors, despite the above command loading the LinearAlgebra module, as LinearAlgebra.norm is evaluated in a new namespace
>> jlcall('LinearAlgebra.norm', {[3.0; 4.0]}, 'shared', false)
ERROR: LoadError: UndefVarError: LinearAlgebra not defined
Stacktrace:
...Instead of running Julia code on a persistent Julia server, unique Julia instances can be launched for each call to jlcall.m by passing the 'server' flag with value false.
Note: this may cause significant overhead when repeatedly calling jlcall.m due to Julia package precompilation and loading:
>> tic; jlcall('x -> sum(abs2, x)', {1:5}, 'server', false); toc
Elapsed time is 4.181178 seconds. % call unique Julia instance
>> tic; jlcall('x -> sum(abs2, x)', {1:5}, 'restart', true); toc
Elapsed time is 5.046929 seconds. % re-initialize Julia server
>> tic; jlcall('x -> sum(abs2, x)', {1:5}); toc
Elapsed time is 0.267088 seconds. % call server; significantly fasterCode from a local Julia project can be loaded and called:
>> jlcall('MyProject.my_function', args, kwargs, ...
'project', '/path/to/MyProject', ...
'modules', {'MyProject'})Note: the string passed via the 'project' flag is simply forwarded to Pkg.activate; it is the user's responsibility to ensure that the project's dependencies have been installed.
Julia functions may require or return types which cannot be directly passed from or loaded into MATLAB.
For example, suppose one would like to query Base.VERSION.
Naively calling jlcall('() -> Base.VERSION') would fail, as typeof(Base.VERSION) is not a String but a VersionNumber.
One possible remedy is to define a wrapper function in a Julia script:
# setup.jl
julia_version() = string(Base.VERSION)Then, use the 'setup' flag to pass the above script to jlcall.m:
>> jlcall('julia_version', 'setup', '/path/to/setup.jl')
ans =
'1.6.1'In this case, jlcall('() -> string(Base.VERSION)') would work just as well.
In general, however, interfacing with complex Julia libraries using MATLAB types may be nontrivial, and the 'setup' flag allows for the execution of arbitrary setup code.
Note: the setup script is loaded into the global scope using include; when using persistent environments, symbols defined in the setup script will be available on subsequent calls to jlcall.m.
Output(s) from Julia are returned using the MATLAB cell array varargout, MATLAB's variable-length list of output arguments.
A helper function MATDaemon.matlabify is used to convert Julia values into MATLAB-compatible values.
Specifically, the following rules are used to populate varargout with the Julia output y:
- If
y::Nothing, thenvarargout = {}and no outputs are returned to MATLAB - If
y::Tuple, thenlength(y)outputs are returned, withvarargout{i}given bymatlabify(y[i]) - Otherwise, one output is returned with
varargout{1}given bymatlabify(y)
The following matlabify methods are defined by default:
matlabify(x) = x # default fallback
matlabify(::Union{Nothing, Missing}) = zeros(0,0) # equivalent to MATLAB's []
matlabify(x::Symbol) = string(x)
matlabify(xs::Tuple) = Any[matlabify(x) for x in xs] # matlabify values
matlabify(xs::Union{<:AbstractDict, <:NamedTuple, <:Base.Iterators.Pairs}) = Dict{String, Any}(string(k) => matlabify(v) for (k, v) in pairs(xs)) # convert keys to strings and matlabify valuesNote: MATLAB cell and struct types correspond to Array{Any} and Dict{String, Any} in Julia.
Conversion via matlabify can easily be extended to additional types.
Returning to the example from the above section, we can define a matlabify method for Base.VersionNumber:
# setup.jl
MATDaemon.matlabify(v::Base.VersionNumber) = string(v)Now, the return type will be automatically converted:
>> jlcall('() -> Base.VERSION', 'setup', '/path/to/setup.jl')
ans =
'1.6.1'In case the Julia server gets into a bad state, the following troubleshooting tips may be helpful:
- Try restarting the server:
jlcall('', 'restart', true) - Enable debug mode for verbose logging:
jlcall('', 'debug', true) - Call Julia directly instead of calling the server:
jlcall('', 'server', false)- This will be slower, since each call to
jlcall.mwill start a new Julia instance, but it may fix server issues on Windows
- This will be slower, since each call to
- Update the
MATDaemon.jlJulia project environment (note: this will restart the server):jlcall('', 'update', true) - Reinstall the
MATDaemon.jlworkspace folder (note: this will restart the server):jlcall('', 'reinstall', true)- By default, the workspace folder is named
.jlcalland is stored in the same directory asjlcall.m - The
'reinstall'flag deletes the workspace folder, forcingMATDaemon.jlto be reinstalled; you can also delete it manually
- By default, the workspace folder is named
MATLAB inputs and Julia ouputs are passed back and forth between MATLAB and the DaemonMode.jl server by writing to temporary .mat files.
The location of these files can be configured with the 'infile' and 'outfile' flags, respectively.
Pointing these files to a ram-backed file system is recommended when possible (for example, the /tmp folder on Linux is usually ram-backed), as read/write speed will likely improve.
This is now the default; 'infile' and 'outfile' are created via the MATLAB tempname function (thanks to @mauro3 for this tip).
Nevertheless, this naturally leads to some overhead when calling Julia, particularly when the MATLAB inputs and/or Julia outputs have large memory footprints.
It is therefore not recommended to use jlcall.m in performance critical loops.
This package has been tested on a variety of MATLAB versions.
However, for some versions of Julia and MATLAB, supported versions of external libraries may clash.
For example, running jlcall.m using Julia v1.6.1 and MATLAB R2015b gives the following error:
>> jlcall
ERROR: Unable to load dependent library ~/.local/julia-1.6.1/bin/../lib/julia/libjulia-internal.so.1
Message: /usr/local/MATLAB/R2015b/sys/os/glnxa64/libstdc++.so.6: version `GLIBCXX_3.4.20' not found (required by ~/.local/julia-1.6.1/bin/../lib/julia/libjulia-internal.so.1)This error results due to a clash of supported libstdc++ versions, and does not occur when using e.g. Julia v1.5.4 with MATLAB R2015b, or Julia v1.6.1 with MATLAB R2020b.
If you encounter this issue, see the Julia and MATLAB documentation for information on mutually supported external libraries.
This repository contains utilities for parsing and running Julia code, passing MATLAB arguments to Julia, and retrieving Julia outputs from MATLAB.
The workhorse behind MATDaemon.jl and jlcall.m is DaemonMode.jl which is used to start a persistent Julia server in the background.