yottadb/context_api/
call_in.rs

1/****************************************************************
2*                                                               *
3* Copyright (c) 2020-2024 YottaDB LLC and/or its subsidiaries.  *
4* All rights reserved.                                          *
5*                                                               *
6*       This source code contains the intellectual property     *
7*       of its copyright holder(s), and is made available       *
8*       under a license.  If you do not know the terms of       *
9*       the license, please stop and do not read further.       *
10*                                                               *
11****************************************************************/
12
13use std::ffi::CStr;
14
15use crate::{YDBResult, simple_api::call_in::*};
16use super::Context;
17
18/// Make an FFI call to M.
19///
20/// `ci_t` is equivalent to a variadic function with the following signature:
21/// ```ignore
22/// unsafe fn ci_t(tptoken: u64, err_buffer: Vec<u8>, routine: &CStr, ...) -> YDBResult<Vec<u8>>;
23/// ```
24/// However, since Rust does not allow implementing variadic functions, it is a macro instead.
25///
26/// # Safety
27/// Each argument passed (after `routine`) must correspond to the appropriate argument expected by `routine`.
28/// If `routine` returns a value, the first argument must be a pointer to an out parameter in which to store the value.
29/// All arguments must be [representable as C types][repr-c].
30///
31/// # See also
32/// - [C to M FFI](https://docs.yottadb.com/MultiLangProgGuide/cprogram.html#calling-m-routines)
33/// - [The M documentation on call-ins](https://docs.yottadb.com/ProgrammersGuide/extrout.html#calls-from-external-routines-call-ins)
34/// - [`cip_t!`], which allows caching the `routine` lookup, making future calls faster.
35///
36/// # Example
37/// Call the M routine described by `HelloWorld1` in the call-in table.
38/// See also `examples/m-ffi/helloworld1.m` and `examples/m-ffi/calltab.ci`.
39/// ```
40/// use std::env;
41/// use std::ffi::CString;
42/// use std::os::raw::c_char;
43/// use yottadb::{craw, ci_t, TpToken};
44///
45/// env::set_var("ydb_routines", "examples/m-ffi");
46/// env::set_var("ydb_ci", "examples/m-ffi/calltab.ci");
47///
48/// let mut buf = Vec::<u8>::with_capacity(100);
49/// let mut msg = craw::ydb_string_t { length: buf.capacity() as u64, address: buf.as_mut_ptr() as *mut c_char };
50/// let routine = CString::new("HelloWorld1").unwrap();
51/// unsafe {
52///     ci_t!(TpToken::default(), Vec::new(), &routine, &mut msg as *mut _).unwrap();
53///     buf.set_len(msg.length as usize);
54/// }
55/// assert_eq!(&buf, b"entry called");
56/// ```
57/// [repr-c]: https://doc.rust-lang.org/nomicon/ffi.html#interoperability-with-foreign-code
58/// [`cip_t!`]: crate::cip_t!
59#[macro_export]
60macro_rules! ci_t {
61    ($tptoken: expr, $err_buffer: expr, $routine: expr $(, $args: expr)* $(,)?) => {{
62        let tptoken: $crate::TpToken = $tptoken;
63        let err_buffer: ::std::vec::Vec<u8> = $err_buffer;
64        let routine: &::std::ffi::CStr = $routine;
65
66        $crate::resize_call(tptoken, err_buffer, |tptoken, err_buffer_p| {
67            $crate::craw::ydb_ci_t(tptoken, err_buffer_p, routine.as_ptr(), $($args),*)
68        })
69    }}
70}
71
72/// Make a FFI call to M using a cached function descriptor.
73///
74/// `cip_t` is equivalent to a variadic function with the following signature:
75/// ```ignore
76/// unsafe fn ci_t(tptoken: u64, err_buffer: Vec<u8>, routine: CallInDescriptor, ...) -> YDBResult<Vec<u8>>;
77/// ```
78/// However, since Rust does not allow implementing variadic functions, it is a macro instead.
79///
80/// # See also
81/// - [`CallInDescriptor`](crate::CallInDescriptor)
82/// - [`ci_t!`], which has more information about call-ins in YottaDB.
83///
84/// # Safety
85/// Each argument passed (after `routine`) must correspond to the appropriate argument expected by `routine`.
86/// If `routine` returns a value, the first argument must be a pointer to an out parameter in which to store the value.
87/// All arguments must be [representable as C types][repr-c].
88///
89/// [repr-c]: https://doc.rust-lang.org/nomicon/ffi.html#interoperability-with-foreign-code
90///
91/// # Example
92/// Call the M routine described by `HelloWorld1` in the call-in table.
93/// See also `examples/m-ffi/helloworld1.m` and `examples/m-ffi/calltab.ci`.
94/// ```
95/// use std::env;
96/// use std::ffi::CString;
97/// use std::os::raw::c_char;
98/// use yottadb::{craw, cip_t, CallInDescriptor, TpToken};
99///
100/// env::set_var("ydb_routines", "examples/m-ffi");
101/// env::set_var("ydb_ci", "examples/m-ffi/calltab.ci");
102///
103/// let mut buf = Vec::<u8>::with_capacity(100);
104/// let mut msg = craw::ydb_string_t { length: buf.capacity() as u64, address: buf.as_mut_ptr() as *mut c_char };
105/// let mut routine = CallInDescriptor::new(CString::new("HelloWorld1").unwrap());
106/// unsafe {
107///     cip_t!(TpToken::default(), Vec::new(), &mut routine, &mut msg as *mut _).unwrap();
108///     buf.set_len(msg.length as usize);
109/// }
110/// assert_eq!(&buf, b"entry called");
111/// ```
112/// [`ci_t!`]: crate::ci_t!
113#[macro_export]
114macro_rules! cip_t {
115    ($tptoken: expr, $err_buffer: expr, $routine: expr, $($args: expr),* $(,)?) => {{
116        let tptoken: $crate::TpToken = $tptoken;
117        let err_buffer: ::std::vec::Vec<u8> = $err_buffer;
118        let routine: &mut $crate::CallInDescriptor = $routine;
119
120        $crate::resize_call(tptoken, err_buffer, |tptoken, err_buffer_p| {
121            $crate::craw::ydb_cip_t(tptoken, err_buffer_p, routine.as_mut_ptr(), $($args),*)
122        })
123    }}
124}
125
126/// Call-in functions
127impl Context {
128    /// Open the call-in table stored in `file` and return its file descriptor.
129    ///
130    /// You can later switch the active call-in table by calling [`ci_tab_switch`] with the file descriptor.
131    ///
132    /// # See also
133    /// - [C SimpleAPI documentation](https://docs.yottadb.com/MultiLangProgGuide/cprogram.html#ydb-ci-tab-open-ydb-ci-tab-open-t)
134    /// - [Call-in interface](https://docs.yottadb.com/ProgrammersGuide/extrout.html#call-in-interface)
135    /// - [`ci_t!`] and [`cip_t!`]
136    ///
137    /// [`cip_t!`]: crate::cip_t!
138    /// [`ci_t!`]: crate::ci_t!
139    ///
140    #[allow(clippy::empty_line_after_doc_comments)]
141    /// # Errors
142
143    // The upstream documentation says
144    // > YDB_ERR_PARAMINVALID if the input parameters fname or ret_value are NULL; or
145    // PARAMINVALID is not possible because `ptr` and `&mut ret_val` are always non-null.
146
147    /// - a negative [error return code] (for example, if the call-in table in the file had parse errors).
148    ///
149    /// [`ci_tab_switch`]: Context::ci_tab_switch()
150    /// [error return code]: https://docs.yottadb.com/MessageRecovery/errormsgref.html#zmessage-codes
151    ///
152    /// # Example
153    /// ```
154    /// # fn main() -> yottadb::YDBResult<()> {
155    /// use std::ffi::CString;
156    /// use yottadb::Context;
157    ///
158    /// let ctx = Context::new();
159    /// let file = CString::new("examples/m-ffi/calltab.ci").unwrap();
160    /// let descriptor = ctx.ci_tab_open(&file)?;
161    /// # Ok(())
162    /// # }
163    pub fn ci_tab_open(&self, file: &CStr) -> YDBResult<CallInTableDescriptor> {
164        let tptoken = self.tptoken();
165        let buffer = self.take_buffer();
166        let (descriptor, buffer) = ci_tab_open_t(tptoken, buffer, file)?;
167        *self.context.buffer.borrow_mut() = buffer;
168        Ok(descriptor)
169    }
170
171    /// Switch the active call-in table to `new_handle`. Returns the previously active table.
172    ///
173    /// `new_handle` is a file descriptor returned by [`ci_tab_open`].
174    ///
175    #[allow(clippy::empty_line_after_doc_comments)]
176    /// # Errors
177
178    // The upstream docs say this:
179    // > YDB_ERR_PARAMINVALID if the output parameter ret_old_handle is NULL or if the input parameter new_handle points to an invalid handle (i.e. not returned by a prior ydb_ci_tab_open()/ydb_ci_tab_open_t()) call)
180    // YDB_ERR_PARAMINVALID isn't possible because
181    // a) we always pass in `&ret_val`, which is non-null, and
182    // b) we pass in a handle from `CallInDescriptor`, which can only be created by `ci_tab_open_t`
183
184    /// - [a negative error return code](https://docs.yottadb.com/MessageRecovery/errormsgref.html#standard-error-codes)
185    ///
186    /// [`ci_tab_open`]: Context::ci_tab_open()
187    ///
188    /// # Example
189    /// ```
190    /// # fn main() -> yottadb::YDBResult<()> {
191    /// use std::ffi::CString;
192    /// use yottadb::Context;
193    ///
194    /// let ctx = Context::new();
195    /// let file = CString::new("examples/m-ffi/calltab.ci").unwrap();
196    /// let descriptor = ctx.ci_tab_open(&file)?;
197    /// let old_ci_table = ctx.ci_tab_switch(descriptor)?;
198    /// # Ok(())
199    /// # }
200    /// ```
201    pub fn ci_tab_switch(
202        &self, new_handle: CallInTableDescriptor,
203    ) -> YDBResult<CallInTableDescriptor> {
204        let tptoken = self.tptoken();
205        let buffer = self.take_buffer();
206        let (descriptor, buffer) = ci_tab_switch_t(tptoken, buffer, new_handle)?;
207        *self.context.buffer.borrow_mut() = buffer;
208        Ok(descriptor)
209    }
210}
211
212#[cfg(test)]
213mod test {
214    use std::env;
215    use std::ffi::CString;
216    use std::os::raw::c_char;
217    use super::*;
218    use crate::craw::{self, ydb_string_t, ydb_long_t};
219    use crate::YDB_NOTTP;
220    use crate::test_lock::LockGuard;
221
222    fn call<F: FnOnce() -> T, T>(f: F) -> T {
223        let _guard = LockGuard::read();
224        env::set_var("ydb_routines", "examples/m-ffi");
225        env::set_var("ydb_ci", "examples/m-ffi/calltab.ci");
226        f()
227    }
228
229    // no arguments already tested by doc-test
230    #[test]
231    fn string_args() {
232        call(|| {
233            let mut routine = CallInDescriptor::new(CString::new("HelloWorld2").unwrap());
234
235            let mut ret_buf = Vec::<u8>::with_capacity(100);
236            let mut ret_msg = ydb_string_t {
237                length: ret_buf.capacity() as u64,
238                address: ret_buf.as_mut_ptr() as *mut c_char,
239            };
240
241            let buf1 = b"parm1";
242            let mut msg1 =
243                ydb_string_t { length: buf1.len() as u64, address: buf1.as_ptr() as *mut c_char };
244
245            let buf2 = b"parm2";
246            let mut msg2 =
247                ydb_string_t { length: buf2.len() as u64, address: buf2.as_ptr() as *mut c_char };
248
249            let buf3 = b"parm3";
250            let mut msg3 =
251                ydb_string_t { length: buf3.len() as u64, address: buf3.as_ptr() as *mut c_char };
252
253            unsafe {
254                cip_t!(
255                    YDB_NOTTP,
256                    Vec::new(),
257                    &mut routine,
258                    &mut ret_msg,
259                    &mut msg1 as *mut _,
260                    &mut msg2 as *mut _,
261                    &mut msg3 as *mut _
262                )
263                .unwrap();
264                ret_buf.set_len(ret_msg.length as usize);
265            };
266            assert_eq!(&ret_buf, b"parm3parm2parm1");
267        });
268    }
269    #[test]
270    fn int_args() {
271        call(|| {
272            use crate::craw::ydb_long_t;
273
274            let mut routine = CallInDescriptor::new(CString::new("Add").unwrap());
275            let a = 1 as ydb_long_t;
276            let b = 2 as ydb_long_t;
277            let mut out = 0;
278            unsafe {
279                cip_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap();
280            }
281            assert_eq!(out, 3);
282            // make sure it works if called multiple times
283            unsafe {
284                cip_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap();
285            }
286            assert_eq!(out, 3);
287            // make sure it works with `ci_t`
288            let mut routine = routine.into_cstr();
289            unsafe {
290                ci_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap();
291            }
292            assert_eq!(out, 3);
293        });
294    }
295    #[test]
296    fn no_args() {
297        call(|| {
298            let mut routine = CStr::from_bytes_with_nul(b"noop\0").unwrap();
299            unsafe {
300                ci_t!(YDB_NOTTP, Vec::new(), &mut routine).unwrap();
301            }
302        });
303    }
304    #[test]
305    fn no_callin_env_var() {
306        // This modifies the active call-in table and so cannot run in parallel with other tests in
307        // this module.
308        let _guard = LockGuard::write();
309
310        // NOTE: this does NOT set ydb_ci
311        env::set_var("ydb_routines", "examples/m-ffi");
312
313        let file = CString::new("examples/m-ffi/calltab.ci").unwrap();
314        let (descriptor, err_buf) = ci_tab_open_t(YDB_NOTTP, Vec::new(), &file).unwrap();
315        ci_tab_switch_t(YDB_NOTTP, err_buf, descriptor).unwrap();
316
317        // same as doc-test for `ci_t`
318        let mut buf = Vec::<u8>::with_capacity(100);
319        let mut msg = ydb_string_t {
320            length: buf.capacity() as u64,
321            address: buf.as_mut_ptr() as *mut c_char,
322        };
323        let routine = CString::new("HelloWorld1").unwrap();
324        unsafe {
325            ci_t!(YDB_NOTTP, Vec::new(), &routine, &mut msg as *mut _).unwrap();
326            buf.set_len(msg.length as usize);
327        }
328        assert_eq!(&buf, b"entry called");
329    }
330    #[test]
331    fn tab_open_switch() {
332        // This test cannot run in parallel with any others.
333        let _guard = LockGuard::write();
334
335        // NOTE: this does NOT set ydb_ci
336        env::set_var("ydb_routines", "examples/m-ffi");
337
338        let small_file = CString::new("examples/m-ffi/small_calltab.ci").unwrap();
339        let mut routine = CallInDescriptor::new(CString::new("Add").unwrap());
340        let a = 1 as ydb_long_t;
341        let b = 2 as ydb_long_t;
342        let mut out = 0;
343
344        // first try a table that doesn't have `Add`
345        let (small_fd, _) = ci_tab_open_t(YDB_NOTTP, Vec::new(), &small_file).unwrap();
346        ci_tab_switch_t(YDB_NOTTP, Vec::new(), small_fd).unwrap();
347
348        let err =
349            unsafe { cip_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap_err() };
350        assert_eq!(err.status, craw::YDB_ERR_CINOENTRY);
351        assert_eq!(out, 0);
352
353        // now try a table that does
354        let big_file = CString::new("examples/m-ffi/calltab.ci").unwrap();
355        let (big_fd, _) = ci_tab_open_t(YDB_NOTTP, Vec::new(), &big_file).unwrap();
356        let (small_fd, _) = ci_tab_switch_t(YDB_NOTTP, Vec::new(), big_fd).unwrap();
357
358        unsafe { cip_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap() };
359        assert_eq!(out, 3);
360
361        // make sure the call works even though the calltable has been changed back
362        out = 0;
363        ci_tab_switch_t(YDB_NOTTP, Vec::new(), small_fd).unwrap();
364        unsafe { cip_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap() };
365        assert_eq!(out, 3);
366
367        // make sure the old descriptor still works and updates the `Add` function name when called with `ci_t`
368        let mut routine = routine.into_cstr();
369        out = 0;
370        let err =
371            unsafe { ci_t!(YDB_NOTTP, Vec::new(), &mut routine, &mut out, a, b).unwrap_err() };
372        assert_eq!(err.status, craw::YDB_ERR_CINOENTRY);
373        assert_eq!(out, 0);
374
375        // switch back the calltable to use an environment variable now that we're done
376        ci_tab_switch_t(YDB_NOTTP, Vec::new(), CallInTableDescriptor::default()).unwrap();
377    }
378    #[test]
379    // Test that M FFI works from within a transaction
380    fn call_in_tp() {
381        use crate::simple_api::{tp_st, TransactionStatus};
382
383        // Set up environment variables
384        call(|| {
385            let do_callin = |tptoken| {
386                // Create a C string with a no-op M function
387                let mut routine = CStr::from_bytes_with_nul(b"noop\0").unwrap();
388                unsafe {
389                    // Call the `noop` M function
390                    ci_t!(tptoken, Vec::new(), &mut routine).unwrap();
391                }
392                Ok(TransactionStatus::Ok)
393            };
394            // Start a transaction before making the call
395            tp_st(YDB_NOTTP, Vec::new(), do_callin, "BATCH", &[]).unwrap();
396        });
397    }
398}