Full Code of JuliaInterop/MATLAB.jl for AI

master 11f657c7593b cached
23 files
90.1 KB
27.6k tokens
1 requests
Download .txt
Repository: JuliaInterop/MATLAB.jl
Branch: master
Commit: 11f657c7593b
Files: 23
Total size: 90.1 KB

Directory structure:
gitextract_utsjq7lg/

├── .JuliaFormatter.toml
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── CI.yml
│       ├── Format.yml
│       ├── RegisterAction.yml
│       └── TagBot.yml
├── .gitignore
├── LICENSE.md
├── Project.toml
├── README.md
├── deps/
│   └── build.jl
├── src/
│   ├── MATLAB.jl
│   ├── engine.jl
│   ├── exceptions.jl
│   ├── init.jl
│   ├── matfile.jl
│   ├── matstr.jl
│   └── mxarray.jl
└── test/
    ├── engine.jl
    ├── matfile.jl
    ├── matstr.jl
    ├── mxarray.jl
    └── runtests.jl

================================================
FILE CONTENTS
================================================

================================================
FILE: .JuliaFormatter.toml
================================================
whitespace_typedefs = false
whitespace_ops_in_indices = false
whitespace_in_kwargs = false
remove_extra_newlines = true

format_docstrings = true
annotate_untyped_fields_with_any = true
join_lines_based_on_source = true

align_assignment = true
align_pair_arrow = true
align_struct_field = true


================================================
FILE: .github/dependabot.yml
================================================
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/" # Location of package manifests
    schedule:
      interval: "monthly"


================================================
FILE: .github/workflows/CI.yml
================================================
name: CI

on:
  push:
    branches:
      - master
      - release-*
    tags: '*'
  pull_request:

jobs:
  test:
    name: julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }}
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        version:
          - '1.3'
          - '1'
          - 'nightly'
        os:
          - ubuntu-latest
        arch:
          - x64
    steps:
      - uses: actions/checkout@v6
      - uses: julia-actions/setup-julia@latest
        with:
          version: ${{ matrix.version }}
          arch: ${{ matrix.arch }}
      - uses: julia-actions/julia-buildpkg@latest
      - uses: julia-actions/julia-runtest@latest


================================================
FILE: .github/workflows/Format.yml
================================================
name: Format suggestions
on:
  pull_request:
    # this argument is not required if you don't use the `suggestion-label` input
    types: [opened, reopened, synchronize, labeled, unlabeled]
jobs:
  code-style:
    runs-on: ubuntu-latest
    steps:
      - uses: julia-actions/julia-format@v4
        with:
          version: "1" # Set `version` to '1.0.54' if you need to use JuliaFormatter.jl v1.0.54 (default: '1')
          suggestion-label: "format-suggest" # leave this unset or empty to show suggestions for all PRs


================================================
FILE: .github/workflows/RegisterAction.yml
================================================
name: RegisterAction
on:
  workflow_dispatch:
    inputs:
      version:
        description: Version to register or component to bump
        required: true
jobs:
  register:
    runs-on: ubuntu-latest
    steps:
      - uses: julia-actions/RegisterAction@latest
        with:
          token: ${{ secrets.GITHUB_TOKEN }}


================================================
FILE: .github/workflows/TagBot.yml
================================================
name: TagBot
on:
  issue_comment:
    types:
      - created
  workflow_dispatch:
    inputs:
      lookback:
        default: 3
permissions:
  actions: read
  checks: read
  contents: write
  deployments: read
  issues: read
  discussions: read
  packages: read
  pages: read
  pull-requests: read
  repository-projects: read
  security-events: read
  statuses: read
jobs:
  TagBot:
    if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot'
    runs-on: ubuntu-latest
    steps:
      - uses: JuliaRegistries/TagBot@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          # Edit the following line to reflect the actual name of the GitHub Secret containing your private key
          ssh: ${{ secrets.DOCUMENTER_KEY }}
          # ssh: ${{ secrets.NAME_OF_MY_SSH_PRIVATE_KEY_SECRET }}


================================================
FILE: .gitignore
================================================
deps/build.log
deps/deps.jl
*.mat
Manifest.toml


================================================
FILE: LICENSE.md
================================================
Copyright (c) 2013 Dahua Lin

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: Project.toml
================================================
name = "MATLAB"
uuid = "10e44e05-a98a-55b3-a45b-ba969058deb6"
license = "MIT"
repo = "https://github.com/JuliaInterop/MATLAB.jl.git"
version = "0.9.0"

[deps]
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"

[compat]
julia = "1"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]


================================================
FILE: README.md
================================================
## MATLAB

