PGS62 / JuliaExcel.jl

Call Julia from Microsoft Excel worksheets and from VBA
MIT License
60 stars 6 forks source link
excel julia julia-package vba vba-excel

JuliaExcel

Call Julia from Microsoft Excel worksheets and from VBA.

Compatible with Excel's dynamic array formulas.

Support Excel for Windows (not Mac). Julia can be run under Windows or Linux (via WSL).

Contents

Installation
Installation (Linux)
Functions
Demo
Example VBA
Function Documentation
Marshalling
Alternatives
Compatibility
Viewing the code
How JuliaExcel works
Shortcomings

Installation

Installation does not require admin rights on the PC.

The process is illustrated in the GIF below. F5 to replay.

installation

Installation (Linux)

As an alternative to calling Julia running in a Windows process, JuliaExcel can call Julia running in a Linux process under Windows Subsystem for Linux (WSL). If that's your preference, then in addition to the steps described above, you need to:

Functions

JuliaExcel makes the following functions available from Excel worksheets and from VBA:

Name Description
JuliaLaunch Launches a local Julia session which "listens" to the current Excel session and responds to calls to JuliaEval etc..
JuliaInclude Load a Julia source file into the Julia process, to make additional functions available via JuliaEval and JuliaCall.
JuliaEval Evaluate a Julia expression and return the result to an Excel worksheet.
JuliaCall Call a named Julia function, passing in data from the worksheet.
JuliaSetVar Set a global variable in the Julia process.
JuliaEvalVBA Evaluate a Julia expression from VBA . Differs from JuliaCall in handling of 1-dimensional arrays, and strings longer than 32,767 characters. May return data of types that cannot be displayed on a worksheet, such as a dictionary or an array of arrays.
JuliaCallVBA Call a named Julia function from VBA. Differs from JuliaCall in handling of 1-dimensional arrays, and strings longer than 32,767 characters. May return data of types that cannot be displayed on a worksheet, such as a dictionary or an array of arrays.
JuliaResultFile Returns the name of the file to which the results of calls to JuliaCall, JuliaEval etc. are written. The file may be unserialised with function JuliaUnserialiseFile.
JuliaUnserialiseFile Unserialises the contents of the JuliaResultsFile.
JuliaIsRunning Returns TRUE if an instance of Julia is running and "listening" to the current Excel session, or FALSE otherwise.

Demo

Here's a quick demonstration of the functions in action.

Example VBA

The VBA code below makes a call to JuliaLaunch and JuliaEvalVBA and then pastes the result to range A1:J10 in a new worksheet. To run it, make sure that the project has a reference to JuliaExcel (VBA editor, Tools menu -> References).

Sub DemoCallVBA()

    Dim ResultFromJulia As Variant, PasteHere As Range

    JuliaLaunch

    ResultFromJulia = JuliaEvalVBA("(1:10).^(1:10)'")

    Set PasteHere = Application.Workbooks.Add.Worksheets(1) _
        .Cells(1, 1).Resize(UBound(ResultFromJulia, 1), _
        UBound(ResultFromJulia, 2))

    PasteHere.Value = ResultFromJulia

End Sub

Function Documentation

JuliaLaunch

Launches a local Julia session which "listens" to the current Excel session and responds to calls to JuliaEval etc..

Public Function JuliaLaunch(Optional UseLinux As Boolean, Optional MinimiseWindow As Boolean, _
    Optional ByVal CommandLineOptions As String, Optional ByVal Packages As String, _
    Optional ByVal BashStatements As String, Optional TimeOut As Long = 30)
Argument Description
UseLinux TRUE to run Julia as a Linux process under Windows Subsystem for Linux; FALSE (the default) to run as a Windows process.
MinimiseWindow If TRUE, then the Julia session window is minimised; if FALSE (the default) then the window is sized normally.
CommandLineOptions Command line options set when launching Julia.
Example : --threads=auto --banner=no.
https://docs.julialang.org/en/v1/manual/command-line-options/
Packages Packages to load, which must be available in the default Julia environment (or environment set via the --project command line option). Delimit multiple packages with commas.
BashStatements Relevant only when UseLinux is TRUE. Bash statements executed prior to launching Julia, which can be used to set environment variables. Example export JULIA_PKG_DEVDIR=/mnt/c/Projects. Delimit multiple statements with the line feed character.
TimeOut The number of seconds to wait for Julia to launch before the function assumes that launch has failed (perhaps because of mal-formed CommandLineOptions). Optional and defaults to 30.

JuliaInclude

Load a Julia source file into the Julia process, to make additional functions available via JuliaEval and JuliaCall.

Public Function JuliaInclude(FileName As String)
Argument Description
FileName The full name of the file to be included.

JuliaEval

Evaluate a Julia expression and return the result to an Excel worksheet.

Public Function JuliaEval(ByVal JuliaExpression As Variant)
Argument Description
JuliaExpression Any valid Julia code, as a string. Can also be a one-column range to evaluate multiple Julia statements.

JuliaCall

Call a named Julia function, passing in data from the worksheet.

Public Function JuliaCall(JuliaFunction As String, ParamArray Args())
Argument Description
JuliaFunction The name of a Julia function that's defined in the Julia session, perhaps as a result of prior calls to JuliaInclude.
Args... Zero or more arguments. Each argument may be a number, string, Boolean value, empty cell, an array of such values or an Excel range.

JuliaSetVar

Set a global variable in the Julia process.

Public Function JuliaSetVar(VariableName As String, RefersTo As Variant)
Argument Description
VariableName The name of the variable to be set. Must follow Julia's rules for allowed variable names.
RefersTo An Excel range (from which the .Value2 property is read) or more generally a number, string, Boolean, Empty or array of such types. When called from VBA, nested arrays are supported.

