1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Utilities for interacting with a Wolfram Kernel process via WSTP.
//!
//! # Example
//!
//! Launch a new Wolfram Kernel process from the file path to a
//! [`WolframKernel`][WolframKernel] executable:
//!
//! ```no_run
//! use std::path::PathBuf;
//! use wstp::kernel::WolframKernelProcess;
//!
//! let exe = PathBuf::from(
//!     "/Applications/Mathematica.app/Contents/MacOS/WolframKernel"
//! );
//!
//! let kernel = WolframKernelProcess::launch(&exe).unwrap();
//! ```
//!
//! ### Automatic Wolfram Kernel discovery
//!
//! Use the [wolfram-app-discovery] crate to automatically discover a suitable
//! `WolframKernel`:
//!
//! ```no_run
//! use std::path::PathBuf;
//! use wolfram_app_discovery::WolframApp;
//! use wstp::kernel::WolframKernelProcess;
//!
//! let app = WolframApp::try_default()
//!     .expect("unable to find any Wolfram Language installations");
//!
//! let exe: PathBuf = app.kernel_executable_path().unwrap();
//!
//! let kernel = WolframKernelProcess::launch(&exe).unwrap();
//! ```
//!
//! Using automatic discovery makes it easy to write programs that are portable to
//! different computers, without relying on end-user configuration to specify the location
//! of the local Wolfram Language installation.
//!
//!
//! [WolframKernel]: https://reference.wolfram.com/language/ref/program/WolframKernel.html
//! [wolfram-app-discovery]: https://crates.io/crates/wolfram-app-discovery
//!
//!
//! # Related Links
//!
//! #### Wolfram Language documentation
//!
//! These resources describe the packet expression interface used by the Wolfram Kernel.
//!
//! * [WSTP Packets](https://reference.wolfram.com/language/guide/WSTPPackets.html)
//! * [Running the Wolfram System from within an External Program](https://reference.wolfram.com/language/tutorial/RunningTheWolframSystemFromWithinAnExternalProgram.html)
//!
//! #### Link packet methods
//!
//! * [`Link::put_eval_packet()`]

use std::{path::PathBuf, process};

use wolfram_expr::Expr;

use crate::{Error as WstpError, Link, Protocol};

/// Handle to a Wolfram Kernel process connected via WSTP.
///
/// Use [`WolframKernelProcess::launch()`] to launch a new Wolfram Kernel process.
///
/// Use [`WolframKernelProcess::link()`] to access the WSTP [`Link`] used to communicate with
/// this kernel.
#[derive(Debug)]
pub struct WolframKernelProcess {
    #[allow(dead_code)]
    process: process::Child,
    link: Link,
}

/// Wolfram Kernel process error.
#[derive(Debug)]
pub struct Error(String);

impl From<WstpError> for Error {
    fn from(err: WstpError) -> Error {
        Error(format!("WSTP error: {err}"))
    }
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Error {
        Error(format!("IO error: {err}"))
    }
}

impl WolframKernelProcess {
    /// Launch a new Wolfram Kernel child process and establish a WSTP connection with it.
    ///
    /// See also the [wolfram-app-discovery](https://crates.io/crates/wolfram-app-discovery)
    /// crate, whose
    /// [`WolframApp::kernel_executable_path()`](https://docs.rs/wolfram-app-discovery/0.2.0/wolfram_app_discovery/struct.WolframApp.html#method.kernel_executable_path)
    /// method can be used to get the location of a [`WolframKernel`][WolframKernel]
    /// executable suitable for use with this function.
    ///
    /// [WolframKernel]: https://reference.wolfram.com/language/ref/program/WolframKernel.html
    //
    // TODO: Would it be correct to describe this as essentially `LinkLaunch`? Also note
    //       that this doesn't actually use `-linkmode launch`.
    pub fn launch(path: &PathBuf) -> Result<WolframKernelProcess, Error> {
        // FIXME: Make this a random string.
        const NAME: &str = "SHM_WK_LINK";

        let listener = std::thread::spawn(|| {
            // This will block until a connection is made.
            Link::listen(Protocol::SharedMemory, NAME)
        });

        let kernel_process = process::Command::new(path)
            .arg("-wstp")
            .arg("-linkprotocol")
            .arg("SharedMemory")
            .arg("-linkconnect")
            .arg("-linkname")
            .arg(NAME)
            .spawn()?;

        let link: Link = match listener.join() {
            Ok(result) => result?,
            Err(panic) => {
                return Err(Error(format!(
                    "unable to launch Wolfram Kernel: listening thread panicked: {:?}",
                    panic
                )))
            },
        };

        Ok(WolframKernelProcess {
            process: kernel_process,
            link,
        })
    }

    /// Get the WSTP [`Link`] connection used to communicate with this Wolfram Kernel
    /// process.
    pub fn link(&mut self) -> &mut Link {
        let WolframKernelProcess { process: _, link } = self;
        link
    }
}

impl Link {
    /// Put an [`EvaluatePacket[expr]`][EvaluatePacket] onto the link.
    ///
    /// [EvaluatePacket]: https://reference.wolfram.com/language/ref/EvaluatePacket.html
    pub fn put_eval_packet(&mut self, expr: &Expr) -> Result<(), Error> {
        self.put_function("System`EvaluatePacket", 1)?;
        self.put_expr(expr)?;
        self.end_packet()?;

        Ok(())
    }
}