| ❗ If you get a 'Segmentation fault' error when calling MATLAB with version R2022 or newer, try using an [older version](https://github.com/JuliaInterop/MATLAB.jl#changing-matlab-version) of MATLAB. ❗ |
|:----:|

The `MATLAB.jl` package provides an interface for using [MATLAB®](http://www.mathworks.com/products/matlab/) from [Julia](http://julialang.org) using the MATLAB C api.  In other words, this package allows users to call MATLAB functions within Julia, thus making it easy to interoperate with MATLAB from the Julia language.

You cannot use `MATLAB.jl` without having purchased and installed a copy of MATLAB® from [MathWorks](http://www.mathworks.com/). This package is available free of charge and in no way replaces or alters any functionality of MathWorks's MATLAB product.

## Overview

This package is composed of two aspects:

* Creating and manipulating mxArrays (the data structure that MATLAB used to represent arrays and other kinds of data)

* Communicating with MATLAB engine sessions

**Warning**:

* MATLAB string arrays are not supported, and will throw an error exception. This also applies if they are nested within a MATLAB struct. This is a limitation of the MATLAB C api. The MATLAB function `convertContainedStringsToChars` may be used to facilitate conversion to a compatible format for use with `MATLAB.jl`.

* Threading is also not supported within Julia when using the MATLAB.jl library.


## Installation

**Important**: The procedure to setup this package consists of the following steps.

### Windows

1. For Matlab R2020a onwards, you should be able to go directly to step 2. If you encounter issues, run `matlab -batch "comserver('register')"` in the command prompt. For earlier versions of Matlab, start a command prompt as an administrator and enter `matlab /regserver`. Alternatively, you can do the same using the MATLAB GUI. To do this, open MATLAB with administrative privileges and run the following command in the MATLAB command window: `!matlab -regserver`. Close MATLAB and restart Julia.

2. From Julia run: `Pkg.add("MATLAB")`


### Linux

1. Make sure ``matlab`` is in executable path.

2. Make sure ``csh`` is installed. (Note: MATLAB for Linux relies on ``csh`` to open an engine session.)

	To install ``csh`` in Debian/Ubuntu/Linux Mint, you may type in the following command in terminal:

	```bash
	sudo apt-get install csh
	```

3. From Julia run: `Pkg.add("MATLAB")`


If you experience problems when starting the MATLAB engine with versions R2022 or R2023, try to [update](https://se.mathworks.com/help/matlab/matlab_env/check-for-software-updates.html) your MATLAB release.


### Mac OS X

1. Ensure that MATLAB is installed in `/Applications` (for example, if you are using MATLAB R2012b, you may add the following command to `.profile`:  `export MATLAB_HOME=/Applications/MATLAB_R2012b.app`).

2. From Julia run: `Pkg.add("MATLAB")`


## Changing MATLAB version

By default, `MATLAB.jl` is built using the MATLAB installation with the greatest version number. To specify that a specific MATLAB installation should be used, set the environment variable `MATLAB_ROOT`:
```julia
julia> ENV["MATLAB_ROOT"] = "/usr/local/MATLAB/R2021b" # example on a Linux machine
```
```julia
julia> ENV["MATLAB_ROOT"] = raw"C:\Program Files\MATLAB\R2021b" # example on a Windows machine
```
Replace the path string with the location of the MATLAB folder on your machine. You need to set the path to the `R20XX` folder, not the `matlab` binary.

If you had the package `MATLAB.jl` already installed and built before changing the environment variable, you will need to rebuild it to apply the change:
```julia
julia> using Pkg; Pkg.build("MATLAB")
```

### Notes for Windows Users

If you are using Windows, MATLAB needs to register its COM interface to properly work with MATLAB.jl. This should happen automatically during installation, but if you encounter issues, you can manually re-register MATLAB (with the version you want to use). To do this

1. Open a MATLAB window with administrative privileges
2. Run the following command in the MATLAB command window: `!matlab -regserver`
3. Close MATLAB and restart Julia

> [!IMPORTANT]
> The version of MATLAB that is registered must be same that `MATLAB.jl` uses.



## Usage

### MxArray class

An instance of ``MxArray`` encapsulates a MATLAB variable. This package provides a series of functions to manipulate such instances.

#### Create MATLAB variables in Julia

One can use the function ``mxarray`` to create MATLAB variables (of type ``MxArray``), as follows

```julia
mxarray(Float64, n)   # creates an n-by-1 MATLAB zero array of double valued type
mxarray(Int32, m, n)  # creates an m-by-n MATLAB zero array of int32 valued type
mxarray(Bool, m, n)   # creates a MATLAB logical array of size m-by-n

mxarray(Float64, (n1, n2, n3))  # creates a MATLAB array of size n1-by-n2-by-n3

mxcellarray(m, n)        # creates a MATLAB cell array
mxstruct("a", "b", "c")  # creates a MATLAB struct with given fields
```

You may also convert a Julia variable to MATLAB variable

```julia
a = rand(m, n)

x = mxarray(a)     # converts a to a MATLAB array
x = mxarray(1.2)   # converts a scalar 1.2 to a MATLAB variable

a = sprand(m, n, 0.1)
x = mxarray(a)     # converts a sparse matrix to a MATLAB sparse matrix

x = mxarray("abc") # converts a string to a MATLAB char array

x = mxarray(["a", 1, 2.3])  # converts a Julia array to a MATLAB cell array

x = mxarray(Dict("a"=>1, "b"=>"string", "c"=>[1,2,3])) # converts a Julia dictionary to a MATLAB struct
```

The function ``mxarray`` can also convert a compound type to a Julia struct:
```julia
struct S
    x::Float64
    y::Vector{Int32}
    z::Bool
end

s = S(1.2, Int32[1, 2], false)

x = mxarray(s)   # creates a MATLAB struct with three fields: x, y, z
xc = mxarray([s, s])  # creates a MATLAB cell array, each cell is a struct.
xs = mxstructarray([s, s])  # creates a MATLAB array of structs
```

**Note:** For safety, the conversation between MATLAB and Julia variables uses deep copy.

When you finish using a MATLAB variable, you may call ``delete`` to free the memory. But this is optional, it will be deleted when reclaimed by the garbage collector.

```julia
delete(x)
```

*Note:* if you put a MATLAB variable ``x`` to MATLAB engine session, then the MATLAB engine will take over the management of its life cylce, and you don't have to delete it explicitly.


#### Access MATLAB variables

You may access attributes and data of a MATLAB variable through the functions provided by this package.

```julia
# suppose x is of type MxArray
nrows(x)    # returns number of rows in x
ncols(x)    # returns number of columns in x
nelems(x)   # returns number of elements in x
ndims(x)    # returns number of dimensions in x
size(x)     # returns the size of x as a tuple
size(x, d)  # returns the size of x along a specific dimension

eltype(x)   # returns element type of x (in Julia Type)
elsize(x)   # return number of bytes per element

data_ptr(x)   # returns pointer to data (in Ptr{T}), where T is eltype(x)

# suppose s is a MATLAB struct
mxnfields(s)	# returns the number of fields in struct s

```

You may also make tests on a MATLAB variable.

```julia
is_double(x)   # returns whether x is a double array
is_sparse(x)   # returns whether x is sparse
is_complex(x)  # returns whether x is complex
is_cell(x)     # returns whether x is a cell array
is_struct(x)   # returns whether x is a struct
is_empty(x)    # returns whether x is empty

...            # there are many more there
```

#### Convert MATLAB variables to Julia

```julia
a = jarray(x)   # converts x to a Julia array
a = jvector(x)  # converts x to a Julia vector (1D array) when x is a vector
a = jscalar(x)  # converts x to a Julia scalar
a = jmatrix(x)  # converts x to a Julia matrix
a = jstring(x)  # converts x to a Julia string
a = jdict(x)    # converts a MATLAB struct to a Julia dictionary (using fieldnames as keys)

a = jvalue(x)  # converts x to a Julia value in default manner
```

### Read/Write MAT Files

This package provides functions to manipulate MATLAB's mat files:

```julia
mf = MatFile(filename, mode)    # opens a MAT file using a specific mode, and returns a handle
mf = MatFile(filename)          # opens a MAT file for reading, equivalent to MatFile(filename, "r")
close(mf)                       # closes a MAT file.

get_mvariable(mf, name)   # gets a variable and returns an mxArray
get_variable(mf, name)    # gets a variable, but converts it to a Julia value using `jvalue`

put_variable(mf, name, v)   # puts a variable v to the MAT file
                            # v can be either an MxArray instance or normal variable
                            # If v is not an MxArray, it will be converted using `mxarray`

put_variables(mf; name1=v1, name2=v2, ...)  # put multiple variables using keyword arguments

variable_names(mf)   # get a vector of all variable names in a MAT file
```

There are also convenient functions that can get/put all variables in one call:

```julia
read_matfile(filename)    # returns a dictionary that maps each variable name
                          # to an MxArray instance

write_matfile(filename; name1=v1, name2=v2, ...)  # writes all variables given in the
                                                  # keyword argument list to a MAT file
```
Both ``read_matfile`` and ``write_matfile`` will close the MAT file handle before returning.

**Examples:**

```julia
struct S
    x::Float64
    y::Bool
    z::Vector{Float64}
end

write_matfile("test.mat";
    a = Int32[1 2 3; 4 5 6],
    b = [1.2, 3.4, 5.6, 7.8],
    c = [[0.0, 1.0], [1.0, 2.0], [1.0, 2.0, 3.0]],
    d = Dict("name"=>"MATLAB", "score"=>100.0),
    s = "abcde",
    ss = [S(1.0, true, [1., 2.]), S(2.0, false, [3., 4.])] )
```

This example will create a MAT file called ``test.mat``, which contains six MATLAB variables:

* ``a``: a 2-by-3 int32 array
* ``b``: a 4-by-1 double array
* ``c``: a 3-by-1 cell array, each cell contains a double vector
* ``d``: a struct with two fields: name and score
* ``s``: a string (i.e. char array)
* ``ss``: an array of structs with two elements, and three fields: x, y, and z.


### Use MATLAB Engine

#### Basic Use

To evaluate expressions in MATLAB, one may open a MATLAB engine session and communicate with it. There are three ways to call MATLAB from Julia:

- The `mat""` custom string literal allows you to write MATLAB syntax inside Julia and use Julia variables directly from MATLAB via interpolation
- The `eval_string` evaluate a string containing MATLAB expressions (typically used with the helper macros `@mget` and `@mput`
- The `mxcall` function calls a given MATLAB function and returns the result

In general, the `mat""` custom string literal is the preferred method to interact with the MATLAB engine.

*Note:* There can be multiple (reasonable) ways to convert a MATLAB variable to Julia array. For example, MATLAB represents a scalar using a 1-by-1 matrix. Here we have two choices in terms of converting such a matrix back to Julia: (1) convert to a scalar number, or (2) convert to a matrix of size 1-by-1.

##### The `mat""` custom string literal

Text inside the `mat""` custom string literal is in MATLAB syntax. Variables from Julia can be "interpolated" into MATLAB code by prefixing them with a dollar sign as you would interpolate them into an ordinary string.

```julia
using MATLAB

x = range(-10.0, stop=10.0, length=500)
mat"plot($x, sin($x))"  # evaluate a MATLAB function

y = range(2.0, stop=3.0, length=500)
mat"""
    $u = $x + $y
	$v = $x - $y
"""
@show u v               # u and v are accessible from Julia
```

As with ordinary string literals, you can also interpolate whole Julia expressions, e.g. `mat"$(x[1]) = $(x[2]) + $(binomial(5, 2))"`.

##### `eval_string`

You may also use the `eval_string` function to evaluate MATLAB code as follows
 ```julia
eval_string("a = sum([1,2,3])")
```

The `eval_string` function also takes an optional argument that specifies which MATLAB session to evaluate the code in, e.g.
```julia
julia> s = MSession();
julia> eval_string(s, "a = sum([1,2,3])")
a =
     6
```

##### `mxcall`

You may also directly call a MATLAB function on Julia variables using `mxcall`:

```julia
x = -10.0:0.1:10.0
y = -10.0:0.1:10.0
xx, yy = mxcall(:meshgrid, 2, x, y)
```
*Note:* Since MATLAB functions behavior depends on the number of outputs, you have to specify the number of output arguments in ``mxcall`` as the second argument.

``mxcall`` puts the input arguments to the MATLAB workspace (using mangled names), evaluates the function call in MATLAB, and retrieves the variable from the MATLAB session. This function is mainly provided for convenience. However, you should keep in mind that it may incur considerable overhead due to the communication between MATLAB and Julia domain.

##### `@mget` and `@mput`

The macro `@mget` can be used to extract the value of a MATLAB variable into Julia
```julia
julia> mat"a = 6"
julia> @mget a
6.0
```

The macro `@mput` can be used to translate a Julia variable into MATLAB
```julia
julia> x = [1,2,3]
julia> @mput x
julia> eval_string("y = sum(x)")
julia> @mget y
6.0
julia> @show y
a = 63.0
```

#### Calling custom MATLAB function

If the MATLAB function is not in the current directory, we need to first add it to the MATLAB path before calling through Julia:

```julia
mat"addpath('/path/to/folder')"
val = mat"myfunction($arg1, $arg2)"
```
For example, if there is a MATLAB file located at `/path/to/folder` with contents:

```matlab
function [r,u] = test(x, y)
	r = x + y;
	u = x - y;
end
```

We can call this function as follows in Julia:

```julia
using MATLAB

x = range(-10.0, stop=10.0, length=500)
y = range(2.0, stop=3.0, length=500)

mat"addpath('/path/to/folder')"

r, u = mxcall(:test,2,x,y)
```



#### Viewing the MATLAB Session (Windows only)

To open an interactive window for the MATLAB session, use the command `show_msession()` and to hide the window, use `hide_msession()`. *Warning: manually closing this window will result in an error or result in a segfault; it is advised that you only use the `hide_msession()` command to hide the interactive window.*

Note that this feature only works on Windows.

```julia
# default
show_msession() # open the default MATLAB session interactive window
get_msession_visiblity() # get the session's visibility state
hide_msession() # hide the default MATLAB session interactive window

# similarly
s = MSession()
show_msession(s)
get_msession_visiblity(a)
hide_msession(s)
```


#### Advanced use of MATLAB Engines

This package provides a series of functions for users to control the communication with MATLAB sessions.

Here is an example:

```julia
s1 = MSession()    # creates a MATLAB session
s2 = MSession(0)   # creates a MATLAB session without recording output

x = rand(3, 4)
put_variable(s1, :x, x)  # put x to session s1

y = rand(2, 3)
put_variable(s2, :y, y)  # put y to session s2

eval_string(s1, "r = sin(x)")  # evaluate sin(x) in session s1
eval_string(s2, "r = sin(y)")  # evaluate sin(y) in session s2

r1_mx = get_mvariable(s1, :r)  # get r from s1
r2_mx = get_mvariable(s2, :r)  # get r from s2

r1 = jarray(r1_mx)
r2 = jarray(r2_mx)

# ... do other stuff on r1 and r2

close(s1)  # close session s1
close(s2)  # close session s2
```


================================================
FILE: deps/build.jl
================================================
import Libdl

const depsfile = joinpath(@__DIR__, "deps.jl")

function find_matlab_root()
    # Determine MATLAB library path and provide facilities to load libraries with this path
    matlab_root = get(ENV, "MATLAB_ROOT",
        get(ENV, "MATLAB_HOME", nothing))
    if isnothing(matlab_root)
        matlab_exe = Sys.which("matlab")
        if !isnothing(matlab_exe)
            matlab_exe = islink(matlab_exe) ? readlink(matlab_exe) : matlab_exe # guard against /usr/local 
            matlab_root = dirname(dirname(matlab_exe))
        else
            if Sys.isapple()
                default_dir = "/Applications"
                if isdir(default_dir)
                    dirs = readdir(default_dir)
                    filter!(app -> occursin(r"^MATLAB_R[0-9]+[ab]\.app$", app), dirs)
                    if !isempty(dirs)
                        matlab_root = joinpath(default_dir, maximum(dirs))
                    end
                end
            elseif Sys.iswindows()
                default_dir =
                    Sys.WORD_SIZE == 32 ? "C:\\Program Files (x86)\\MATLAB" :
                    "C:\\Program Files\\MATLAB"
                if isdir(default_dir)
                    dirs = readdir(default_dir)
                    filter!(dir -> occursin(r"^R[0-9]+[ab]$", dir), dirs)
                    if !isempty(dirs)
                        matlab_root = joinpath(default_dir, maximum(dirs))
                    end
                end
            elseif Sys.islinux()
                default_dir = "/usr/local/MATLAB"
                if isdir(default_dir)
                    dirs = readdir(default_dir)
                    filter!(dir -> occursin(r"^R[0-9]+[ab]$", dir), dirs)
                    if !isempty(dirs)
                        matlab_root = joinpath(default_dir, maximum(dirs))
                    end
                end
            end
        end
    end
    !isnothing(matlab_root) && isdir(matlab_root) &&
        @info("Detected MATLAB root folder at \"$matlab_root\"")
    return matlab_root
end

function find_matlab_libpath(matlab_root)
    # get path to MATLAB libraries
    matlab_libdir = if Sys.islinux()
        Sys.WORD_SIZE == 32 ? "glnx86" : "glnxa64"
    elseif Sys.isapple()
        archchar = Sys.ARCH == :aarch64 ? "a" : "i"
        Sys.WORD_SIZE == 32 ? "maci" : "mac" * archchar * "64"
    elseif Sys.iswindows()
        Sys.WORD_SIZE == 32 ? "win32" : "win64"
    end
    matlab_libpath = joinpath(matlab_root, "bin", matlab_libdir)
    isdir(matlab_libpath) && @info("Detected MATLAB library path at \"$matlab_libpath\"")
    return matlab_libpath
end

function find_matlab_cmd(matlab_root)
    if Sys.iswindows()
        matlab_cmd = joinpath(
            matlab_root,
            "bin",
            (Sys.WORD_SIZE == 32 ? "win32" : "win64"),
            "matlab.exe",
        )
        isfile(matlab_cmd) && @info("Detected MATLAB executable at \"$matlab_cmd\"")
    else
        matlab_exe = joinpath(matlab_root, "bin", "matlab")
        isfile(matlab_exe) && @info("Detected MATLAB executable at \"$matlab_exe\"")
        matlab_cmd = "exec $(Base.shell_escape(matlab_exe))"
    end
    return matlab_cmd
end

matlab_root = find_matlab_root()

if !isnothing(matlab_root)
    matlab_libpath = find_matlab_libpath(matlab_root)
    matlab_cmd = find_matlab_cmd(matlab_root)
    libmx_size = filesize(Libdl.dlpath(joinpath(matlab_libpath, "libmx")))
    open(depsfile, "w") do io
        println(io,
            """
            # This file is automatically generated, do not edit.

            function check_deps()
                if libmx_size != filesize(Libdl.dlpath(joinpath(matlab_libpath, "libmx")))
                    error("MATLAB library has changed, re-run Pkg.build(\\\"MATLAB\\\")")
                end
            end
            """,
        )
        println(io, "const matlab_libpath = \"$(escape_string(matlab_libpath))\"")
        println(io, "const matlab_cmd = \"$(escape_string(matlab_cmd))\"")
        println(io, "const libmx_size = $libmx_size")
    end
elseif get(ENV, "JULIA_REGISTRYCI_AUTOMERGE", nothing) == "true" ||
       get(ENV, "CI", nothing) == "true"
    # We need to be able to install and load this package without error for
    # Julia's registry AutoMerge to work, so we just use dummy values.
    # Similarly we want to also be able to install and load this package for CI.
    matlab_libpath = ""
    matlab_cmd = ""
    libmx_size = 0

    open(depsfile, "w") do io
        println(io,
            """
            # This file is automatically generated, do not edit.

            check_deps() = nothing
            """,
        )
        println(io, "const matlab_libpath = \"$(escape_string(matlab_libpath))\"")
        println(io, "const matlab_cmd = \"$(escape_string(matlab_cmd))\"")
        println(io, "const libmx_size = $libmx_size")
    end
else
    error(
        "MATLAB cannot be found. Set the \"MATLAB_ROOT\" environment variable to the MATLAB root directory and re-run Pkg.build(\"MATLAB\").",
    )
end


================================================
FILE: src/MATLAB.jl
================================================
module MATLAB

using Libdl
using SparseArrays

import Base: eltype, close, size, copy, ndims, unsafe_convert

# mxarray
export MxArray,
    mxClassID, mxComplexity,
    mxclassid, data_ptr,
    classid, nrows, ncols, nelems, elsize

export is_double, is_single,
    is_int8, is_uint8, is_int16, is_uint16,
    is_int32, is_uint32, is_int64, is_uint64,
    is_numeric, is_complex, is_sparse, is_empty,
    is_logical, is_char, is_struct, is_cell

export mxarray, mxsparse, delete,
    mxcellarray, get_cell, set_cell,
    mxstruct, mxstructarray, mxnfields, get_fieldname, get_field, set_field,
    jvalue, jarray, jscalar, jvector, jmatrix, jsparse, jstring, jdict

# engine & matfile
export MSession, MatFile,
    get_default_msession, restart_default_msession, close_default_msession,
    eval_string, get_mvariable, get_variable, put_variable, put_variables,
    variable_names, read_matfile, write_matfile,
    mxcall,
    @mput, @mget, @mat_str

if Sys.iswindows()
    export show_msession, hide_msession, get_msession_visiblity
end

const depsfile = joinpath(dirname(@__DIR__), "deps", "deps.jl")
if isfile(depsfile)
    include(depsfile)
else
    error(
        "MATLAB is not properly installed. Please run Pkg.build(\"MATLAB\") and restart Julia.",
    )
end

include("exceptions.jl")
include("init.jl") # initialize Refs
include("mxarray.jl")
include("matfile.jl")
include("engine.jl")
include("matstr.jl")

if Sys.iswindows()
    # workaround "primary message table for module 77" error
    # creates a dummy Engine session and keeps it open so the libraries used by all other
    # Engine clients are not loaded and unloaded repeatedly
    # see: https://www.mathworks.com/matlabcentral/answers/305877-what-is-the-primary-message-table-for-module-77

    # initialization is delayed until first call to MSession
    const persistent_msession_ref = Ref{MSession}()
    const persistent_msession_assigned = Ref(false)

    function assign_persistent_msession()
        if persistent_msession_assigned[] == false
            persistent_msession_assigned[] = true
            persistent_msession_ref[] = MSession(0)
        end
    end
end

# helper library access function
engfunc(fun::Symbol) = Libdl.dlsym(libeng[], fun)
mxfunc(fun::Symbol)  = Libdl.dlsym(libmx[], fun)
matfunc(fun::Symbol) = Libdl.dlsym(libmat[], fun)

function __init__()
    check_deps()

    if libmx_size > 0 # non-zero size library path

        # load libraries
        # workaround for https://github.com/JuliaInterop/MATLAB.jl/issues/200
        if Sys.iswindows()
            ENV["PATH"] = string(matlab_libpath, ";", ENV["PATH"])
        elseif Sys.islinux()
            ENV["PATH"] = string(matlab_libpath, ":", ENV["PATH"])
        end
        libmx[]  = Libdl.dlopen(joinpath(matlab_libpath, "libmx"), Libdl.RTLD_GLOBAL)
        libmat[] = Libdl.dlopen(joinpath(matlab_libpath, "libmat"), Libdl.RTLD_GLOBAL)
        libeng[] = Libdl.dlopen(joinpath(matlab_libpath, "libeng"), Libdl.RTLD_GLOBAL)

        # engine functions

        eng_open[]          = engfunc(:engOpen)
        eng_close[]         = engfunc(:engClose)
        eng_set_visible[]   = engfunc(:engSetVisible)
        eng_get_visible[]   = engfunc(:engGetVisible)
        eng_output_buffer[] = engfunc(:engOutputBuffer)
        eng_eval_string[]   = engfunc(:engEvalString)
        eng_put_variable[]  = engfunc(:engPutVariable)
        eng_get_variable[]  = engfunc(:engGetVariable)

        # mxarray functions

        mx_destroy_array[]   = mxfunc(:mxDestroyArray)
        mx_duplicate_array[] = mxfunc(:mxDuplicateArray)

        # load functions to access mxarray

        mx_free[] = mxfunc(:mxFree)

        mx_get_classid[]  = mxfunc(:mxGetClassID)
        mx_get_m[]        = mxfunc(:mxGetM)
        mx_get_n[]        = mxfunc(:mxGetN)
        mx_get_nelems[]   = mxfunc(:mxGetNumberOfElements)
        mx_get_ndims[]    = mxfunc(:mxGetNumberOfDimensions_730)
        mx_get_elemsize[] = mxfunc(:mxGetElementSize)
        mx_get_data[]     = mxfunc(:mxGetData)
        mx_get_dims[]     = mxfunc(:mxGetDimensions_730)
        mx_get_nfields[]  = mxfunc(:mxGetNumberOfFields)
        mx_get_pr[]       = mxfunc(:mxGetPr)
        mx_get_pi[]       = mxfunc(:mxGetPi)
        mx_get_ir[]       = mxfunc(:mxGetIr_730)
        mx_get_jc[]       = mxfunc(:mxGetJc_730)

        mx_is_double[] = mxfunc(:mxIsDouble)
        mx_is_single[] = mxfunc(:mxIsSingle)
        mx_is_int64[]  = mxfunc(:mxIsInt64)
        mx_is_uint64[] = mxfunc(:mxIsUint64)
        mx_is_int32[]  = mxfunc(:mxIsInt32)
        mx_is_uint32[] = mxfunc(:mxIsUint32)
        mx_is_int16[]  = mxfunc(:mxIsInt16)
        mx_is_uint16[] = mxfunc(:mxIsUint16)
        mx_is_int8[]   = mxfunc(:mxIsInt8)
        mx_is_uint8[]  = mxfunc(:mxIsUint8)
        mx_is_char[]   = mxfunc(:mxIsChar)

        mx_is_numeric[] = mxfunc(:mxIsNumeric)
        mx_is_logical[] = mxfunc(:mxIsLogical)
        mx_is_complex[] = mxfunc(:mxIsComplex)
        mx_is_sparse[]  = mxfunc(:mxIsSparse)
        mx_is_empty[]   = mxfunc(:mxIsEmpty)
        mx_is_struct[]  = mxfunc(:mxIsStruct)
        mx_is_cell[]    = mxfunc(:mxIsCell)

        # load functions to create & delete MATLAB array

        mx_create_numeric_matrix[] = mxfunc(:mxCreateNumericMatrix_730)
        mx_create_numeric_array[]  = mxfunc(:mxCreateNumericArray_730)

        mx_create_double_scalar[]  = mxfunc(:mxCreateDoubleScalar)
        mx_create_logical_scalar[] = mxfunc(:mxCreateLogicalScalar)

        mx_create_sparse[]         = mxfunc(:mxCreateSparse_730)
        mx_create_sparse_logical[] = mxfunc(:mxCreateSparseLogicalMatrix_730)

        mx_create_string[]     = mxfunc(:mxCreateString)
        mx_create_char_array[] = mxfunc(:mxCreateCharArray_730)

        mx_create_cell_array[] = mxfunc(:mxCreateCellArray_730)

        mx_create_struct_matrix[] = mxfunc(:mxCreateStructMatrix_730)
        mx_create_struct_array[]  = mxfunc(:mxCreateStructArray_730)

        mx_get_cell[] = mxfunc(:mxGetCell_730)
        mx_set_cell[] = mxfunc(:mxSetCell_730)

        mx_get_field[]       = mxfunc(:mxGetField_730)
        mx_set_field[]       = mxfunc(:mxSetField_730)
        mx_get_field_bynum[] = mxfunc(:mxGetFieldByNumber_730)
        mx_get_fieldname[]   = mxfunc(:mxGetFieldNameByNumber)

        mx_get_string[] = mxfunc(:mxGetString_730)

        # load I/O mat functions

        mat_open[]         = matfunc(:matOpen)
        mat_close[]        = matfunc(:matClose)
        mat_get_variable[] = matfunc(:matGetVariable)
        mat_put_variable[] = matfunc(:matPutVariable)
        mat_get_dir[]      = matfunc(:matGetDir)
    end
end

###########################################################
#
#   deprecations
#
###########################################################

end


================================================
FILE: src/engine.jl
================================================
# operation on MATLAB engine sessions

###########################################################
#
#   Session open & close
#
###########################################################
const default_startflag = "-nodisplay -nosplash -nodesktop" # no additional flags
const default_matlabcmd = matlab_cmd * " -nodisplay -nosplash -nodesktop"
# pass matlab flags directly or as a Vector of flags, i.e. "-a" or ["-a", "-b", "-c"]
startcmd(flag::AbstractString=default_startflag) =
    isempty(flag) ? default_matlabcmd : default_matlabcmd * " " * flag
startcmd(flags::AbstractVector{<:AbstractString}) =
    isempty(flags) ? default_matlabcmd : default_matlabcmd * " " * join(flags, " ")

# 64 K buffer should be sufficient to store the output text in most cases
const default_output_buffer_size = 64 * 1024

const windows_regserver_warning = """
Failed to start MATLAB engine. If you have/had multiple versions of MATLAB installed, this can happen if you 
tried to start a different version of MATLAB in Julia compared to which MATLAB server is registered in Windows.

Steps to resolve this:

1. Register a specific MATLAB version manually as a server, open a MATLAB window as a user with administrator privileges. 
In MATLAB, enter the command `!matlab -regserver`. Then close the MATLAB window. More details:
https://de.mathworks.com/help/matlab/matlab_external/registering-matlab-software-as-a-com-server.html

2. Ensure that the MATLAB.jl package is using the same MATLAB version that was registered in step 1. See the instructions on GitHub
on how to change the version that MATLAB.jl uses:
https://github.com/JuliaInterop/MATLAB.jl?tab=readme-ov-file#changing-matlab-version
"""

mutable struct MSession
    ptr::Ptr{Cvoid}
    buffer::Vector{UInt8}
    bufptr::Ptr{UInt8}
    check_exceptions::Bool

    function MSession(
        bufsize::Integer=default_output_buffer_size;
        flags=default_startflag,
        check_exceptions::Bool=true,
    )
        if Sys.iswindows()
            assign_persistent_msession()
        end
        ep = ccall(eng_open[], Ptr{Cvoid}, (Ptr{UInt8},), startcmd(flags))
        if ep == C_NULL
            @warn(
                "Confirm MATLAB is installed and discoverable.",
                matlab_libpath,
                maxlog = 1
            )
            if Sys.iswindows()
                @warn(windows_regserver_warning, maxlog = 1)
            elseif Sys.islinux()
                @warn(
                    "Ensure `csh` is installed; this may require running `sudo apt-get install csh`.",
                    maxlog = 1
                )
            end
            throw(MEngineError("failed to open MATLAB engine session"))
        end
        if Sys.iswindows()
            # hide the MATLAB command window on Windows and change to current directory
            ccall(eng_set_visible[], Cint, (Ptr{Cvoid}, Cint), ep, 0)
            ccall(eng_eval_string[], Cint, (Ptr{Cvoid}, Ptr{UInt8}),
                ep, "try cd('$(escape_string(pwd()))'); end")
        end
        buf = Vector{UInt8}(undef, bufsize)
        if bufsize > 0
            bufptr = pointer(buf)
            ccall(eng_output_buffer[], Cint, (Ptr{Cvoid}, Ptr{UInt8}, Cint),
                ep, bufptr, bufsize)
        else
            bufptr = convert(Ptr{UInt8}, C_NULL)
        end

        self = new(ep, buf, bufptr, check_exceptions)
        finalizer(release, self)
        return self
    end
end

function unsafe_convert(::Type{Ptr{Cvoid}}, m::MSession)
    ptr = m.ptr
    ptr == C_NULL && throw(UndefRefError())
    return ptr
end

function release(session::MSession)
    ptr = session.ptr
    if ptr != C_NULL
        ccall(eng_close[], Cint, (Ptr{Cvoid},), ptr)
    end
    session.ptr = C_NULL
    return nothing
end

function close(session::MSession)
    # close a MATLAB Engine session
    ret = ccall(eng_close[], Cint, (Ptr{Cvoid},), session)
    ret != 0 && throw(MEngineError("failed to close MATLAB engine session (err = $ret)"))
    session.ptr = C_NULL
    return nothing
end

has_exception_check_enabled(session::MSession=get_default_msession()) =
    session.check_exceptions
disable_exception_check!(session::MSession=get_default_msession()) =
    (session.check_exceptions = false; nothing)
enable_exception_check!(session::MSession=get_default_msession()) =
    (session.check_exceptions = true; nothing)

# default session

const default_msession_ref = Ref{MSession}()

# this function will start an MSession if default_msession_ref is undefined or if the
# MSession has been closed so that the engine ptr is void
function get_default_msession()
    if !isassigned(default_msession_ref) || default_msession_ref[].ptr == C_NULL
        default_msession_ref[] = MSession()
    end
    return default_msession_ref[]
end

function restart_default_msession(bufsize::Integer=default_output_buffer_size)
    close_default_msession()
    default_msession_ref[] = MSession(bufsize)
    return nothing
end

function close_default_msession()
    if isassigned(default_msession_ref) && default_msession_ref[].ptr !== C_NULL
        close(default_msession_ref[])
    end
    return nothing
end

if Sys.iswindows()
    function show_msession(m::MSession=get_default_msession())
        ret = ccall(eng_set_visible[], Cint, (Ptr{Cvoid}, Cint), m, 1)
        ret != 0 && throw(MEngineError("failed to show MATLAB engine session (err = $ret)"))
        return nothing
    end

    function hide_msession(m::MSession=get_default_msession())
        ret = ccall(eng_set_visible[], Cint, (Ptr{Cvoid}, Cint), m, 0)
        ret != 0 && throw(MEngineError("failed to hide MATLAB engine session (err = $ret)"))
        return nothing
    end

    function get_msession_visiblity(m::MSession=get_default_msession())
        vis = Ref{Cint}(true)
        ccall(eng_get_visible[], Int, (Ptr{Cvoid}, Ptr{Cint}), m, vis)
        return vis[] == 1 ? true : false
    end
end

###########################################################
#
#   communication with MATLAB session
#
###########################################################

function _eval_string(session::MSession, stmt::String)
    # evaluate a MATLAB statement in a given MATLAB session
    ret = ccall(eng_eval_string[], Cint, (Ptr{Cvoid}, Ptr{UInt8}), session, stmt)
    ret != 0 && throw(MEngineError("invalid engine session (err = $ret)"))

    bufptr = session.bufptr
    if bufptr != C_NULL
        bs = unsafe_string(bufptr)
        if ~isempty(bs)
            print(bs)
        end
    end
    return nothing
end

function eval_string(session::MSession, stmt::String)
    _eval_string(session, stmt)
    if session.check_exceptions
        check_and_clear_last_exception(session)
    end
end

eval_string(stmt::String) = eval_string(get_default_msession(), stmt)

function put_variable(session::MSession, name::Symbol, v::MxArray)
    # put a variable into a MATLAB engine session
    ret = ccall(
        eng_put_variable[],
        Cint,
        (Ptr{Cvoid}, Ptr{UInt8}, Ptr{Cvoid}),
        session,
        string(name),
        v,
    )
    ret != 0 && throw(
        MEngineError("failed to put variable $(name) into MATLAB session (err = $ret)"),
    )
    return nothing
end

put_variable(session::MSession, name::Symbol, v) = put_variable(session, name, mxarray(v))

put_variable(name::Symbol, v) = put_variable(get_default_msession(), name, v)

function get_mvariable(session::MSession, name::Symbol)
    pv = ccall(
        eng_get_variable[],
        Ptr{Cvoid},
        (Ptr{Cvoid}, Ptr{UInt8}),
        session,
        string(name),
    )
    pv == C_NULL &&
        throw(MEngineError("failed to get variable $(name) from MATLAB session"))
    return MxArray(pv)
end

get_mvariable(name::Symbol) = get_mvariable(get_default_msession(), name)

get_variable(name::Symbol) = jvalue(get_mvariable(name))
get_variable(name::Symbol, kind) = jvalue(get_mvariable(name), kind)

"""
    check_and_clear_last_exception(session::MSession)

Checks if an exception has been thrown in the MATLAB session by checking the `MException.last` variable.
If it is not empty, it throws a `MatlabException` with the message and identifier of the last exception.
In any case, it clears the `MException.last` variable.
"""
function check_and_clear_last_exception(session::MSession)
    exception_check_code = """
    matlab_exception_jl_message = MException.last.message; 
    matlab_exception_jl_identifier = MException.last.identifier; 
    MException.last('reset');
    """
    _eval_string(session, exception_check_code)
    message = jvalue(get_mvariable(session, :matlab_exception_jl_message))
    identifier = jvalue(get_mvariable(session, :matlab_exception_jl_identifier))

    if !isempty(identifier)
        throw(MatlabException(identifier, message))
    end

    _eval_string(
        session,
        "clear matlab_exception_jl_message matlab_exception_jl_identifier;",
    )
end

###########################################################
#
#   macro to simplify syntax
#
###########################################################

function _mput_multi(vs::Symbol...)
    nv = length(vs)
    if nv == 1
        v = vs[1]
        :(MATLAB.put_variable($(Meta.quot(v)), $(v)))
    else
        stmts = Vector{Expr}(undef, nv)
        for i = 1:nv
            v = vs[i]
            stmts[i] = :(MATLAB.put_variable($(Meta.quot(v)), $(v)))
        end
        Expr(:block, stmts...)
    end
end

macro mput(vs...)
    esc(_mput_multi(vs...))
end

function make_getvar_statement(v::Symbol)
    :($(v) = MATLAB.get_variable($(Meta.quot(v))))
end

function make_getvar_statement(ex::Expr)
    if !(ex.head == :(::))
        error("Invalid expression for @mget.")
    end
    v::Symbol = ex.args[1]
    k::Symbol = ex.args[2]

    :($(v) = MATLAB.get_variable($(Meta.quot(v)), $(k)))
end

function _mget_multi(vs::Union{Symbol,Expr}...)
    nv = length(vs)
    if nv == 1
        make_getvar_statement(vs[1])
    else
        stmts = Vector{Expr}(undef, nv)
        for i = 1:nv
            stmts[i] = make_getvar_statement(vs[i])
        end
        Expr(:block, stmts...)
    end
end

macro mget(vs...)
    esc(_mget_multi(vs...))
end

###########################################################
#
#   mxcall
#
###########################################################

# MATLAB does not allow underscore as prefix of a variable name
_gen_marg_name(mfun::Symbol, prefix::String, i::Int) = "jx_$(mfun)_arg_$(prefix)_$(i)"

function mxcall(session::MSession, mfun::Symbol, nout::Integer, in_args...)
    nin = length(in_args)

    # generate temporary variable names

    in_arg_names = Vector{String}(undef, nin)
    out_arg_names = Vector{String}(undef, nout)

    for i = 1:nin
        in_arg_names[i] = _gen_marg_name(mfun, "in", i)
    end

    for i = 1:nout
        out_arg_names[i] = _gen_marg_name(mfun, "out", i)
    end

    # generate MATLAB statement

    buf = IOBuffer()
    if nout > 0
        if nout > 1
            print(buf, "[")
        end
        join(buf, out_arg_names, ", ")
        if nout > 1
            print(buf, "]")
        end
        print(buf, " = ")
    end

    print(buf, string(mfun))
    print(buf, "(")
    if nin > 0
        join(buf, in_arg_names, ", ")
    end
    print(buf, ");")

    stmt = String(take!(buf))

    # put variables to MATLAB

    for i = 1:nin
        put_variable(session, Symbol(in_arg_names[i]), in_args[i])
    end

    # execute MATLAB statement

    eval_string(session, stmt)

    # get results from MATLAB

    ret = if nout == 1
        jvalue(get_mvariable(session, Symbol(out_arg_names[1])))
    elseif nout >= 2
        results = Vector{Any}(undef, nout)
        for i = 1:nout
            results[i] = jvalue(get_mvariable(session, Symbol(out_arg_names[i])))
        end
        tuple(results...)
    else
        nothing
    end

    # clear temporaries from MATLAB workspace

    for i = 1:nin
        eval_string(session, string("clear ", in_arg_names[i], ";"))
    end

    for i = 1:nout
        eval_string(session, string("clear ", out_arg_names[i], ";"))
    end

    return ret
end

mxcall(mfun::Symbol, nout::Integer, in_args...) =
    mxcall(get_default_msession(), mfun, nout, in_args...)


================================================
FILE: src/exceptions.jl
================================================
struct MEngineError <: Exception
    message::String
end

"""
    MEngineError(message::String)

Exception thrown by MATLAB, e.g. due to syntax errors in the code
passed to `eval_string` or `mat"..."`.
"""
struct MatlabException <: Exception
    identifier::String
    message::String
end


================================================
FILE: src/init.jl
================================================
# libraries

const libeng = Ref{Ptr{Cvoid}}()
const libmx  = Ref{Ptr{Cvoid}}()
const libmat = Ref{Ptr{Cvoid}}()

# matlab engine functions

const eng_open          = Ref{Ptr{Cvoid}}()
const eng_close         = Ref{Ptr{Cvoid}}()
const eng_set_visible   = Ref{Ptr{Cvoid}}()
const eng_get_visible   = Ref{Ptr{Cvoid}}()
const eng_output_buffer = Ref{Ptr{Cvoid}}()
const eng_eval_string   = Ref{Ptr{Cvoid}}()
const eng_put_variable  = Ref{Ptr{Cvoid}}()
const eng_get_variable  = Ref{Ptr{Cvoid}}()

# mxarray functions

const mx_destroy_array   = Ref{Ptr{Cvoid}}()
const mx_duplicate_array = Ref{Ptr{Cvoid}}()

# functions to access mxarray

const mx_free = Ref{Ptr{Cvoid}}()

const mx_get_classid  = Ref{Ptr{Cvoid}}()
const mx_get_m        = Ref{Ptr{Cvoid}}()
const mx_get_n        = Ref{Ptr{Cvoid}}()
const mx_get_nelems   = Ref{Ptr{Cvoid}}()
const mx_get_ndims    = Ref{Ptr{Cvoid}}()
const mx_get_elemsize = Ref{Ptr{Cvoid}}()
const mx_get_data     = Ref{Ptr{Cvoid}}()
const mx_get_dims     = Ref{Ptr{Cvoid}}()
const mx_get_nfields  = Ref{Ptr{Cvoid}}()
const mx_get_pr       = Ref{Ptr{Cvoid}}()
const mx_get_pi       = Ref{Ptr{Cvoid}}()
const mx_get_ir       = Ref{Ptr{Cvoid}}()
const mx_get_jc       = Ref{Ptr{Cvoid}}()

const mx_is_double = Ref{Ptr{Cvoid}}()
const mx_is_single = Ref{Ptr{Cvoid}}()
const mx_is_int64  = Ref{Ptr{Cvoid}}()
const mx_is_uint64 = Ref{Ptr{Cvoid}}()
const mx_is_int32  = Ref{Ptr{Cvoid}}()
const mx_is_uint32 = Ref{Ptr{Cvoid}}()
const mx_is_int16  = Ref{Ptr{Cvoid}}()
const mx_is_uint16 = Ref{Ptr{Cvoid}}()
const mx_is_int8   = Ref{Ptr{Cvoid}}()
const mx_is_uint8  = Ref{Ptr{Cvoid}}()
const mx_is_char   = Ref{Ptr{Cvoid}}()

const mx_is_numeric = Ref{Ptr{Cvoid}}()
const mx_is_logical = Ref{Ptr{Cvoid}}()
const mx_is_complex = Ref{Ptr{Cvoid}}()
const mx_is_sparse  = Ref{Ptr{Cvoid}}()
const mx_is_empty   = Ref{Ptr{Cvoid}}()
const mx_is_struct  = Ref{Ptr{Cvoid}}()
const mx_is_cell    = Ref{Ptr{Cvoid}}()

# functions to create & delete MATLAB arrays

const mx_create_numeric_matrix = Ref{Ptr{Cvoid}}()
const mx_create_numeric_array  = Ref{Ptr{Cvoid}}()

const mx_create_double_scalar  = Ref{Ptr{Cvoid}}()
const mx_create_logical_scalar = Ref{Ptr{Cvoid}}()

const mx_create_sparse         = Ref{Ptr{Cvoid}}()
const mx_create_sparse_logical = Ref{Ptr{Cvoid}}()

const mx_create_string     = Ref{Ptr{Cvoid}}()
const mx_create_char_array = Ref{Ptr{Cvoid}}()

const mx_create_cell_array = Ref{Ptr{Cvoid}}()

const mx_create_struct_matrix = Ref{Ptr{Cvoid}}()
const mx_create_struct_array  = Ref{Ptr{Cvoid}}()

const mx_get_cell = Ref{Ptr{Cvoid}}()
const mx_set_cell = Ref{Ptr{Cvoid}}()

const mx_get_field       = Ref{Ptr{Cvoid}}()
const mx_set_field       = Ref{Ptr{Cvoid}}()
const mx_get_field_bynum = Ref{Ptr{Cvoid}}()
const mx_get_fieldname   = Ref{Ptr{Cvoid}}()

const mx_get_string = Ref{Ptr{Cvoid}}()

# load I/O mat functions

const mat_open         = Ref{Ptr{Cvoid}}()
const mat_close        = Ref{Ptr{Cvoid}}()
const mat_get_variable = Ref{Ptr{Cvoid}}()
const mat_put_variable = Ref{Ptr{Cvoid}}()
const mat_get_dir      = Ref{Ptr{Cvoid}}()


================================================
FILE: src/matfile.jl
================================================
# mat file open & close

mutable struct MatFile
    ptr::Ptr{Cvoid}
    filename::String

    function MatFile(filename::String, mode::String)
        p = ccall(mat_open[], Ptr{Cvoid}, (Ptr{Cchar}, Ptr{Cchar}), filename, mode)
        self = new(p, filename)
        finalizer(release, self)
        return self
    end
end
MatFile(filename::String) = MatFile(filename, "r")

function unsafe_convert(::Type{Ptr{Cvoid}}, f::MatFile)
    ptr = f.ptr
    ptr == C_NULL && throw(UndefRefError())
    return ptr
end

function release(f::MatFile)
    ptr = f.ptr
    if ptr != C_NULL
        ccall(mat_close[], Cint, (Ptr{Cvoid},), ptr)
    end
    f.ptr = C_NULL
    return nothing
end

function close(f::MatFile)
    ret = ccall(mat_close[], Cint, (Ptr{Cvoid},), f)
    ret != 0 && throw(MEngineError("failed to close file (err = $ret)"))
    f.ptr = C_NULL
    return nothing
end

# get & put variables

function get_mvariable(f::MatFile, name::String)
    pm = ccall(mat_get_variable[], Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cchar}), f, name)
    pm == C_NULL && error("Attempt to get variable $(name) failed.")
    MxArray(pm)
end

get_mvariable(f::MatFile, name::Symbol) = get_mvariable(f, string(name))

get_variable(f::MatFile, name::String) = jvalue(get_mvariable(f, name))
get_variable(f::MatFile, name::Symbol) = jvalue(get_mvariable(f, name))

function put_variable(f::MatFile, name::String, v::MxArray)
    ret = ccall(mat_put_variable[], Cint, (Ptr{Cvoid}, Ptr{Cchar}, Ptr{Cvoid}), f, name, v)
    ret != 0 && error("Attempt to put variable $(name) failed.")
    return nothing
end

put_variable(f::MatFile, name::Symbol, v::MxArray) = put_variable(f, string(name), v)

put_variable(f::MatFile, name::String, v) = put_variable(f, name, mxarray(v))
put_variable(f::MatFile, name::Symbol, v) = put_variable(f, name, mxarray(v))

# operation over entire file

function put_variables(f::MatFile; kwargs...)
    for (name, val) in kwargs
        put_variable(f, name, val)
    end
end

function write_matfile(filename::String; kwargs...)
    mf = MatFile(filename, "w")
    put_variables(mf; kwargs...)
    close(mf)
end

function variable_names(f::MatFile)
    # get a list of all variable names
    _n = Ref{Cint}()
    _a = ccall(mat_get_dir[], Ptr{Ptr{Cchar}}, (Ptr{Cvoid}, Ref{Cint}), f, _n)

    n = Int(_n[])
    a = unsafe_wrap(Array, _a, (n,))

    names = String[unsafe_string(s) for s in a]
    ccall(mx_free[], Cvoid, (Ptr{Cvoid},), _a)
    return names
end

function read_matfile(f::MatFile)
    # return a dictionary of all variables
    names = variable_names(f)
    r = Dict{String,MxArray}()
    sizehint!(r, length(names))
    for nam in names
        r[nam] = get_mvariable(f, nam)
    end
    return r
end

function read_matfile(filename::String)
    f = MatFile(filename, "r")
    r = read_matfile(f)
    close(f)
    return r
end


================================================
FILE: src/matstr.jl
================================================
# Syntax for mat"" string interpolation

# A really basic parser intended only to handle checking whether
# a variable is on the left or right hand side of an expression
mutable struct DumbParserState
    paren_depth::Int
    in_string::Bool
end
DumbParserState() = DumbParserState(0, false)

# Returns true if an = is encountered and updates pstate
function dumb_parse!(pstate::DumbParserState, str::String)
    paren_depth = pstate.paren_depth
    in_string = pstate.in_string
    x = '\0'
    s = firstindex(str)
    while s <= ncodeunits(str)
        lastx = x
        x = str[s]
        s = nextind(str, s)
        if in_string
            if x == '\''
                if (s <= ncodeunits(str)) && (str[s] == '\'')
                    x = str[s]
                    s = nextind(str, s)
                else
                    in_string = false
                end
            end
        else
            if x == '('
                paren_depth += 1
            elseif x == ')'
                paren_depth -= 1
            elseif x == '\'' && lastx in ",( \t\0;"
                in_string = true
            elseif x == '=' && !(lastx in "<>~")
                if (s <= ncodeunits(str)) && (str[s] == '=')
                    x = str[s]
                    s = nextind(str, s)
                else
                    return true
                end
            elseif x == '%'
                break
            end
        end
    end
    pstate.paren_depth = paren_depth
    pstate.in_string = in_string
    return false
end

# Check if a given variable is assigned, used, or both. Returns the#
# assignment and use status
function check_assignment(interp, i)
    # Go back to the last newline
    before = String[]
    for j = (i-1):-1:1
        if isa(interp[j], String)
            sp = split(interp[j], "\n")
            pushfirst!(before, sp[end])
            for k = (length(sp)-1):-1:1
                match(r"\.\.\.[ \t]*\r?$", sp[k]) === nothing && @goto done_before
                pushfirst!(before, sp[k])
            end
        end
    end
    @label done_before

    # Check if this reference is inside parens at the start, or on the rhs of an assignment
    pstate = DumbParserState()
    (dumb_parse!(pstate, join(before)) || pstate.paren_depth > 1) && return (false, true)

    # Go until the next newline or comment
    after = String[]
    both_sides = false
    for j = (i+1):length(interp)
        if isa(interp[j], String)
            sp = split(interp[j], "\n")
            push!(after, sp[1])
            for k = 2:length(sp)
                match(r"\.\.\.[ \t]*\r?$", sp[k-1]) === nothing && @goto done_after
                push!(after, sp[k])
            end
        elseif interp[j] == interp[i]
            both_sides = true
        end
    end
    @label done_after

    assigned = dumb_parse!(pstate, join(after))
    used =
        !assigned || both_sides ||
        (i < length(interp) && match(r"^[ \t]*\(", interp[i+1]) != nothing)
    return (assigned, used)
end

function do_mat_str(ex)
    # Hack to do interpolation
    interp = Meta.parse(string("\"\"\"", replace(ex, "\"\"\"" => "\\\"\"\""), "\"\"\""))
    if isa(interp, String)
        interp = [interp]
    elseif interp.head == :string
        interp = interp.args
    elseif interp.head == :macrocall
        interp = interp.args[2:end]
    else
        throw(ArgumentError("unexpected input"))
    end

    # Handle interpolated variables
    putblock = Expr(:block)
    getblock = Expr(:block)
    usedvars = Set{Symbol}()
    assignedvars = Set{Symbol}()
    varmap = Dict{Symbol,Symbol}()
    for i = 1:length(interp)
        if !isa(interp[i], String)
            # Don't put the same symbol to MATLAB twice
            if haskey(varmap, interp[i])
                var = varmap[interp[i]]
            else
                var = Symbol("matlab_jl_", i)
                if isa(interp[i], Symbol)
                    varmap[interp[i]] = var
                end
            end

            # Try to determine if variable is being used in an assignment
            (assigned, used) = check_assignment(interp, i)

            if used && !(var in usedvars)
                push!(usedvars, var)
                (var in assignedvars) || push!(
                    putblock.args,
                    :(put_variable($(Meta.quot(var)), $(esc(interp[i])))),
                )
            end
            if assigned && !(var in assignedvars)
                push!(assignedvars, var)
                if isa(interp[i], Expr) && (interp[i].head == :ref)
                    # Assignment to a sliced variable, e.g., x[1:3], must use broadcasting in v0.7+
                    push!(
                        getblock.args,
                        Expr(:(.=), esc(interp[i]), :(get_variable($(Meta.quot(var))))),
                    )
                else
                    push!(
                        getblock.args,
                        Expr(:(=), esc(interp[i]), :(get_variable($(Meta.quot(var))))),
                    )
                end
            end

            interp[i] = var
        end
    end

    # Clear `ans` and set `matlab_jl_has_ans` before we run the code
    pushfirst!(interp, "clear ans;\nmatlab_jl_has_ans = 0;\n")

    # Add a semicolon to the end of the last statement to suppress output
    isa(interp[end], String) && (interp[end] = rstrip(interp[end]))
    push!(interp, ";")

    # Figure out if `ans` exists in code to avoid an error if it doesn't
    push!(interp, "\nmatlab_jl_has_ans = exist('ans', 'var');")

    quote
        $(putblock)
        eval_string($(join(interp)))
        $(getblock)
        $(
            if !isempty(usedvars) || !isempty(assignedvars)
                # Clear variables we created
                :(eval_string(
                    $(string("clear ", join(union(usedvars, assignedvars), " "), ";")),
                ))
            end
        )
        if get_variable(:matlab_jl_has_ans) != 0
            # Return ans if it was set
            get_variable(:ans)
        end
    end
end

macro mat_str(ex)
    do_mat_str(ex)
end


================================================
FILE: src/mxarray.jl
================================================
# functions to deal with MATLAB arrays

mutable struct MxArray
    ptr::Ptr{Cvoid}
    own::Bool

    function MxArray(p::Ptr{Cvoid}, own::Bool)
        p == C_NULL && error("NULL pointer for MxArray.")
        self = new(p, own)
        if own
            finalizer(release, self)
        end
        return self
    end
end
MxArray(p::Ptr{Cvoid}) = MxArray(p, true)

mxarray(mx::MxArray) = mx

function release(mx::MxArray)
    if mx.own && mx.ptr != C_NULL
        ccall(mx_destroy_array[], Cvoid, (Ptr{Cvoid},), mx.ptr)
    end
    mx.ptr = C_NULL
    return nothing
end

# delete & copy

function delete(mx::MxArray)
    if mx.own
        ccall(mx_destroy_array[], Cvoid, (Ptr{Cvoid},), mx)
    end
    mx.ptr = C_NULL
    return nothing
end

function copy(mx::MxArray)
    pm = ccall(mx_duplicate_array[], Ptr{Cvoid}, (Ptr{Cvoid},), mx)
    return MxArray(pm)
end

function unsafe_convert(::Type{Ptr{Cvoid}}, mx::MxArray)
    ptr = mx.ptr
    ptr == C_NULL && throw(UndefRefError())
    return ptr
end
# functions to create mxArray from Julia values/arrays

const MxRealNum =
    Union{Float64,Float32,Int32,UInt32,Int64,UInt64,Int16,UInt16,Int8,UInt8,Bool}
const MxComplexNum = Union{ComplexF32,ComplexF64}
const MxNum = Union{MxRealNum,MxComplexNum}

###########################################################
#
#  MATLAB types
#
###########################################################

const mwSize = UInt
const mwIndex = Int

@enum mxClassID::Cint begin
    mxUNKNOWN_CLASS
    mxCELL_CLASS
    mxSTRUCT_CLASS
    mxLOGICAL_CLASS
    mxCHAR_CLASS
    mxVOID_CLASS
    mxDOUBLE_CLASS
    mxSINGLE_CLASS
    mxINT8_CLASS
    mxUINT8_CLASS
    mxINT16_CLASS
    mxUINT16_CLASS
    mxINT32_CLASS
    mxUINT32_CLASS
    mxINT64_CLASS
    mxUINT64_CLASS
    mxFUNCTION_CLASS
    mxOPAQUE_CLASS
    mxOBJECT_CLASS
end

@enum mxComplexity::Cint begin
    mxREAL
    mxCOMPLEX
end

mxclassid(::Type{Bool})                            = mxLOGICAL_CLASS
mxclassid(::Union{Type{Float64},Type{ComplexF64}}) = mxDOUBLE_CLASS
mxclassid(::Union{Type{Float32},Type{ComplexF32}}) = mxSINGLE_CLASS
mxclassid(::Type{Int8})                            = mxINT8_CLASS
mxclassid(::Type{UInt8})                           = mxUINT8_CLASS
mxclassid(::Type{Int16})                           = mxINT16_CLASS
mxclassid(::Type{UInt16})                          = mxUINT16_CLASS
mxclassid(::Type{Int32})                           = mxINT32_CLASS
mxclassid(::Type{UInt32})                          = mxUINT32_CLASS
mxclassid(::Type{Int64})                           = mxINT64_CLASS
mxclassid(::Type{UInt64})                          = mxUINT64_CLASS

mxcomplexflag(::Type{T}) where {T<:MxRealNum}    = mxREAL
mxcomplexflag(::Type{T}) where {T<:MxComplexNum} = mxCOMPLEX

const classid_type_map = Dict{mxClassID,Type}(
    mxLOGICAL_CLASS => Bool,
    mxCHAR_CLASS    => Char,
    mxDOUBLE_CLASS  => Float64,
    mxSINGLE_CLASS  => Float32,
    mxINT8_CLASS    => Int8,
    mxUINT8_CLASS   => UInt8,
    mxINT16_CLASS   => Int16,
    mxUINT16_CLASS  => UInt16,
    mxINT32_CLASS   => Int32,
    mxUINT32_CLASS  => UInt32,
    mxINT64_CLASS   => Int64,
    mxUINT64_CLASS  => UInt64,
)

function mxclassid_to_type(cid::mxClassID)
    ty = get(classid_type_map, cid, nothing)
    ty === nothing && throw(ArgumentError("The input class id is not a primitive type id."))
    return ty
end

###########################################################
#
#  Functions to access mxArray
#
#  Part of the functions (e.g. mxGetNumberOfDimensions)
#  are actually a macro replacement of an internal
#  function name as (xxxx_730)
#
###########################################################

macro mxget_attr(fun, ret, mx)
    :(ccall($(esc(fun)), $(esc(ret)), (Ptr{Cvoid},), $(esc(mx))))
end

classid(mx::MxArray) = @mxget_attr(mx_get_classid[], mxClassID, mx)
nrows(mx::MxArray)   = convert(Int, @mxget_attr(mx_get_m[], UInt, mx))
ncols(mx::MxArray)   = convert(Int, @mxget_attr(mx_get_n[], UInt, mx))
nelems(mx::MxArray)  = convert(Int, @mxget_attr(mx_get_nelems[], UInt, mx))
ndims(mx::MxArray)   = convert(Int, @mxget_attr(mx_get_ndims[], mwSize, mx))

eltype(mx::MxArray) = mxclassid_to_type(classid(mx))
elsize(mx::MxArray) = convert(Int, @mxget_attr(mx_get_elemsize[], UInt, mx))
data_ptr(mx::MxArray) = convert(Ptr{eltype(mx)}, @mxget_attr(mx_get_data[], Ptr{Cvoid}, mx))
real_ptr(mx::MxArray) = convert(Ptr{eltype(mx)}, @mxget_attr(mx_get_pr[], Ptr{Cvoid}, mx))
imag_ptr(mx::MxArray) = convert(Ptr{eltype(mx)}, @mxget_attr(mx_get_pi[], Ptr{Cvoid}, mx))

mxnfields(mx::MxArray) = convert(Int, @mxget_attr(mx_get_nfields[], Cint, mx))

# validation functions

macro mx_test_is(fun, mx)
    :((ccall($(esc(fun)), Bool, (Ptr{Cvoid},), $(esc(mx)))))
end

is_double(mx::MxArray) = @mx_test_is(mx_is_double[], mx)
is_single(mx::MxArray) = @mx_test_is(mx_is_single[], mx)
is_int64(mx::MxArray)  = @mx_test_is(mx_is_int64[], mx)
is_uint64(mx::MxArray) = @mx_test_is(mx_is_uint64[], mx)
is_int32(mx::MxArray)  = @mx_test_is(mx_is_int32[], mx)
is_uint32(mx::MxArray) = @mx_test_is(mx_is_uint32[], mx)
is_int16(mx::MxArray)  = @mx_test_is(mx_is_int16[], mx)
is_uint16(mx::MxArray) = @mx_test_is(mx_is_uint16[], mx)
is_int8(mx::MxArray)   = @mx_test_is(mx_is_int8[], mx)
is_uint8(mx::MxArray)  = @mx_test_is(mx_is_uint8[], mx)

is_numeric(mx::MxArray) = @mx_test_is(mx_is_numeric[], mx)
is_logical(mx::MxArray) = @mx_test_is(mx_is_logical[], mx)
is_complex(mx::MxArray) = @mx_test_is(mx_is_complex[], mx)
is_sparse(mx::MxArray)  = @mx_test_is(mx_is_sparse[], mx)
is_struct(mx::MxArray)  = @mx_test_is(mx_is_struct[], mx)
is_cell(mx::MxArray)    = @mx_test_is(mx_is_cell[], mx)
is_char(mx::MxArray)    = @mx_test_is(mx_is_char[], mx)
is_empty(mx::MxArray)   = @mx_test_is(mx_is_empty[], mx)

# size function

function size(mx::MxArray)
    nd = ndims(mx)
    pdims::Ptr{mwSize} = @mxget_attr(mx_get_dims[], Ptr{mwSize}, mx)
    _dims = unsafe_wrap(Array, pdims, (nd,))
    dims = Vector{Int}(undef, nd)
    for i = 1:nd
        dims[i] = Int(_dims[i])
    end
    tuple(dims...)
end

function size(mx::MxArray, d::Integer)
    d <= 0 && throw(ArgumentError("The dimension must be a positive integer."))

    nd = ndims(mx)
    if nd == 2
        d == 1 ? nrows(mx) :
        d == 2 ? ncols(mx) : 1
    else
        pdims::Ptr{mwSize} = @mxget_attr(mx_get_dims[], Ptr{mwSize}, mx)
        _dims = unsafe_wrap(Array, pdims, (nd,))
        d <= nd ? Int(_dims[d]) : 1
    end
end

###########################################################
#
#  functions to create & delete MATLAB arrays
#
###########################################################

function _dims_to_mwSize(dims::Tuple{Vararg{Integer,N}}) where {N}
    _dims = Vector{mwSize}(undef, N)
    for i = 1:N
        _dims[i] = mwSize(dims[i])
    end
    _dims
end

function mxarray(::Type{T}, dims::Tuple{Vararg{Integer,N}}) where {T<:MxNum,N}
    pm = ccall(mx_create_numeric_array[], Ptr{Cvoid},
        (mwSize, Ptr{mwSize}, mxClassID, mxComplexity),
        N, _dims_to_mwSize(dims), mxclassid(T), mxcomplexflag(T))
    MxArray(pm)
end
mxarray(::Type{T}, dims::Integer...) where {T<:MxNum} = mxarray(T, dims)

# create scalars

function mxarray(x::Float64)
    pm = ccall(mx_create_double_scalar[], Ptr{Cvoid}, (Cdouble,), x)
    MxArray(pm)
end

function mxarray(x::Bool)
    pm = ccall(mx_create_logical_scalar[], Ptr{Cvoid}, (Bool,), x)
    MxArray(pm)
end

function mxarray(x::T) where {T<:MxRealNum}
    pm = ccall(mx_create_numeric_matrix[], Ptr{Cvoid},
        (mwSize, mwSize, mxClassID, mxComplexity),
        1, 1, mxclassid(T), mxcomplexflag(T))

    pdat = ccall(mx_get_data[], Ptr{T}, (Ptr{Cvoid},), pm)

    unsafe_wrap(Array, pdat, (1,))[1] = x
    MxArray(pm)
end
mxarray(x::T) where {T<:MxComplexNum} = mxarray([x])

# conversion from Julia variables to MATLAB
# Note: the conversion is deep-copy, as there is no way to let
# mxArray use Julia array's memory

function mxarray(a::Array{T}) where {T<:MxRealNum}
    mx = mxarray(T, size(a))
    ccall(
        :memcpy,
        Ptr{Cvoid},
        (Ptr{Cvoid}, Ptr{Cvoid}, UInt),
        data_ptr(mx),
        a,
        length(a) * sizeof(T),
    )
    return mx
end

function mxarray(a::Array{T}) where {T<:MxComplexNum}
    mx = mxarray(T, size(a))
    na = length(a)
    rdat = unsafe_wrap(Array, real_ptr(mx), na)
    idat = unsafe_wrap(Array, imag_ptr(mx), na)
    for i = 1:na
        rdat[i] = real(a[i])
        idat[i] = imag(a[i])
    end
    mx
end

function mxarray(a::AbstractArray{T}) where {T<:MxRealNum}
    mx = mxarray(T, size(a))
    ptr = data_ptr(mx)
    na = length(a)
    dat = unsafe_wrap(Array{T}, ptr, na)
    for (i, ix) in enumerate(eachindex(a))
        dat[i] = a[ix]
    end
    return mx
end

function mxarray(a::AbstractArray{T}) where {T<:MxComplexNum}
    mx = mxarray(T, size(a))
    na = length(a)
    rdat = unsafe_wrap(Array, real_ptr(mx), na)
    idat = unsafe_wrap(Array, imag_ptr(mx), na)
    for (i, ix) in enumerate(eachindex(a))
        rdat[i] = real(a[ix])
        idat[i] = imag(a[ix])
    end
    return mx
end

function mxarray(a::NTuple{N,T}) where {N,T<:MxRealNum}
    mx = mxarray(T, N)
    pdat = ccall(mx_get_data[], Ptr{T}, (Ptr{Cvoid},), mx)
    dat = unsafe_wrap(Array, pdat, N)
    for i = 1:N
        dat[i] = a[i]
    end
    return mx
end

function mxarray(a::NTuple{N,T}) where {N,T<:MxComplexNum}
    mx = mxarray(T, size(a))
    na = length(a)
    rdat = unsafe_wrap(Array, real_ptr(mx), na)
    idat = unsafe_wrap(Array, imag_ptr(mx), na)
    for (i, ix) in enumerate(eachindex(a))
        rdat[i] = real(a[ix])
        idat[i] = imag(a[ix])
    end
    return mx
end

function mxarray(a::Tuple)
    mx = mxcellarray(length(a))
    for i = 1:length(a)
        set_cell(mx, i, mxarray(a[i]))
    end
    return mx
end

# sparse matrix

function mxsparse(ty::Type{Float64}, m::Integer, n::Integer, nzmax::Integer)
    pm = ccall(mx_create_sparse[], Ptr{Cvoid},
        (mwSize, mwSize, mwSize, mxComplexity), m, n, nzmax, mxREAL)
    MxArray(pm)
end

function mxsparse(ty::Type{ComplexF64}, m::Integer, n::Integer, nzmax::Integer)
    pm = ccall(mx_create_sparse[], Ptr{Cvoid},
        (mwSize, mwSize, mwSize, mxComplexity), m, n, nzmax, mxCOMPLEX)
    MxArray(pm)
end

function mxsparse(ty::Type{Bool}, m::Integer, n::Integer, nzmax::Integer)
    pm = ccall(mx_create_sparse_logical[], Ptr{Cvoid},
        (mwSize, mwSize, mwSize), m, n, nzmax)
    MxArray(pm)
end

function _copy_sparse_mat(
    a::SparseMatrixCSC{V,I},
    ir_p::Ptr{mwIndex},
    jc_p::Ptr{mwIndex},
    pr_p::Ptr{Float64},
    pi_p::Ptr{Float64},
) where {V<:ComplexF64,I}
    colptr::Vector{I} = a.colptr
    rinds::Vector{I} = a.rowval
    vr::Vector{Float64} = real(a.nzval)
    vi::Vector{Float64} = imag(a.nzval)
    n::Int = a.n
    nnz::Int = length(vr)

    # Note: ir and jc contain zero-based indices

    ir = unsafe_wrap(Array, ir_p, (nnz,))
    for i = 1:nnz
        ir[i] = rinds[i] - 1
    end

    jc = unsafe_wrap(Array, jc_p, (n + 1,))
    for i = 1:(n+1)
        jc[i] = colptr[i] - 1
    end

    copyto!(unsafe_wrap(Array, pr_p, (nnz,)), vr)
    copyto!(unsafe_wrap(Array, pi_p, (nnz,)), vi)
end

function _copy_sparse_mat(
    a::SparseMatrixCSC{V,I},
    ir_p::Ptr{mwIndex},
    jc_p::Ptr{mwIndex},
    pr_p::Ptr{V},
) where {V,I}
    colptr::Vector{I} = a.colptr
    rinds::Vector{I} = a.rowval
    v::Vector{V} = a.nzval
    n::Int = a.n
    nnz::Int = length(v)

    # Note: ir and jc contain zero-based indices

    ir = unsafe_wrap(Array, ir_p, (nnz,))
    for i = 1:nnz
        ir[i] = rinds[i] - 1
    end

    jc = unsafe_wrap(Array, jc_p, (n + 1,))
    for i = 1:(n+1)
        jc[i] = colptr[i] - 1
    end

    copyto!(unsafe_wrap(Array, pr_p, (nnz,)), v)
end

function mxarray(a::SparseMatrixCSC{V,I}) where {V<:Union{Float64,ComplexF64,Bool},I}
    m::Int = a.m
    n::Int = a.n
    nnz = length(a.nzval)
    @assert nnz == a.colptr[n+1] - 1

    mx = mxsparse(V, m, n, nnz)
    ir_p = ccall(mx_get_ir[], Ptr{mwIndex}, (Ptr{Cvoid},), mx)
    jc_p = ccall(mx_get_jc[], Ptr{mwIndex}, (Ptr{Cvoid},), mx)

    if V <: ComplexF64
        pr_p = ccall(mx_get_pr[], Ptr{Float64}, (Ptr{Cvoid},), mx)
        pi_p = ccall(mx_get_pi[], Ptr{Float64}, (Ptr{Cvoid},), mx)
        _copy_sparse_mat(a, ir_p, jc_p, pr_p, pi_p)
    else
        pr_p = ccall(mx_get_pr[], Ptr{V}, (Ptr{Cvoid},), mx)
        _copy_sparse_mat(a, ir_p, jc_p, pr_p)
    end
    return mx
end

# char arrays and string

function mxarray(s::String)
    utf16string = transcode(UInt16, s)
    pm = ccall(mx_create_char_array[], Ptr{Cvoid}, (mwSize, Ptr{mwSize}), 2,
        _dims_to_mwSize((1, length(utf16string))))
    mx = MxArray(pm)
    ccall(:memcpy, Ptr{Cvoid}, (Ptr{Cvoid}, Ptr{Cvoid}, UInt), data_ptr(mx), utf16string,
        length(utf16string) * sizeof(UInt16))
    return mx
end

# cell arrays

function mxcellarray(dims::Tuple{Vararg{Integer,N}}) where {N}
    pm = ccall(mx_create_cell_array[], Ptr{Cvoid}, (mwSize, Ptr{mwSize}),
        N, _dims_to_mwSize(dims))
    MxArray(pm)
end
mxcellarray(dims::Integer...) = mxcellarray(dims)

function get_cell(mx::MxArray, i::Integer)
    pm = ccall(mx_get_cell[], Ptr{Cvoid}, (Ptr{Cvoid}, mwIndex), mx, i - 1)
    MxArray(pm, false)
end

function set_cell(mx::MxArray, i::Integer, v::MxArray)
    v.own = false
    ccall(mx_set_cell[], Cvoid, (Ptr{Cvoid}, mwIndex, Ptr{Cvoid}), mx, i - 1, v)
    return nothing
end

function mxcellarray(a::Array)
    pm = mxcellarray(size(a))
    for i = 1:length(a)
        set_cell(pm, i, mxarray(a[i]))
    end
    return pm
end

mxarray(a::Array) = mxcellarray(a)

# struct arrays

function _fieldname_array(fieldnames::String...)
    n = length(fieldnames)
    a = Vector{Ptr{UInt8}}(undef, n)
    for i = 1:n
        a[i] = unsafe_convert(Ptr{UInt8}, fieldnames[i])
    end
    return a
end

function mxstruct(fns::Vector{String})
    a = _fieldname_array(fns...)
    pm = ccall(mx_create_struct_matrix[], Ptr{Cvoid},
        (mwSize, mwSize, Cint, Ptr{Ptr{UInt8}}), 1, 1, length(a), a)
    MxArray(pm)
end

function mxstruct(fn1::String, fnr::String...)
    a = _fieldname_array(fn1, fnr...)
    pm = ccall(mx_create_struct_matrix[], Ptr{Cvoid},
        (mwSize, mwSize, Cint, Ptr{Ptr{UInt8}}), 1, 1, length(a), a)
    MxArray(pm)
end

function set_field(mx::MxArray, i::Integer, f::String, v::MxArray)
    v.own = false
    ccall(
        mx_set_field[],
        Cvoid,
        (Ptr{Cvoid}, mwIndex, Ptr{UInt8}, Ptr{Cvoid}),
        mx,
        i - 1,
        f,
        v,
    )
    return nothing
end

set_field(mx::MxArray, f::String, v::MxArray) = set_field(mx, 1, f, v)

function get_field(mx::MxArray, i::Integer, f::String)
    pm = ccall(mx_get_field[], Ptr{Cvoid}, (Ptr{Cvoid}, mwIndex, Ptr{UInt8}), mx, i - 1, f)
    pm == C_NULL && throw(ArgumentError("Failed to get field."))
    MxArray(pm, false)
end

get_field(mx::MxArray, f::String) = get_field(mx, 1, f)

function get_field(mx::MxArray, i::Integer, fn::Integer)
    pm = ccall(
        mx_get_field_bynum[],
        Ptr{Cvoid},
        (Ptr{Cvoid}, mwIndex, Cint),
        mx,
        i - 1,
        fn - 1,
    )
    pm == C_NULL && throw(ArgumentError("Failed to get field."))
    MxArray(pm, false)
end

get_field(mx::MxArray, fn::Integer) = get_field(mx, 1, fn)

function get_fieldname(mx::MxArray, i::Integer)
    p = ccall(mx_get_fieldname[], Ptr{UInt8}, (Ptr{Cvoid}, Cint), mx, i - 1)
    unsafe_string(p)
end

const Pairs = Union{Pair,NTuple{2}}

function mxstruct(pairs::Pairs...)
    nf = length(pairs)
    fieldnames = Vector{String}(undef, nf)
    for i = 1:nf
        fn = pairs[i][1]
        fieldnames[i] = string(fn)
    end
    mx = mxstruct(fieldnames)
    for i = 1:nf
        set_field(mx, fieldnames[i], mxarray(pairs[i][2]))
    end
    return mx
end

function mxstruct(d::T) where {T}
    names = fieldnames(T)
    names_str = map(string, names)
    mx = mxstruct(names_str...)
    for i = 1:length(names)
        set_field(mx, names_str[i], mxarray(getfield(d, names[i])))
    end
    return mx
end

function mxstructarray(d::Array{T}) where {T}
    names = fieldnames(T)
    names_str = map(string, names)
    a = _fieldname_array(names_str...)

    pm = ccall(mx_create_struct_array[], Ptr{Cvoid},
        (mwSize, Ptr{mwSize}, Cint,
            Ptr{Ptr{UInt8}}), ndims(d), _dims_to_mwSize(size(d)), length(a), a)
    mx = MxArray(pm)

    for i = 1:length(d), j = 1:length(names)
        set_field(mx, i, names_str[j], mxarray(getfield(d[i], names[j])))
    end
    return mx
end

mxstruct(d::AbstractDict) = mxstruct(collect(d)...)
mxarray(d) = mxstruct(d)

###########################################################
#
#  convert from MATLAB to Julia
#
###########################################################

# use deep-copy from MATLAB variable to Julia array
# in practice, MATLAB variable often has shorter life-cycle

function _jarrayx(fun::String, mx::MxArray, siz::Tuple)
    if is_numeric(mx) || is_logical(mx)
        @assert !is_sparse(mx)
        T = eltype(mx)
        if is_complex(mx)
            rdat = unsafe_wrap(Array, real_ptr(mx), siz)
            idat = unsafe_wrap(Array, imag_ptr(mx), siz)
            a = complex.(rdat, idat)
        else
            a = Array{T}(undef, siz)
            if !isempty(a)
                ccall(
                    :memcpy,
                    Ptr{Cvoid},
                    (Ptr{Cvoid}, Ptr{Cvoid}, UInt),
                    a,
                    data_ptr(mx),
                    length(a) * sizeof(T),
                )
            end
        end
        return a
        #unsafe_wrap(Array, data_ptr(mx), siz)
    elseif is_cell(mx)
        a = Array{Any}(undef, siz)
        for i = 1:length(a)
            a[i] = jvalue(get_cell(mx, i))
        end
        return a
    else
        throw(ArgumentError("$(fun) only applies to numeric, logical or cell arrays."))
    end
end

jarray(mx::MxArray) = _jarrayx("jarray", mx, size(mx))
jvector(mx::MxArray) = _jarrayx("jvector", mx, (nelems(mx),))

function jmatrix(mx::MxArray)
    if ndims(mx) != 2
        throw(ArgumentError("jmatrix only applies to MATLAB arrays with ndims == 2."))
    end
    return _jarrayx("jmatrix", mx, (nrows(mx), ncols(mx)))
end

function jscalar(mx::MxArray)
    if !(nelems(mx) == 1 && (is_logical(mx) || is_numeric(mx)))
        throw(
            ArgumentError(
                "jscalar only applies to numeric or logical arrays with exactly one element.",
            ),
        )
    end
    @assert !is_sparse(mx)
    if is_complex(mx)
        return unsafe_wrap(Array, real_ptr(mx), (1,))[1] +
               im * unsafe_wrap(Array, imag_ptr(mx), (1,))[1]
    else
        return unsafe_wrap(Array, data_ptr(mx), (1,))[1]
    end
end

function _jsparse(ty::Type{T}, mx::MxArray) where {T<:MxRealNum}
    m = nrows(mx)
    n = ncols(mx)
    ir_ptr = ccall(mx_get_ir[], Ptr{mwIndex}, (Ptr{Cvoid},), mx)
    jc_ptr = ccall(mx_get_jc[], Ptr{mwIndex}, (Ptr{Cvoid},), mx)

    jc_a::Vector{mwIndex} = unsafe_wrap(Array, jc_ptr, (n + 1,))
    nnz = jc_a[n+1]

    ir = Vector{Int}(undef, nnz)
    jc = Vector{Int}(undef, n + 1)

    ir_x = unsafe_wrap(Array, ir_ptr, (nnz,))
    for i = 1:nnz
        ir[i] = ir_x[i] + 1
    end

    jc_x = unsafe_wrap(Array, jc_ptr, (n + 1,))
    for i = 1:(n+1)
        jc[i] = jc_x[i] + 1
    end

    pr_ptr = ccall(mx_get_pr[], Ptr{T}, (Ptr{Cvoid},), mx)
    pr::Vector{T} = copy(unsafe_wrap(Array, pr_ptr, (nnz,)))
    if is_complex(mx)
        pi_ptr = ccall(mx_get_pi[], Ptr{T}, (Ptr{Cvoid},), mx)
        pi::Vector{T} = copy(unsafe_wrap(Array, pi_ptr, (nnz,)))
        return SparseMatrixCSC(m, n, jc, ir, pr + im .* pi)
    else
        return SparseMatrixCSC(m, n, jc, ir, pr)
    end
end

function jsparse(mx::MxArray)
    if !is_sparse(mx)
        throw(ArgumentError("jsparse only applies to sparse matrices."))
    end
    return _jsparse(eltype(mx), mx)
end

function String(mx::MxArray)
    if !(
        classid(mx) == mxCHAR_CLASS && ((ndims(mx) == 2 && nrows(mx) == 1) || is_empty(mx))
    )
        throw(
            ArgumentError(
                "String(mx::MxArray) only applies to strings (i.e. char vectors)",
            ),
        )
    end
    return transcode(String, unsafe_wrap(Array, Ptr{UInt16}(data_ptr(mx)), ncols(mx)))
end

function Dict(mx::MxArray)
    if !(is_struct(mx) && nelems(mx) == 1)
        throw(ArgumentError("Dict(mx::MxArray) only applies to a single struct"))
    end
    nf = mxnfields(mx)
    fnames = Vector{String}(undef, nf)
    fvals = Vector{Any}(undef, nf)
    for i = 1:nf
        fnames[i] = get_fieldname(mx, i)
        pv =
            ccall(
                mx_get_field_bynum[],
                Ptr{Cvoid},
                (Ptr{Cvoid}, mwIndex, Cint),
                mx,
                0,
                i - 1,
            )
        fx = MxArray(pv, false)
        fvals[i] = jvalue(fx)
    end
    Dict(zip(fnames, fvals))
end

function jvalue(mx::MxArray)
    if is_numeric(mx) || is_logical(mx)
        if !is_sparse(mx)
            nelems(mx) == 1 ? jscalar(mx) :
            ndims(mx) == 2 ? (ncols(mx) == 1 ? jvector(mx) : jmatrix(mx)) :
            jarray(mx)
        else
            jsparse(mx)
        end
    elseif is_char(mx) && (nrows(mx) == 1 || is_empty(mx))
        String(mx)
    elseif is_cell(mx)
        ndims(mx) == 2 ? (ncols(mx) == 1 ? jvector(mx) : jmatrix(mx)) :
        jarray(mx)
    elseif is_struct(mx) && nelems(mx) == 1
        Dict(mx)
    else
        throw(ArgumentError("Unsupported kind of variable."))
    end
end

# deep conversion from MATLAB variable to Julia array

jvalue(mx::MxArray, ::Type{Array}) = jarray(mx)
jvalue(mx::MxArray, ::Type{Vector}) = jvector(mx)
jvalue(mx::MxArray, ::Type{Matrix}) = jmatrix(mx)
jvalue(mx::MxArray, ::Type{Number}) = jscalar(mx)::Number
jvalue(mx::MxArray, ::Type{String}) = String(mx)
jvalue(mx::MxArray, ::Type{Dict}) = Dict(mx)
jvalue(mx::MxArray, ::Type{SparseMatrixCSC}) = jsparse(mx)

# legacy support (eventually drop, when all constructors added)
jdict(mx::MxArray) = Dict(mx)
jstring(mx::MxArray) = String(mx)


================================================
FILE: test/engine.jl
================================================
using MATLAB
using Test
using SparseArrays

# test engine

restart_default_msession()

a = [1.0 2.0 3.0; 4.0 5.0 6.0]
b = [2.0 3.0 4.0; 8.0 7.0 6.0]

@mput a b
mat"""
    r1 = a .* b;
    r2 = a + b;
"""
@mget r1 r2

@test isequal(r1, a .* b)
@test isequal(r2, a + b)

@mget r1::Vector
@test isequal(r1, vec(a .* b))

s = sparse([1.0 0.0 0.0; 2.0 3.0 0.0; 0.0 4.0 5.0])
put_variable(:s, s)
s2 = get_variable(:s)
@test isequal(s, s2)

# mxcall

r = mxcall(:plus, 1, a, b)
@test isequal(r, a + b)

(xx, yy) = mxcall(:meshgrid, 2, [1.0, 2.0], [3.0, 4.0])
@test isequal(xx, [1.0 2.0; 1.0 2.0])
@test isequal(yy, [3.0 3.0; 4.0 4.0])

close_default_msession()

# test for segfault

s = MSession()
close(s)
@test_throws UndefRefError close(s)

# segfault on deleted references
x = mxarray(3.0)
delete(x)
@test_throws UndefRefError delete(x)
@test_throws UndefRefError nrows(x)
@test_throws UndefRefError is_numeric(x)
@test_throws UndefRefError jscalar(x)
@test_throws UndefRefError jvalue(x)

# make sure restart_default_msession() doesn't error on null references on
# default msession
s = get_default_msession()
close(s)
restart_default_msession()


================================================
FILE: test/matfile.jl
================================================
using MATLAB
using Test

# test MMAT file I/O
fn = "$(tempname()).mat"

a32 = Int32[1 2 3; 4 5 6]
a64 = Int64[1 2 3; 4 5 6]
b = [1.2, 3.4, 5.6, 7.8]
c = [[0.0, 1.0], [1.0, 2.0], [1.0, 2.0, 3.0]]
d = Dict("name" => "MATLAB", "score" => 100.0)

struct S
    x::Float64
    y::Bool
    z::Vector{Float64}
end

ss = S[S(1.0, true, [1.0, 2.0]), S(2.0, false, [3.0, 4.0])]

write_matfile(fn; a32=a32, a64=a64, b=b, c=c, d=d, ss=mxstructarray(ss))

r = read_matfile(fn)
@test isa(r, Dict{String,MxArray})
@test length(r) == 6

ra32 = jmatrix(r["a32"])
ra64 = jmatrix(r["a64"])
rb = jvector(r["b"])
rc = jvalue(r["c"])
rd = jdict(r["d"])
rss = r["ss"]

GC.gc()  # make sure that ra, rb, rc, rd remain valid

@test ra32 == a32
@test ra64 == a64
@test rb == b
@test rc == c

@test rd["name"] == d["name"]
@test rd["score"] == d["score"]

@test is_struct(rss)
@test jscalar(get_field(rss, 1, "x")) == 1.0
@test jscalar(get_field(rss, 1, "y"))
@test jvector(get_field(rss, 1, "z")) == ss[1].z
@test jscalar(get_field(rss, 2, "x")) == 2.0
@test !jscalar(get_field(rss, 2, "y"))
@test jvector(get_field(rss, 2, "z")) == ss[2].z

# segfault on deleted references
s = MatFile(fn)
close(s)
@test_throws UndefRefError close(s)

rm(fn)


================================================
FILE: test/matstr.jl
================================================
using MATLAB
using Test

@test mat"1" == 1
@test mat"[1, 2, 3]" == [1 2 3]

# Test interpolation
x = 1
@test mat"$x + 1" == 2

ret = mat"$y = $x + 2"
@test ret === nothing
@test y == 3

ret = mat"$y = $(x + 3)"
@test ret === nothing
@test y == 4

x = 5
@test mat"$x == 5"

# Test assignment
x = [1, 2, 3, 4, 5]
ret = mat"$x(1:3) = 1"
@test ret === nothing
@test x == [1, 1, 1, 4, 5]
ret = mat"$(x[1:3]) = 2"
@test ret === nothing
@test x == [2, 2, 2, 4, 5]

# Test a more complicated case with assignments on LHS and RHS
x = 20
mat"""
for i = 1:10
   $x = $x + 1;
end
"""

# Test assignment then use
ret = mat"""
$z = 5;
$q = $z;
"""
@test ret === nothing
@test z == 5
@test q == 5

# Test multiple assignment
ret = mat"[$a, $b] = sort([4, 3])"
@test ret === nothing
@test a == [3 4]
@test b == [2 1]

# Test comments
a = 5
@test mat"$a + 1; % = 2" == 6

# Test indexing
c = [1, 2]
@test mat"$c($c == 2)" == 2

# Test line continuations
ret = mat"""
$d ...
= 3
"""
@test ret === nothing
@test d == 3

# Test strings with =
text = "hello = world"
@test mat"strfind($text, 'o = w')" == 5

@testset "Propagate Matlab Exceptions" begin

    # Checks should be enabled by default
    @test MATLAB.has_exception_check_enabled() == true

    # Test invalid command
    @test_throws MATLAB.MatlabException mat"invalid_command"

    # Test invalid assignment
    @test_throws MATLAB.MatlabException mat"1 = 2"

    # Test invalid command within a block
    @test_throws MATLAB.MatlabException mat"""
    xyz = 1 + 2;
    invalid_command;
    abc = 2 * xyz;
    """

    # Disable Checks
    MATLAB.disable_exception_check!()
    @test MATLAB.has_exception_check_enabled() == false

    # Test invalid command
    try
        mat"invalid_command"
    catch ex
        @test false # should not throw an exception
    end
end


================================================
FILE: test/mxarray.jl
================================================
using MATLAB
using Test
using SparseArrays

# Unit testing for MxArray

m = 5
n = 6

# test basic types in 1D & 2D

macro mx_test_basic_types(ty, testfun)
    quote
        a = mxarray($(ty), n)
        @test elsize(a) == sizeof($(ty))
        @test eltype(a) === $(ty)
        @test nrows(a) == n
        @test ncols(a) == 1
        @test nelems(a) == n
        @test ndims(a) == 2
        @test size(a) == (n, 1)
        @test size(a, 1) == n
        @test size(a, 2) == 1
        @test size(a, 3) == 1
        @test !is_complex(a)
        @test $(testfun)(a)
        delete(a)

        b = mxarray($(ty), m, n)
        @test elsize(b) == sizeof($(ty))
        @test eltype(b) === $(ty)
        @test nrows(b) == m
        @test ncols(b) == n
        @test nelems(b) == m * n
        @test ndims(b) == 2
        @test size(b) == (m, n)
        @test size(b, 1) == m
        @test size(b, 2) == n
        @test size(b, 3) == 1
        @test !is_complex(b)
        @test $(testfun)(b)
        delete(b)
        return nothing
    end
end

# empty array

a = mxarray(Float64, 0, 0)
@test nrows(a) == 0
@test ncols(a) == 0
@test nelems(a) == 0
@test ndims(a) == 2
@test eltype(a) == Float64
@test is_empty(a)

# basic arrays

@mx_test_basic_types Float64 is_double
@mx_test_basic_types Float32 is_single
@mx_test_basic_types Int64 is_int64
@mx_test_basic_types UInt64 is_uint64
@mx_test_basic_types Int32 is_int32
@mx_test_basic_types UInt32 is_uint32
@mx_test_basic_types Int16 is_int16
@mx_test_basic_types UInt16 is_uint16
@mx_test_basic_types Int8 is_int8
@mx_test_basic_types UInt8 is_uint8
@mx_test_basic_types Bool is_logical

# complex arrays

macro mx_test_complex_type(ty, testfun)
    quote
        b = mxarray(Complex{$(ty)}, m, n)
        @test elsize(b) == sizeof($(ty))
        @test eltype(b) === $(ty)
        @test nrows(b) == m
        @test ncols(b) == n
        @test nelems(b) == m * n
        @test ndims(b) == 2
        @test size(b) == (m, n)
        @test size(b, 1) == m
        @test size(b, 2) == n
        @test size(b, 3) == 1
        @test is_complex(b)
        @test $(testfun)(b)
        delete(b)
        return nothing
    end
end
@mx_test_complex_type Float64 is_double
@mx_test_complex_type Float32 is_single

# test creating multi-dimensional arrays

a = mxarray(Float64, (6, 5, 4))
@test elsize(a) == sizeof(Float64)
@test eltype(a) === Float64
@test size(a) == (6, 5, 4)
@test size(a, 1) == 6
@test size(a, 2) == 5
@test size(a, 3) == 4
@test size(a, 4) == 1
@test nelems(a) == 6 * 5 * 4
@test is_numeric(a)
@test !is_sparse(a)

a = mxarray(Bool, (6, 5, 4))
@test elsize(a) == 1
@test eltype(a) === Bool
@test size(a) == (6, 5, 4)
@test size(a, 1) == 6
@test size(a, 2) == 5
@test size(a, 3) == 4
@test size(a, 4) == 1
@test nelems(a) == 6 * 5 * 4
@test is_logical(a)
@test !is_sparse(a)

# scalars

a_mx = mxarray(3.25)
@test eltype(a_mx) == Float64
@test size(a_mx) == (1, 1)
@test jscalar(a_mx) == 3.25
delete(a_mx)

a_mx = mxarray(Int32(12))
@test eltype(a_mx) == Int32
@test size(a_mx) == (1, 1)
@test jscalar(a_mx) == Int32(12)
delete(a_mx)

a_mx = mxarray(true)
@test eltype(a_mx) == Bool
@test size(a_mx) == (1, 1)
@test jscalar(a_mx)
delete(a_mx)

a_mx = mxarray(false)
@test eltype(a_mx) == Bool
@test size(a_mx) == (1, 1)
@test !jscalar(a_mx)
delete(a_mx)

a_mx = mxarray(3.25 + 4im)
@test eltype(a_mx) == Float64
@test size(a_mx) == (1, 1)
@test jscalar(a_mx) == 3.25 + 4im
delete(a_mx)

# conversion between Julia and MATLAB numeric arrays

a = rand(5, 6)
a_mx = mxarray(a)
a2 = jarray(a_mx)
@test isequal(a, a2)
delete(a_mx)

a = rand(5)
a_mx = mxarray(a)
a2 = jvector(a_mx)
@test isequal(a, a2)
delete(a_mx)

a_t = reshape(a, 1, 5)
a_mx = mxarray(a_t)
a2 = jvector(a_mx)
@test isequal(a, a2)
delete(a_mx)

a = 1:5
a_mx = mxarray(a)
a2 = jvector(a_mx)
@test isequal([1:5;], a2)
delete(a_mx)

a = rand(5, 6) + rand(5, 6) * im
a_mx = mxarray(a)
a2 = jarray(a_mx)
@test isequal(a, a2)
delete(a_mx)

# sparse matrices

a = sprand(8, 9, 0.2)
a_mx = mxarray(a)
@test is_double(a_mx)
@test is_sparse(a_mx)
@test nrows(a_mx) == 8
@test ncols(a_mx) == 9

a2 = jsparse(a_mx)
@test size(a2) == (8, 9)
@test count(!iszero, a2) == count(!iszero, a)
@test isequal(a2, a)
delete(a_mx)

a = sparse(convert(Array{Bool}, rand(8, 9) .< 0.3))
a_mx = mxarray(a)
@test is_logical(a_mx)
@test is_sparse(a_mx)
@test nrows(a_mx) == 8
@test ncols(a_mx) == 9

a2 = jsparse(a_mx)
@test size(a2) == (8, 9)
@test count(!iszero, a2) == count(!iszero, a)
@test isequal(a2, a)
delete(a_mx)

a = sparse([1.0 1.0im])
a_mx = mxarray(a)
@test is_sparse(a_mx)
@test is_double(a_mx)
@test is_complex(a_mx)
@test nrows(a_mx) == 1
@test ncols(a_mx) == 2
delete(a_mx)

# strings

s = "MATLAB.jl"
s_mx = mxarray(s)
@test classid(s_mx) == MATLAB.mxCHAR_CLASS
@test nrows(s_mx) == 1
@test ncols(s_mx) == length(s)
@test nelems(s_mx) == length(s)
@test ndims(s_mx) == 2
@test is_char(s_mx)

s2 = jstring(s_mx)
@test s == s2
delete(s_mx)

s = ""
s_mx = mxarray(s)
@test classid(s_mx) == MATLAB.mxCHAR_CLASS
@test is_char(s_mx)
@test is_empty(s_mx)

s2 = jstring(s_mx)
@test s == s2
delete(s_mx)

# cell arrays

a = mxcellarray(10)
@test nrows(a) == 10
@test ncols(a) == 1
@test nelems(a) == 10
@test classid(a) == MATLAB.mxCELL_CLASS
@test is_cell(a)
delete(a)

a = mxcellarray(4, 5)
@test nrows(a) == 4
@test ncols(a) == 5
@test nelems(a) == 20
@test classid(a) == MATLAB.mxCELL_CLASS
@test is_cell(a)
delete(a)

a = mxcellarray((3, 4, 5))
@test size(a) == (3, 4, 5)
@test nelems(a) == 60
@test classid(a) == MATLAB.mxCELL_CLASS
@test is_cell(a)
delete(a)

s = ["abc", "efg"]
s_mx = mxcellarray(s)
@test jstring(get_cell(s_mx, 1)) == "abc"
@test jstring(get_cell(s_mx, 2)) == "efg"
delete(s_mx)

# struct

a = mxstruct("abc", "efg", "xyz")
@test is_struct(a)
@test mxnfields(a) == 3
@test nrows(a) == 1
@test ncols(a) == 1
@test nelems(a) == 1
@test ndims(a) == 2

@test get_fieldname(a, 1) == "abc"
@test get_fieldname(a, 2) == "efg"
@test get_fieldname(a, 3) == "xyz"
delete(a)

s = Dict("name" => "MATLAB", "version" => 12.0, "data" => [1, 2, 3])
a = mxstruct(s)
@test is_struct(a)
@test mxnfields(a) == 3
@test jstring(get_field(a, "name")) == "MATLAB"
@test jscalar(get_field(a, "version")) == 12.0
@test isequal(jvector(get_field(a, "data")), [1, 2, 3])
delete(a)

mutable struct TestType
    name::String
    version::Float64
    data::Vector{Int}
end
t = TestType("MATLAB", 12.0, [1, 2, 3])
a = mxstruct(t)
@test is_struct(a)
@test mxnfields(a) == 3
@test jstring(get_field(a, "name")) == "MATLAB"
@test jscalar(get_field(a, "version")) == 12.0
@test isequal(jvector(get_field(a, "data")), [1, 2, 3])
delete(a)

a = mxstructarray([TestType("MATLAB", 12.0, [1, 2, 3]),
    TestType("Julia", 0.2, [4, 5, 6])])
@test is_struct(a)
@test mxnfields(a) == 3
@test jstring(get_field(a, 1, "name")) == "MATLAB"
@test jscalar(get_field(a, 1, "version")) == 12.0
@test isequal(jvector(get_field(a, 1, "data")), [1, 2, 3])
@test jstring(get_field(a, 2, "name")) == "Julia"
@test jscalar(get_field(a, 2, "version")) == 0.2
@test isequal(jvector(get_field(a, 2, "data")), [4, 5, 6])
delete(a)

# bi-directional conversions

x = mxarray(12.0)
y = jvalue(x)
delete(x)
@test isa(y, Float64)
@test y == 12.0

a = rand(5)
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Vector{Float64})
@test isequal(y, a)

a = rand(3, 4)
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Matrix{Float64})
@test isequal(y, a)

a = rand(3, 4, 5)
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Array{Float64,3})
@test isequal(y, a)

