fm_plugin/plugin.rs
1use crate::{
2 fmx_Data, fmx_DataVect, fmx_ExprEnv, fmx_ExtPluginType, fmx__fmxcpt, fmx_ptrtype,
3 write_to_u16_buff, AllowedVersions, ApplicationVersion, Data, DataVect, ExprEnv,
4 ExternStringType, ExternVersion, FMError, FM_ExprEnv_RegisterExternalFunctionEx,
5 FM_ExprEnv_RegisterScriptStep, FM_ExprEnv_UnRegisterExternalFunction,
6 FM_ExprEnv_UnRegisterScriptStep, QuadChar, Text,
7};
8use widestring::U16CStr;
9
10/// Implement this trait for your plugin struct. The different functions are used to give FileMaker information about the plugin. You also need to register all your functions/script steps in the trait implementation.
11///
12/// # Example
13/// ```rust
14/// # use fm_plugin::prelude::*;
15/// # use fm_plugin::{DataVect, ExprEnv, Data, FMError};
16/// # struct MyFunction;
17/// # impl FileMakerFunction for MyFunction {
18/// # fn function(id: i16, env: &ExprEnv, args: &DataVect, result: &mut Data) -> FMError {
19/// # FMError::NoError
20/// # }
21/// # }
22/// struct MyPlugin;
23///
24/// impl Plugin for MyPlugin {
25/// fn id() -> &'static [u8; 4] { &b"MyPl" }
26/// fn name() -> &'static str { "MY PLUGIN" }
27/// fn description() -> &'static str { "Does all sorts of great things." }
28/// fn url() -> &'static str { "http://myplugin.com" }
29///
30/// fn register_functions() -> Vec<Registration> {
31/// vec![Registration::Function {
32/// id: 100,
33/// name: "MyPlugin_MyFunction",
34/// definition: "MyPlugin_MyFunction( arg1 ; arg2 )",
35/// description: "Does some really great stuff.",
36/// min_args: 2,
37/// max_args: 2,
38/// display_in_dialogs: true,
39/// compatibility_flags: Compatibility::Future as u32,
40/// min_ext_version: ExternVersion::V160,
41/// min_fm_version: "18.0.2",
42/// allowed_versions: AllowedVersions {developer: true, pro: true, web: true, sase: true, runtime: true},
43/// function_ptr: Some(MyFunction::extern_func),
44/// }
45/// ]
46/// }
47/// }
48/// ```
49pub trait Plugin {
50 /// Unique 4 letter identifier for the plug-in.
51 fn id() -> &'static [u8; 4];
52 /// Plug-in's name.
53 fn name() -> &'static str;
54 /// Description of the plug-in.
55 fn description() -> &'static str;
56 /// Url to send users to from the help in FileMaker. The function's name that the user will be appended to the url when clicked.
57 fn url() -> &'static str;
58
59 /// Register all custom functions/script steps
60 fn register_functions() -> Vec<Registration>;
61
62 /// Defaults to false
63 fn enable_configure_button() -> bool {
64 false
65 }
66 /// Defaults to true
67 fn enable_init_and_shutdown() -> bool {
68 true
69 }
70 /// Defaults to false
71 fn enable_idle() -> bool {
72 false
73 }
74 /// Defaults to false
75 fn enable_file_and_session_shutdown() -> bool {
76 false
77 }
78
79 fn session_shutdown(_session_id: fmx_ptrtype) {}
80 fn file_shutdown(_session_id: fmx_ptrtype, _file_id: fmx_ptrtype) {}
81 fn preferences() {}
82 fn idle(_session_id: fmx_ptrtype) {}
83 fn not_idle(_session_id: fmx_ptrtype) {}
84 fn script_paused(_session_id: fmx_ptrtype) {}
85 fn script_running(_session_id: fmx_ptrtype) {}
86 fn un_safe(_session_id: fmx_ptrtype) {}
87}
88
89pub trait PluginInternal<T>
90where
91 T: Plugin,
92{
93 fn get_string(
94 which_string: ExternStringType,
95 _win_lang_id: u32,
96 out_buffer_size: u32,
97 out_buffer: *mut u16,
98 ) {
99 use ExternStringType::*;
100 let string = match which_string {
101 Name => T::name().to_string(),
102 AppConfig => T::description().to_string(),
103 Options => {
104 let mut options: String = ::std::str::from_utf8(T::id()).unwrap().to_string();
105 options.push('1');
106 options.push(if T::enable_configure_button() {
107 'Y'
108 } else {
109 'n'
110 });
111 options.push('n');
112 options.push(if T::enable_init_and_shutdown() {
113 'Y'
114 } else {
115 'n'
116 });
117 options.push(if T::enable_idle() { 'Y' } else { 'n' });
118 options.push(if T::enable_file_and_session_shutdown() {
119 'Y'
120 } else {
121 'n'
122 });
123 options.push('n');
124 options
125 }
126 HelpUrl => T::url().to_string(),
127 Blank => "".to_string(),
128 };
129 unsafe { write_to_u16_buff(out_buffer, out_buffer_size, &string) }
130 }
131
132 fn initialize(
133 ext_version: ExternVersion,
134 app_version: ApplicationVersion,
135 app_version_number: fmx_ptrtype,
136 ) -> ExternVersion {
137 let plugin_id = QuadChar::new(T::id());
138 for f in T::register_functions() {
139 if ext_version < f.min_ext_version()
140 || !f.is_fm_version_allowed(&app_version)
141 || !is_version_high_enough(app_version_number, f.min_fm_version())
142 {
143 continue;
144 }
145
146 if f.register(&plugin_id) != FMError::NoError {
147 return ExternVersion::DoNotEnable;
148 }
149 }
150 ExternVersion::V190
151 }
152
153 fn shutdown(version: ExternVersion) {
154 let plugin_id = QuadChar::new(T::id());
155 for f in T::register_functions() {
156 if version < f.min_ext_version() {
157 continue;
158 }
159 f.unregister(&plugin_id);
160 }
161 }
162}
163
164fn is_version_high_enough(app_version_number: fmx_ptrtype, min_version: &str) -> bool {
165 let string = unsafe { U16CStr::from_ptr_str(app_version_number as *const u16) };
166 let string = string.to_string_lossy();
167 let version_number = string.split(' ').last().unwrap();
168
169 let (major, minor, patch) = semantic_version(version_number);
170 let (min_major, min_minor, min_patch) = semantic_version(min_version);
171
172 match (major, min_major, minor, min_minor, patch, min_patch) {
173 (None, None, ..) => false,
174 (Some(major), Some(min_major), ..) if major < min_major => false,
175 (Some(major), Some(min_major), ..) if major > min_major => true,
176 (Some(major), Some(min_major), _, None, ..) if major == min_major => true,
177
178 (.., Some(minor), Some(min_minor), _, _) if minor < min_minor => false,
179 (.., Some(minor), Some(min_minor), _, _) if minor > min_minor => true,
180 (.., Some(minor), Some(min_minor), _, None) if minor == min_minor => true,
181
182 (.., Some(patch), Some(min_patch)) if patch < min_patch => false,
183 (.., Some(patch), Some(min_patch)) if patch > min_patch => true,
184 _ => true,
185 }
186}
187
188fn semantic_version(version: &str) -> (Option<u8>, Option<u8>, Option<u8>) {
189 let mut str_vec = version.split('.');
190 let major = str_vec.next();
191 let minor = str_vec.next();
192 let patch = str_vec.next();
193 (
194 match major {
195 Some(n) => n.parse::<u8>().ok(),
196 None => None,
197 },
198 match minor {
199 Some(n) => n.parse::<u8>().ok(),
200 None => None,
201 },
202 match patch {
203 Some(n) => n.parse::<u8>().ok(),
204 None => None,
205 },
206 )
207}
208
209/// Sets up the entry point for every FileMaker call into the plug-in. The function then dispatches the calls to the various trait functions you can implement.
210/// Impl [`Plugin`][Plugin] for your plugin struct, and then call the macro on it.
211///
212/// # Example
213/// ```rust
214/// use fm_plugin::prelude::*;
215///
216/// struct MyPlugin;
217///
218/// impl Plugin for MyPlugin {
219/// # fn id()-> &'static [u8; 4] { b"TEST" }
220/// # fn name()-> &'static str { "TEST" }
221/// # fn description()-> &'static str { "TEST" }
222/// # fn url()-> &'static str { "TEST" }
223/// # fn register_functions()-> Vec<Registration> { Vec::new() }
224/// // ...
225/// }
226///
227/// register_plugin!(MyPlugin);
228/// ```
229///
230/// # Macro Contents
231///```rust
232/// # use fm_plugin::prelude::*;
233/// # #[macro_export]
234/// # macro_rules! register_plugin {
235/// # ($x:ident) => {
236/// #[no_mangle]
237/// pub static mut gfmx_ExternCallPtr: *mut fmx_ExternCallStruct = std::ptr::null_mut();
238///
239/// #[no_mangle]
240/// unsafe extern "C" fn FMExternCallProc(pb: *mut fmx_ExternCallStruct) {
241/// // Setup global defined in fmxExtern.h (this will be obsoleted in a later header file)
242/// gfmx_ExternCallPtr = pb;
243/// use FMExternCallType::*;
244///
245/// // Message dispatcher
246/// match (*pb).whichCall {
247/// Init => {
248/// (*pb).result = $x::initialize(
249/// (*pb).extnVersion,
250/// ApplicationVersion::from((*pb).parm1),
251/// (*pb).parm2,
252/// ) as u64
253/// }
254/// Idle => {
255/// use IdleType::*;
256/// match IdleType::from((*pb).parm1) {
257/// Idle => $x::idle((*pb).parm2),
258/// NotIdle => $x::not_idle((*pb).parm2),
259/// ScriptPaused => $x::script_paused((*pb).parm2),
260/// ScriptRunning => $x::script_running((*pb).parm2),
261/// Unsafe => $x::un_safe((*pb).parm2),
262/// }
263/// }
264/// Shutdown => $x::shutdown((*pb).extnVersion),
265/// AppPrefs => $x::preferences(),
266/// GetString => $x::get_string(
267/// (*pb).parm1.into(),
268/// (*pb).parm2 as u32,
269/// (*pb).parm3 as u32,
270/// (*pb).result as *mut u16,
271/// ),
272/// SessionShutdown => $x::session_shutdown((*pb).parm2),
273/// FileShutdown => $x::file_shutdown((*pb).parm2, (*pb).parm3),
274/// }
275/// }
276///
277/// impl PluginInternal<$x> for $x {}
278///
279/// pub fn execute_filemaker_script<F, S>(
280/// file_name: F,
281/// script_name: S,
282/// control: ScriptControl,
283/// parameter: Option<Data>,
284/// ) -> FMError
285/// where
286/// F: ToText,
287/// S: ToText,
288/// {
289/// unsafe {
290/// (*gfmx_ExternCallPtr).execute_filemaker_script(
291/// file_name,
292/// script_name,
293/// control,
294/// parameter,
295/// )
296/// }
297/// }
298///
299/// lazy_static! {
300/// static ref GLOBAL_STATE: RwLock<HashMap<String, String>> = RwLock::new(HashMap::new());
301/// }
302///
303/// pub fn store_state(key: &str, value: &str) {
304/// let mut hmap = GLOBAL_STATE.write().unwrap();
305/// (*hmap).insert(String::from(key), String::from(value));
306/// }
307///
308/// pub fn get_state(key: &str) -> Option<String> {
309/// let hmap = GLOBAL_STATE.read().unwrap();
310/// (*hmap).get(key).cloned()
311/// }
312/// # };
313/// # }
314///
315/// ```
316#[macro_export]
317macro_rules! register_plugin {
318 ($x:ident) => {
319 #[no_mangle]
320 pub static mut gfmx_ExternCallPtr: *mut fmx_ExternCallStruct = std::ptr::null_mut();
321
322 #[no_mangle]
323 unsafe extern "C" fn FMExternCallProc(pb: *mut fmx_ExternCallStruct) {
324 // Setup global defined in fmxExtern.h (this will be obsoleted in a later header file)
325 gfmx_ExternCallPtr = pb;
326 use FMExternCallType::*;
327
328 // Message dispatcher
329 match (*pb).whichCall {
330 Init => {
331 (*pb).result = $x::initialize(
332 (*pb).extnVersion,
333 ApplicationVersion::from((*pb).parm1),
334 (*pb).parm2,
335 ) as u64
336 }
337 Idle => {
338 use IdleType::*;
339 match IdleType::from((*pb).parm1) {
340 Idle => $x::idle((*pb).parm2),
341 NotIdle => $x::not_idle((*pb).parm2),
342 ScriptPaused => $x::script_paused((*pb).parm2),
343 ScriptRunning => $x::script_running((*pb).parm2),
344 Unsafe => $x::un_safe((*pb).parm2),
345 }
346 }
347 Shutdown => $x::shutdown((*pb).extnVersion),
348 AppPrefs => $x::preferences(),
349 GetString => $x::get_string(
350 (*pb).parm1.into(),
351 (*pb).parm2 as u32,
352 (*pb).parm3 as u32,
353 (*pb).result as *mut u16,
354 ),
355 SessionShutdown => $x::session_shutdown((*pb).parm2),
356 FileShutdown => $x::file_shutdown((*pb).parm2, (*pb).parm3),
357 }
358 }
359
360 impl PluginInternal<$x> for $x {}
361
362 pub fn execute_filemaker_script<F, S>(
363 file_name: F,
364 script_name: S,
365 control: ScriptControl,
366 parameter: Option<Data>,
367 ) -> FMError
368 where
369 F: ToText,
370 S: ToText,
371 {
372 unsafe {
373 (*gfmx_ExternCallPtr).execute_filemaker_script(
374 file_name,
375 script_name,
376 control,
377 parameter,
378 )
379 }
380 }
381
382 lazy_static! {
383 static ref GLOBAL_STATE: RwLock<HashMap<String, String>> = RwLock::new(HashMap::new());
384 }
385
386 pub fn store_state(key: &str, value: &str) {
387 let mut hmap = GLOBAL_STATE.write().unwrap();
388 (*hmap).insert(String::from(key), String::from(value));
389 }
390
391 pub fn get_state(key: &str) -> Option<String> {
392 let hmap = GLOBAL_STATE.read().unwrap();
393 (*hmap).get(key).cloned()
394 }
395 };
396}
397
398/// Register [`ScriptSteps`][Registration::ScriptStep] and [`Functions`][Registration::Function] for your plugin.
399/// # Function Registration
400/// Registration enables the function so that it appears in the calculation dialog in the application.
401///
402/// * `id` is the unique id that you can use to represent which function was called, it will be passed back to the registered function as the first parameter (see the parameter of the same name in [`fmx_ExtPluginType`][fmx_ExtPluginType]).
403/// * `name` is the name of the function as it should appear in the calculation formula.
404/// * `definition` is the suggested syntax that will appear in the list of functions in the calculation dialog.
405/// * `description` is the text that will display when auto-entered into the calculation dialog. The format is "type ahead word list|description text".
406/// * `min_args` is the number of required parameters for the function. `0` is the smallest valid value.
407/// * `max_args` is the maximum number of parameters that they user should be able to specify in the calculation dialog and still have correct syntax usage for the function. Use `-1` to allow a variable number of parameters up to the number supported by calculation formulas in the application.
408/// * `compatible_flags` see bit flags above.
409/// * `function_ptr` is the pointer to the function that must match the signature defined by [`fmx_ExtPluginType`][fmx_ExtPluginType]. If you implement [`FileMakerFunction`][FileMakerFunction] for your function, then you can just reference [`MyFunction.extern_func`][FileMakerFunction::extern_func] here.
410///
411/// # Script Step Registration
412///
413/// [`Registration::ScriptStep::definition`][Registration::ScriptStep::definition] must contain XML defining the script step options. Up to ten script parameters can be specified in addition to the optional target parameter. All the parameters are defined with `<Parameter>` tags in a `<PluginStep>` grouping.
414///
415/// The attributes for a `<Parameter>` tag include:
416///
417/// * `Type` - if not one of the following four types, the parameter is ignored
418/// 1. `Calc` - a standard Specify button that brings up the calculation dialog. When the script step is executed, the calculation will be evaluated and its results passed to the plug-in
419/// 2. `Bool` - simple check box that returns the value of `0` or `1`
420/// 3. `List` - a static drop-down or pop-up list in which the id of the item selected is returned. The size limit of this list is limited by the capabilities of the UI widgets used to display it. A `List` type parameter expects to contain `<Value>` tags as specified below
421/// 4. `Target` - will include a specify button that uses the new `Insert From Target` field targeting dialog that allows a developer to put the results of a script step into a field (whether or not it is on a layout), into a variable, or insert into the current active field on a layout. If no `Target` is defined then the result `Data` object is ignored. If there are multiple `Target` definitions, only the first one will be honored.
422///
423/// * `ID` - A value in the range of `0` to `9` which is used as an index into the `DataVect` parms object for the plug-in to retrieve the value of the parameter. Indexes that are not in range or duplicated will cause the parameter to be ignored. A parameter of type `Target` ignores this attribute if specified
424///
425/// * `Label` - The name of parameter or control that is displayed in the UI
426///
427/// * `DataType` - only used by the `Calc` and `Target` parameter types. If not specified or not one of the six data types, the type `Text` will be used
428/// 1. `Text`
429/// 2. `Number`
430/// 3. `Date`
431/// 4. `Time`
432/// 5. `Timestamp`
433/// 6. `Container`
434///
435/// * `ShowInline` - value is either true or false. If defined and true, will cause the parameter to show up inlined with the script step in the Scripting Workspace
436///
437/// * `Default` - either the numeric index of the default list item or the true/false value for a bool item. Ignored for calc and target parameters
438///
439/// Parameters of type `List` are expected to contain `<Value>` tags whose values are used to construct the drop-down or pop-up list. The id of a value starts at zero but specific id can be given to a value by defining an `ID` attribute. If later values do not have an `ID` attributes the id will be set to the previous values id plus one.
440///
441/// Sample XML description:
442///```xml
443///<PluginStep>
444/// <Parameter ID="0" Type="Calc" DataType="text" ShowInline="true" Label="Mood"/>
445/// <Parameter ID="1" Type="List" ShowInline="true" Label="Color">
446/// <Value ID="0">Red</Value>
447/// <Value ID="1">Green</Value>
448/// <Value ID="2">Blue</Value>
449/// </Parameter>
450/// <Parameter ID="2" Type="Bool" Label="Beep when happy"/>
451///</PluginStep>
452///```
453pub enum Registration {
454 Function {
455 id: i16,
456 name: &'static str,
457 definition: &'static str,
458 description: &'static str,
459 min_args: i16,
460 max_args: i16,
461 display_in_dialogs: bool,
462 compatibility_flags: u32,
463 min_ext_version: ExternVersion,
464 min_fm_version: &'static str,
465 allowed_versions: AllowedVersions,
466 function_ptr: fmx_ExtPluginType,
467 },
468 ScriptStep {
469 id: i16,
470 name: &'static str,
471 definition: &'static str,
472 description: &'static str,
473 display_in_dialogs: bool,
474 compatibility_flags: u32,
475 min_ext_version: ExternVersion,
476 min_fm_version: &'static str,
477 allowed_versions: AllowedVersions,
478 function_ptr: fmx_ExtPluginType,
479 },
480}
481
482impl Registration {
483 /// Called automatically by [`register_plugin!`][register_plugin].
484 pub fn register(&self, plugin_id: &QuadChar) -> FMError {
485 let mut _x = fmx__fmxcpt::new();
486
487 let (id, n, desc, def, display, flags, func_ptr) = match *self {
488 Registration::Function {
489 id,
490 name,
491 description,
492 definition,
493 display_in_dialogs,
494 compatibility_flags,
495 function_ptr,
496 ..
497 } => (
498 id,
499 name,
500 description,
501 definition,
502 display_in_dialogs,
503 compatibility_flags,
504 function_ptr,
505 ),
506 Registration::ScriptStep {
507 id,
508 name,
509 description,
510 definition,
511 display_in_dialogs,
512 compatibility_flags,
513 function_ptr,
514 ..
515 } => (
516 id,
517 name,
518 description,
519 definition,
520 display_in_dialogs,
521 compatibility_flags,
522 function_ptr,
523 ),
524 };
525
526 let mut name = Text::new();
527 name.assign(n);
528
529 let mut description = Text::new();
530 description.assign(desc);
531
532 let mut definition = Text::new();
533 definition.assign(def);
534
535 let flags = if display { 0x0000FF00 } else { 0 } | flags;
536
537 let error = match self {
538 Registration::Function {
539 min_args, max_args, ..
540 } => unsafe {
541 FM_ExprEnv_RegisterExternalFunctionEx(
542 plugin_id.ptr,
543 id,
544 name.ptr,
545 definition.ptr,
546 description.ptr,
547 *min_args,
548 *max_args,
549 flags,
550 func_ptr,
551 &mut _x,
552 )
553 },
554 Registration::ScriptStep { .. } => unsafe {
555 FM_ExprEnv_RegisterScriptStep(
556 plugin_id.ptr,
557 id,
558 name.ptr,
559 definition.ptr,
560 description.ptr,
561 flags,
562 func_ptr,
563 &mut _x,
564 )
565 },
566 };
567
568 _x.check();
569 error
570 }
571
572 /// Returns minimum allowed sdk version for a function/script step.
573 pub fn min_ext_version(&self) -> ExternVersion {
574 match self {
575 Registration::Function {
576 min_ext_version, ..
577 } => *min_ext_version,
578 Registration::ScriptStep {
579 min_ext_version, ..
580 } => *min_ext_version,
581 }
582 }
583
584 /// Returns minimum allowed FileMaker version for a function/script step.
585 pub fn min_fm_version(&self) -> &str {
586 match self {
587 Registration::Function { min_fm_version, .. }
588 | Registration::ScriptStep { min_fm_version, .. } => *min_fm_version,
589 }
590 }
591
592 pub fn is_fm_version_allowed(&self, version: &ApplicationVersion) -> bool {
593 let allowed_versions = match self {
594 Registration::Function {
595 allowed_versions, ..
596 }
597 | Registration::ScriptStep {
598 allowed_versions, ..
599 } => allowed_versions,
600 };
601 use ApplicationVersion::*;
602 match version {
603 Developer => allowed_versions.developer,
604 Pro => allowed_versions.pro,
605 Runtime => allowed_versions.runtime,
606 SASE => allowed_versions.sase,
607 Web => allowed_versions.web,
608 }
609 }
610
611 /// Called automatically by [`register_plugin!`][register_plugin].
612 pub fn unregister(&self, plugin_id: &QuadChar) {
613 let mut _x = fmx__fmxcpt::new();
614 match self {
615 Registration::Function { id, .. } => unsafe {
616 FM_ExprEnv_UnRegisterExternalFunction(plugin_id.ptr, *id, &mut _x);
617 },
618 Registration::ScriptStep { id, .. } => unsafe {
619 FM_ExprEnv_UnRegisterScriptStep(plugin_id.ptr, *id, &mut _x);
620 },
621 }
622 _x.check();
623 }
624}
625
626pub trait FileMakerFunction {
627 /// Define your custom function here. Set the return value to the result parameter.
628 fn function(id: i16, env: &ExprEnv, args: &DataVect, result: &mut Data) -> FMError;
629
630 /// Entry point for FileMaker to call your function.
631 extern "C" fn extern_func(
632 id: i16,
633 env_ptr: *const fmx_ExprEnv,
634 args_ptr: *const fmx_DataVect,
635 result_ptr: *mut fmx_Data,
636 ) -> FMError {
637 let arguments = DataVect::from_ptr(args_ptr);
638 let env = ExprEnv::from_ptr(env_ptr);
639 let mut result = Data::from_ptr(result_ptr);
640
641 Self::function(id, &env, &arguments, &mut result)
642 }
643}