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}