a = sparse([1.0 2.0im; 0 -1.0im])
a_mx = mxarray(a)
a_jl = jvalue(a_mx)
delete(a_mx)
@test a == a_jl
@test isa(a_jl, SparseMatrixCSC{Complex{Float64}})

##############################
# Abstract Array Conversions
##############################

a = transpose(rand(10))
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Array{Float64,2})
@test size(y) == size(a)
@test isequal(y, a)

a = rand(10, 10)
a_ = @view a[3:7, 4:8]
x = mxarray(a_)
y = jvalue(x)
delete(x)
@test isa(y, Array{Float64,2})
@test size(y) == size(a_)
@test isequal(y, a_)

a_ = rand(ComplexF32, 10, 10, 10)
a = @view a_[3:7, 4:8, 2:5]
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Array{ComplexF32,3})
@test size(y) == size(a)

a = 1:100 # range
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Array{Int64,1})
@test isequal(y, collect(a))

a = BitArray(rand(Bool, 5, 20, 10))
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Array{Bool,3})
@test isequal(y, a)

# Issue: Tuples converted to MATLAB structs
# https://github.com/JuliaInterop/MATLAB.jl/issues/178
a = (2.5, 2.6)
x = mxarray(a)
y = jvalue(x)
@test classid(x) == MATLAB.mxDOUBLE_CLASS
@test nrows(x) == 2
@test ncols(x) == 1
delete(x)
@test isa(y, Vector{Float64})
@test isequal(y, collect(a))

