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}