JuliaEvalVBA

Evaluate a Julia expression from VBA . Differs from JuliaCall in handling of 1-dimensional arrays, and strings longer than 32,767 characters. May return data of types that cannot be displayed on a worksheet, such as a dictionary or an array of arrays.

Public Function JuliaEvalVBA(ByVal JuliaExpression As Variant)
Argument Description
JuliaExpression Any valid Julia code, as a string. Can also be a one-column range to evaluate multiple Julia statements.

JuliaCallVBA

Call a named Julia function from VBA. Differs from JuliaCall in handling of 1-dimensional arrays, and strings longer than 32,767 characters. May return data of types that cannot be displayed on a worksheet, such as a dictionary or an array of arrays.

Public Function JuliaCallVBA(JuliaFunction As String, ParamArray Args())
Argument Description
JuliaFunction The name of a Julia function that's defined in the Julia session, perhaps as a result of prior calls to JuliaInclude.
Args... Zero or more arguments. Each argument may be a number, string, Boolean value, empty cell, an array of such values or an Excel range.

JuliaResultFile

Returns the name of the file to which the results of calls to JuliaCall, JuliaEval etc. are written. The file may be unserialised with function JuliaUnserialiseFile.

Public Function JuliaResultFile() As String

JuliaUnserialiseFile

Unserialises the contents of the JuliaResultsFile.

Public Function JuliaUnserialiseFile(Optional ByVal FileName As String, Optional ForWorksheet As Boolean = True)
Argument Description
FileName The name (including path) of the file to be unserialised. Optional and defaults to the file name returned by JuliaResultFile.
ForWorksheet Pass TRUE (the default) when calling from a worksheet, FALSE when calling from VBA. If FALSE, the function may return data structures that can exist in VBA but cannot be represented on a worksheet, such as a dictionary or an array of arrays.

JuliaIsRunning

Returns TRUE if an instance of Julia is running and "listening" to the current Excel session, or FALSE otherwise.

Public Function JuliaIsRunning() As Boolean

Marshalling

Two question arose during implementation:

First, when data from a worksheet (or a VBA variable) is passed to JuliaCall or JuliaSetVar, that data is marshalled over to Julia. As what Julia type should the data arrive? Mostly, this is easy to decide, but what about one-dimensional arrays (from VBA) or ranges with just one column or one just row from an Excel worksheet? Should these have one-dimension or two over in Julia?

Second, after Julia has evaluated the expression, how should the result be marshalled in the opposite direction, back to Excel? Again, this is easy to decide for scalars and two dimensional arrays, but what about for vectors in Julia?

There were three objectives to the design of the marshalling processes: 1) Round-tripping should work, i.e. the formula =JuliaCall("identity",x) should return an identical copy of x, whatever the "shape" of x. 2) Matrix arithmetic should work naturally. In Julia, the * operator does matrix multiplication, so marshalling should be such that the formula =JuliaCall("*",Range1,Range2) performs the same matrix multiplication as the formula =MMULT(Range1,Range2), which calls Excel's built-in matrix multiplier. 3) To allow use from JuliaCall of Julia's dot syntax for function broadcasting.

The following marshalling scheme achieves the objectives:

For calls from VBA:

Matrix arithmetic

![matrixarithmetic](images/matrixarithmetic.gif)

Function broadcasting

![functionbroadcasting](images/functionbroadcasting.gif)

Alternatives

There is one alternative method of calling Julia from Excel of which I am aware:

https://github.com/JuliaComputing/JuliaInXL.jl

JuliaComputing has recently (October 2021) made JuliaInXL open source, it having previously required a licence for commercial use. At the time of writing, JuliaInXL is not compatible with dynamic array formulas, and does not permit calling Julia from VBA. On the other hand, JuliaInXL uses socket-based communication and C# rather than VBA for serialisation and unserialisation on the Excel side. So in many scenarios, its performance will be better. I have yet to do extensive testing on that however.

Compatibility

JuliaExcel has been tested on Excel under Microsoft 365, both 32-bit and 64-bit. It should work on earlier versions of Excel (perhaps back to Excel 2010) but it has not been tested on them.

How JuliaExcel works

The implementation of JuliaExcel is very "low-tech". When JuliaEval is called from a worksheet, the following happens: 1) VBA code (in JuliaExcel.xlam) writes the expression to a file in the @JuliaExcel sub-folder of the temporary folder. 2) VBA code then uses the Windows API PostMessage to send keystrokes srv_xl() to the Julia window. 3) That causes the Julia function srv_xl (defined in JuliaExcel.jl) to execute. The function reads the expression from file, evaluates it and writes to a result file. 4) The VBA code (in a wait loop since step 1) detects that the result file has been (completely) written, and unserialises its contents.

Other points to note:

Viewing the code

The VBA project is password protected to prevent accidental changes. You can see the VBA code here, or view it in JuliaExcel.xlam by unprotecting with the password "JuliaExcel". Julia source code is always visible on your PC, and the @functionloc macro is an easy way to locate the code of any function you're interested in.

Shortcomings

Given how JuliaExcel works, with file-based messaging and serialisation in VBA, an interpreted and hence relatively slow language, the most obvious shortcoming will be performance of the data-transfer Excel to Julia and back. That's not always a problem however, notably if the time for marshalling data between Excel and Julia is small (milliseconds) compared with the execution time of the Julia code (tens of seconds). I wrote JuliaExcel for a project where that situation holds.

Other shortcomings are:

 

 

Philip Swannell
8 December 2021