# Tuple with mixed types
a = (1, 2.0, "MATLAB", [1, 2, 3])
x = mxarray(a)
y = jvalue(x)
@test nrows(x) == 4
@test ncols(x) == 1
@test classid(x) == MATLAB.mxCELL_CLASS
@test isa(y, Vector{Any})
@test length(y) == length(a)
@test isequal(y, collect(a))

##############################
# String Conversions
##############################

a = "MATLAB"
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, String)
@test y == a

a = ["abc", 3, "efg"]
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Vector{Any})
@test length(y) == 3
@test y[1] == a[1]
@test y[2] == a[2]
@test y[3] == a[3]

a = Dict("abc" => 10.0, "efg" => [1, 2, 3], "xyz" => "MATLAB")
x = mxarray(a)
y = jvalue(x)
delete(x)
@test isa(y, Dict{String,Any})

@test y["abc"] == 10.0
@test isequal(y["efg"], [1, 2, 3])
@test y["xyz"] == "MATLAB"

# Test string encoding
str = "λ α γ"
@test jstring(mxarray(str)) == str
@test mat"all($str == [955 32 945 32 947])"

GC.gc()


================================================
FILE: test/runtests.jl
================================================
using MATLAB
using Test

is_ci() = lowercase(get(ENV, "CI", "false")) == "true"

