rs-teststand 0.11.0

Community Rust bindings (twin API) for the National Instruments TestStand™ COM API
Documentation
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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
//! One thread of a running execution.

use rs_teststand_sys::{Dispatch, Value};

use crate::Error;
use crate::dispids::thread;

/// A thread within an [`Execution`](crate::Execution).
///
/// Every execution has at least one. A sequence that starts a parallel or
/// asynchronous step gains more, which is why a front end tracks progress per
/// thread rather than per execution.
#[derive(Debug)]
pub struct Thread {
    dispatch: Box<dyn Dispatch>,
}

impl Thread {
    /// Wraps a dispatch handle returned by the engine.
    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
        Self { dispatch }
    }

    /// The thread as a property tree (`Thread.AsPropertyObject`).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn as_property_object(&self) -> Result<crate::PropertyObject, Error> {
        Ok(crate::PropertyObject::new(
            self.dispatch
                .call(thread::AS_PROPERTY_OBJECT, &[])?
                .into_object()?,
        ))
    }

    /// The thread's identifier within its execution (`Thread.Id`).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn id(&self) -> Result<i32, Error> {
        Ok(self.dispatch.get(thread::ID)?.as_i32()?)
    }

    /// An identifier unique across the whole session (`Thread.UniqueThreadId`).
    ///
    /// [`id`](Self::id) only distinguishes threads within one execution, so a
    /// host serving several executions keys on this instead.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn unique_thread_id(&self) -> Result<String, Error> {
        Ok(self.dispatch.get(thread::UNIQUE_THREAD_ID)?.into_string()?)
    }

    /// The name a front end shows for this thread (`Thread.DisplayName`).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn display_name(&self) -> Result<String, Error> {
        Ok(self.dispatch.get(thread::DISPLAY_NAME)?.into_string()?)
    }

    /// How deep the call stack currently is (`Thread.CallStackSize`).
    ///
    /// Index `0` is the innermost frame, which is what
    /// [`get_sequence_context`](Self::get_sequence_context) usually wants.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn call_stack_size(&self) -> Result<i32, Error> {
        Ok(self.dispatch.get(thread::CALL_STACK_SIZE)?.as_i32()?)
    }

    /// Whether a requested suspend has actually taken effect
    /// (`Thread.ExternallySuspended`).
    ///
    /// [`Execution::suspend`](crate::Execution::suspend) only *asks*. This is
    /// how a caller learns the engine has acted, and the reason a suspend
    /// followed immediately by a resume is a race: the resume can arrive before
    /// the suspend takes hold, leaving the run stopped for good.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn externally_suspended(&self) -> Result<bool, Error> {
        Ok(self.dispatch.get(thread::EXTERNALLY_SUSPENDED)?.as_bool()?)
    }

    /// The execution this thread belongs to (`Thread.Execution`).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn execution(&self) -> Result<crate::Execution, Error> {
        Ok(crate::Execution::new(
            self.dispatch.get(thread::EXECUTION)?.into_object()?,
        ))
    }

    /// The sequence context at a call-stack frame (`Thread.GetSequenceContext`).
    ///
    /// Index `0` is the innermost frame, the sequence running right now.
    ///
    /// This is the route to `RunState`, `Locals`, `FileGlobals` and
    /// `StationGlobals` for a live run. **Mind what may outlive the run:** NI
    /// documents `StationGlobals`, `RunState.InitialSelection`,
    /// `RunState.SequenceFile` and `RunState.ProcessModelClient` as existing
    /// before and persisting after the execution, and everything else in the
    /// context as belonging to it. `FileGlobals` in particular is the run's own
    /// copy, so keeping one past the execution holds a reference to a finished
    /// run, read what is needed while it is alive, or take the edit-time
    /// defaults from
    /// [`SequenceFile::file_globals_default_values`](crate::SequenceFile::file_globals_default_values)
    /// instead.
    ///
    /// The engine declares **two** parameters: the call stack index, and an
    /// `[out]` frame id (`VT_BYREF | VT_I4`). Both must be present in the call
    /// even though only the first carries information, supplying one gives
    /// `DISP_E_BADPARAMCOUNT`. The second is passed empty, which the engine
    /// accepts as "no output wanted"; reading the frame id back would need
    /// byref support this crate does not have yet.
    ///
    /// # Errors
    /// [`Error`] if the index is out of range or the COM call fails.
    pub fn get_sequence_context(
        &self,
        call_stack_index: i32,
    ) -> Result<crate::execution::SequenceContext, Error> {
        Ok(crate::execution::SequenceContext::new(
            self.dispatch
                .call(
                    thread::GET_SEQUENCE_CONTEXT,
                    &[Value::I32(call_stack_index), Value::Empty],
                )?
                .into_object()?,
        ))
    }

    /// Asks this thread to stop again once the next step finishes
    /// (`Thread.SetStepOver`).
    ///
    /// Arms a one-shot stop; it does not start the thread moving. Pair it with
    /// [`resume`](Self::resume) to actually step.
    ///
    /// The engine also has `Execution.StepOver`, which arms and resumes in one
    /// call but always acts on the foreground thread. This crate does not wrap
    /// it yet. Going through the thread is the only way to say which thread to
    /// step, which is what a host serving a panel with several threads needs.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn set_step_over(&self) -> Result<(), Error> {
        self.dispatch.call(thread::SET_STEP_OVER, &[])?;
        Ok(())
    }

    /// Arms a stop at the first step inside whatever the next step calls
    /// (`Thread.SetStepInto`).
    ///
    /// Does not resume the thread. See [`set_step_over`](Self::set_step_over).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn set_step_into(&self) -> Result<(), Error> {
        self.dispatch.call(thread::SET_STEP_INTO, &[])?;
        Ok(())
    }

    /// Arms a stop once the current sequence returns to its caller
    /// (`Thread.SetStepOut`).
    ///
    /// Does not resume the thread. See [`set_step_over`](Self::set_step_over).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn set_step_out(&self) -> Result<(), Error> {
        self.dispatch.call(thread::SET_STEP_OUT, &[])?;
        Ok(())
    }

    /// Clears a stop armed by one of the `set_step_*` members
    /// (`Thread.ClearTemporaryBreakpoint`).
    ///
    /// Only the temporary one. Breakpoints set on a step are untouched.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn clear_temporary_breakpoint(&self) -> Result<(), Error> {
        self.dispatch
            .call(thread::CLEAR_TEMPORARY_BREAKPOINT, &[])?;
        Ok(())
    }

    /// Clears the run-time error sitting on the current step
    /// (`Thread.ClearCurrentRTE`).
    ///
    /// Resets the step's recorded error so the thread carries on as though it
    /// had not happened. This is how a host answers a run-time error by
    /// ignoring it, rather than letting the station's configured response
    /// decide.
    ///
    /// Only meaningful while a run-time error is actually outstanding. Called
    /// on a thread that has none, a live engine answers
    /// `TS_Err_UnexpectedType` rather than doing nothing, so a host should call
    /// this in response to a run-time error and not speculatively.
    ///
    /// # Errors
    /// [`Error`] if there is no run-time error to clear, or the COM call fails.
    pub fn clear_current_rte(&self) -> Result<(), Error> {
        self.dispatch.call(thread::CLEAR_CURRENT_RTE, &[])?;
        Ok(())
    }

    /// Hands whatever results have piled up to the post-results callbacks now
    /// (`Thread.FlushPostResults`).
    ///
    /// Results are normally batched. A host that wants a client to see them
    /// sooner can force the handover; with nothing accumulated the call does
    /// nothing, so it is safe to make on a timer.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn flush_post_results(&self) -> Result<(), Error> {
        self.dispatch.call(thread::FLUSH_POST_RESULTS, &[])?;
        Ok(())
    }

    /// Whether the run is about to step into the current step's code module
    /// (`Thread.WillStepIntoModule`).
    ///
    /// Read this only from a pre-step substep. That is the one place it means
    /// anything: it reports true there when the run will suspend inside the
    /// module belonging to the step that owns the substep. Read from anywhere
    /// else, an expression or a step's own code module, the engine answers
    /// false regardless of what the run is doing, so a false here is not
    /// evidence that stepping is off.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn will_step_into_module(&self) -> Result<bool, Error> {
        Ok(self
            .dispatch
            .get(thread::WILL_STEP_INTO_MODULE)?
            .as_bool()?)
    }

    /// Waits for this thread to finish (`Thread.WaitForEnd`).
    ///
    /// Returns `true` when the thread ended and `false` when the wait ran out
    /// first. Pass `-1` for `milliseconds` to wait with no limit, which is
    /// worth avoiding in a host that has to stay answerable.
    ///
    /// `process_windows_messages` decides whether the calling thread keeps
    /// pumping while it waits. A host on a COM apartment should pass `true`:
    /// stop pumping and the engine cannot deliver into this apartment, so a
    /// wait meant to end can sit until the timeout instead.
    ///
    /// Waiting is not the only obligation. This pumps but does not drain the
    /// engine's message queue, so a sequence posting a synchronous message
    /// stays blocked on a host that only waits here. Draining the queue is
    /// separate work.
    ///
    /// The two optional arguments the engine accepts, a step to store results
    /// in and a calling sequence context, are not exposed. Both take an object
    /// this crate has no route to from outside a running sequence.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn wait_for_end(
        &self,
        milliseconds: i32,
        process_windows_messages: bool,
    ) -> Result<bool, Error> {
        Ok(self
            .dispatch
            .call(
                thread::WAIT_FOR_END,
                &[
                    Value::I32(milliseconds),
                    Value::Bool(process_windows_messages),
                    // The two optional arguments, left absent.
                    Value::Empty,
                    Value::Empty,
                ],
            )?
            .as_bool()?)
    }

    /// How this thread answers a request to terminate its execution
    /// (`Thread.TerminationOption`).
    ///
    /// The inner [`Result`] carries the raw number when the engine names an
    /// option this build does not, rather than mapping it onto a neighbour.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails or returns an unexpected type.
    pub fn termination_option(&self) -> Result<Result<crate::ThreadTerminationOption, i32>, Error> {
        let raw = self.dispatch.get(thread::TERMINATION_OPTION)?.as_i32()?;
        Ok(crate::ThreadTerminationOption::from_bits(raw))
    }

    /// Chooses how this thread answers a terminate request
    /// (`Thread.TerminationOption`).
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn set_termination_option(
        &self,
        option: crate::ThreadTerminationOption,
    ) -> Result<(), Error> {
        self.dispatch
            .put(thread::TERMINATION_OPTION, Value::I32(option.bits()))?;
        Ok(())
    }

    /// Starts this thread running (`Thread.Resume`).
    ///
    /// Releases a thread created suspended, which is how a sequence call step
    /// can hand one back before it runs. It is also the second half of a step,
    /// once `set_step_over`, `set_step_into` or `set_step_out` has armed one.
    ///
    /// **This does not continue a run stopped at a breakpoint.** Measured
    /// against a live engine: after a breakpoint stop, calling this leaves the
    /// run where it is and the execution never ends. Use
    /// [`Execution::resume`](crate::Execution::resume) for that, which
    /// continued the same run in about 200 ms.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn resume(&self) -> Result<(), Error> {
        self.dispatch.call(thread::RESUME, &[])?;
        Ok(())
    }

    /// Sends a message to whatever is watching this execution
    /// (`Thread.PostUIMessageEx`).
    ///
    /// The outbound half of a two-way bridge: the sequence reports, a host
    /// forwards.
    ///
    /// Pass `synchronous = true` in the ordinary case. It blocks the posting
    /// thread until the host acknowledges, which is what applies backpressure:
    /// posting faster than the host drains grows the queue without bound and
    /// eventually makes the host unresponsive. The cost is that a host which
    /// never drains its queue stalls the sequence instead, so a host owes the
    /// engine an [`acknowledge`](crate::UIMessage::acknowledge) for every
    /// message it takes.
    ///
    /// `activex_data` is the structured payload. Pass a container and the host
    /// reads the whole tree back from
    /// [`UIMessage::activex_data`](crate::UIMessage::activex_data), instead of
    /// the two of them agreeing on how to pack fields into `string_data`. Pass
    /// `None` to leave the slot empty, which is a null object reference rather
    /// than an absent argument.
    ///
    /// A message a host defines for itself should use a code at or above
    /// [`UIMessageCode::USER_MESSAGE_BASE`](crate::UIMessageCode::USER_MESSAGE_BASE),
    /// which is the range the engine reserves for callers.
    ///
    /// # Errors
    /// [`Error`] if the COM call fails.
    pub fn post_ui_message_ex(
        &self,
        event_code: i32,
        numeric_data: f64,
        string_data: &str,
        activex_data: Option<&crate::PropertyObject>,
        synchronous: bool,
    ) -> Result<(), Error> {
        // The fourth parameter is an object reference, so "no data" is a null
        // *object*, not an absent argument or a boolean. A boolean in its place
        // corrupts the call, the same trap `Engine.NewUser` has.
        let payload = object_argument(activex_data)?;
        self.dispatch.call(
            thread::POST_UI_MESSAGE_EX,
            &[
                Value::I32(event_code),
                Value::F64(numeric_data),
                Value::Str(string_data.to_owned()),
                payload,
                Value::Bool(synchronous),
            ],
        )?;
        Ok(())
    }
}

impl Thread {
    /// Lends the underlying handle for a call that takes a thread reference.
    pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
        self.dispatch.duplicate()
    }
}

/// Turns an optional wrapper into the argument its slot expects.
///
/// The type library declares this parameter `VT_UNKNOWN` rather than
/// `VT_DISPATCH`. Passing a dispatch object is accepted because every
/// `IDispatch` is an `IUnknown`, and it is what the engine hands back when the
/// message is read again.
pub(crate) fn object_argument(object: Option<&crate::PropertyObject>) -> Result<Value, Error> {
    object.map_or_else(
        || Ok(Value::NullObject),
        |property_object| {
            property_object
                .duplicate_dispatch()
                .map(Value::Object)
                .ok_or(Error::UnexpectedType {
                    expected: "a live property object",
                    actual: "a test fake with no COM identity",
                })
        },
    )
}