if !is_ci() # only test if not CI
    include("engine.jl")
    include("matfile.jl")
    include("matstr.jl")
    include("mxarray.jl")
end
Download .txt
gitextract_utsjq7lg/

├── .JuliaFormatter.toml
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── CI.yml
│       ├── Format.yml
│       ├── RegisterAction.yml
│       └── TagBot.yml
├── .gitignore
├── LICENSE.md
├── Project.toml
├── README.md
├── deps/
│   └── build.jl
├── src/
│   ├── MATLAB.jl
│   ├── engine.jl
│   ├── exceptions.jl
│   ├── init.jl
│   ├── matfile.jl
│   ├── matstr.jl
│   └── mxarray.jl
└── test/
    ├── engine.jl
    ├── matfile.jl
    ├── matstr.jl
    ├── mxarray.jl
    └── runtests.jl
Condensed preview — 23 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (97K chars).
[
  {
    "path": ".JuliaFormatter.toml",
    "chars": 295,
    "preview": "whitespace_typedefs = false\nwhitespace_ops_in_indices = false\nwhitespace_in_kwargs = false\nremove_extra_newlines = true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 256,
    "preview": "# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates\nversion: 2\nupda"
  },
  {
    "path": ".github/workflows/CI.yml",
    "chars": 728,
    "preview": "name: CI\r\n\r\non:\r\n  push:\r\n    branches:\r\n      - master\r\n      - release-*\r\n    tags: '*'\r\n  pull_request:\r\n\r\njobs:\r\n  t"
  },
  {
    "path": ".github/workflows/Format.yml",
    "chars": 522,
    "preview": "name: Format suggestions\non:\n  pull_request:\n    # this argument is not required if you don't use the `suggestion-label`"
  },
  {
    "path": ".github/workflows/RegisterAction.yml",
    "chars": 323,
    "preview": "name: RegisterAction\non:\n  workflow_dispatch:\n    inputs:\n      version:\n        description: Version to register or com"
  },
  {
    "path": ".github/workflows/TagBot.yml",
    "chars": 826,
    "preview": "name: TagBot\non:\n  issue_comment:\n    types:\n      - created\n  workflow_dispatch:\n    inputs:\n      lookback:\n        de"
  },
  {
    "path": ".gitignore",
    "chars": 48,
    "preview": "deps/build.log\ndeps/deps.jl\n*.mat\nManifest.toml\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1053,
    "preview": "Copyright (c) 2013 Dahua Lin\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this softw"
  },
  {
    "path": "Project.toml",
    "chars": 365,
    "preview": "name = \"MATLAB\"\nuuid = \"10e44e05-a98a-55b3-a45b-ba969058deb6\"\nlicense = \"MIT\"\nrepo = \"https://github.com/JuliaInterop/MA"
  },
  {
    "path": "README.md",
    "chars": 15359,
    "preview": "## MATLAB\n\n| ❗ If you get a 'Segmentation fault' error when calling MATLAB with version R2022 or newer, try using an [ol"
  },
  {
    "path": "deps/build.jl",
    "chars": 5027,
    "preview": "import Libdl\n\nconst depsfile = joinpath(@__DIR__, \"deps.jl\")\n\nfunction find_matlab_root()\n    # Determine MATLAB library"
  },
  {
    "path": "src/MATLAB.jl",
    "chars": 6780,
    "preview": "module MATLAB\n\nusing Libdl\nusing SparseArrays\n\nimport Base: eltype, close, size, copy, ndims, unsafe_convert\n\n# mxarray\n"
  },
  {
    "path": "src/engine.jl",
    "chars": 12181,
    "preview": "# operation on MATLAB engine sessions\n\n###########################################################\n#\n#   Session open & "
  },
  {
    "path": "src/exceptions.jl",
    "chars": 289,
    "preview": "struct MEngineError <: Exception\n    message::String\nend\n\n\"\"\"\n    MEngineError(message::String)\n\nException thrown by MAT"
  },
  {
    "path": "src/init.jl",
    "chars": 3068,
    "preview": "# libraries\n\nconst libeng = Ref{Ptr{Cvoid}}()\nconst libmx  = Ref{Ptr{Cvoid}}()\nconst libmat = Ref{Ptr{Cvoid}}()\n\n# matla"
  },
  {
    "path": "src/matfile.jl",
    "chars": 2846,
    "preview": "# mat file open & close\n\nmutable struct MatFile\n    ptr::Ptr{Cvoid}\n    filename::String\n\n    function MatFile(filename:"
  },
  {
    "path": "src/matstr.jl",
    "chars": 6098,
    "preview": "# Syntax for mat\"\" string interpolation\n\n# A really basic parser intended only to handle checking whether\n# a variable i"
  },
  {
    "path": "src/mxarray.jl",
    "chars": 22073,
    "preview": "# functions to deal with MATLAB arrays\n\nmutable struct MxArray\n    ptr::Ptr{Cvoid}\n    own::Bool\n\n    function MxArray(p"
  },
  {
    "path": "test/engine.jl",
    "chars": 1144,
    "preview": "using MATLAB\nusing Test\nusing SparseArrays\n\n# test engine\n\nrestart_default_msession()\n\na = [1.0 2.0 3.0; 4.0 5.0 6.0]\nb "
  },
  {
    "path": "test/matfile.jl",
    "chars": 1217,
    "preview": "using MATLAB\nusing Test\n\n# test MMAT file I/O\nfn = \"$(tempname()).mat\"\n\na32 = Int32[1 2 3; 4 5 6]\na64 = Int64[1 2 3; 4 5"
  },
  {
    "path": "test/matstr.jl",
    "chars": 1814,
    "preview": "using MATLAB\nusing Test\n\n@test mat\"1\" == 1\n@test mat\"[1, 2, 3]\" == [1 2 3]\n\n# Test interpolation\nx = 1\n@test mat\"$x + 1\""
  },
  {
    "path": "test/mxarray.jl",
    "chars": 9697,
    "preview": "using MATLAB\nusing Test\nusing SparseArrays\n\n# Unit testing for MxArray\n\nm = 5\nn = 6\n\n# test basic types in 1D & 2D\n\nmacr"
  },
  {
    "path": "test/runtests.jl",
    "chars": 221,
    "preview": "using MATLAB\nusing Test\n\nis_ci() = lowercase(get(ENV, \"CI\", \"false\")) == \"true\"\n\nif !is_ci() # only test if not CI\n    i"
  }
]

About this extraction

This page contains the full source code of the JuliaInterop/MATLAB.jl GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 23 files (90.1 KB), approximately 27.6k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!