papyrus 0.17.2

A rust repl and script runner
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
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
//! Linking an external crate and sharing data.
//!
//! When running a REPL you might want to link an external crate.
//! The specific use case is a developer wants to link the crate they are working on
//! into the REPL for the user to be able to use.
//! A developer might also want to make data available to the REPL.
//! Papyrus has this functionality but makes some assumptions that the developer will
//! need to be aware of, detailed below.
//!
//! ## Worked Example
//!
//! A REPL instance should always be created by invoking the macro `repl!()`.
//! In the examples below this will be elided for as the documentation won't compile with the macros.
//! The macro accepts a type ascription (such as `u32`, `String`, `MyStruct`, etc.) which defines the generic data constraint of the REPL.
//! When an evaluation call is made, a mutable reference of the same type will be required to be passed through.
//! Papyrus uses this data to pass it (across an FFI boundary) for the REPL to access.
//!
//! To show the functionality of linking, let's work on a crate called `some-lib`.
//!
//! ### File Setup
//!
//! ***main.rs***:
//! ```rust,no_run
//! #[macro_use]
//! extern crate papyrus;
//!
//! use papyrus::prelude::*;
//!
//! # #[cfg(not(feature = "runnable"))]
//! # fn main() {}
//!
//! # #[cfg(feature = "runnable")]
//! fn main() {
//!   let mut repl = repl!();
//!
//!   let d = &mut ();
//!
//!   repl.run(papyrus::run::RunCallbacks::new(d));
//! }
//! ```
//!
//! ***lib.rs***:
//! ```rust
//! pub struct MyStruct {
//!   pub a: i32,
//!   pub b: i32,
//! }
//!
//! impl MyStruct {
//!   pub fn new(a: i32, b: i32) -> Self {
//!     MyStruct { a, b }
//!   }
//!
//!   pub fn add_contents(&self) -> i32 {
//!     self.a + self.b
//!   }
//! }
//! ```
//!
//! ***Cargo.toml***:
//! ```toml
//! [package]
//! name = "some-lib"
//!
//! ...
//!
//! [lib]
//! name = "some_lib"
//! crate-type = ["rlib" ]
//! path = "src/lib.rs" # you may need path to the library
//!
//! [dependencies]
//! papyrus = { version = "*", crate-type = [ "rlib" ] }
//! ...
//! ```
//!
//! Notice that you will have to specify the _library_ with a certain `crate-type`.
//! Papyrus links using an `rlib` file, but it is shown that you can also build multiple library files.
//! If you build this project you should find a `libsome_lib.rlib` sitting in your build directory.
//! Papyrus uses this to link when compiling.
//! The `papyrus` dependency also requires a `crate-type` specification.
//! If not specified, references to `papyrus` in the _library_ will cause compilation errors when
//! running the REPL.
//!
//! ### REPL
//!
//! Run this project (`cargo run`). It should spool up fine and prompt you with `papyrus=>`.
//! Now you can try to use the linked crate.
//!
//! ```sh
//! papyrus=> some_lib::MyStruct::new(20, 30).add_contents()
//! papyrus [out0]: 50
//! ```
//!
//! ## Behind the scenes
//!
//! - Papyrus takes the crate name you specify and will add this as `extern crate CRATE_NAME;` to the source file.
//! - When setting the external crate name, the `rlib` library is found and copied into the compilation directory.
//!   - Papyrus uses `std::env::current_exe()` to find the executing folder, and searches for the `rlib` file in that folder (`libCRATE_NAME.rlib`)
//!   - Specify the path to the `rlib` library if it is located in a different folder
//! - When compiling the REPL code, a rustc flag is set, linking the `rlib` such that `extern crate CRATE_NAME;` works.
//!
//! ## Passing `MyStruct` data through
//!
//! Keep the example before, but alter the `main.rs` file.
//!
//! ***main.rs***:
//! ```rust,ignore
//! #[macro_use]
//! extern crate papyrus;
//! extern crate some_lib;
//!
//! use some_lib::MyStruct;
//!
//! # #[cfg(not(feature = "runnable"))]
//! # fn main() {}
//!
//! # #[cfg(feature = "runnable")]
//! fn main() {
//!   let mut app_data = MyStruct::new(20, 10);
//!
//!   let mut repl = repl!(some_lib::MyStruct);
//!
//!   repl.data = repl
//!     .data
//!     .with_extern_crate("some_lib", None)
//!     .expect("failed creating repl data");
//!
//!   repl.run(&mut app_data);
//! }
//! ```
//!
//! Run this project (`cargo run`).
//! It should spool up fine and prompt you with `papyrus=>`.
//! Now you can try to use the linked data.
//! The linked data is in a variable `app_data`. It is borrowed or mutably borrowed depending on the
//! REPL state.
//!
//! ```sh
//! papyrus=> app_data.add_contents()
//! papyrus [out0]: 50
//! ```
//!
//! ## Notes
//! ### Panics
//!
//! To avoid crashing the application on a panic, `catch_unwind` is employed.
//! This function requires data that crosses the boundary be `UnwindSafe`, making `&` and `&mut` not valid data types.
//! Papyrus uses `AssertUnwindSafe` wrappers to make this work, however it makes `app_data` vulnerable to breaking
//! invariant states if a panic is triggered.
//!
//! The developer should keep this in mind when implementing a linked REPL.
//! Some guidelines:
//!
//! 1. Keep the app_data that is being transfered simple.
//! 2. Develop wrappers that only pass through a _clone_ of the data.
//!
//! ## Dependency Duplication
//! When linking an external library, the `deps` folder is linked to ensure that the dependencies that
//! the library is built with link properly. There are specific use cases where the rust compiler will
//! be unable to determine what dependencies to use. This happens when:
//! - The library has a dependency `depx`
//! - The REPL is asked to use a dependency `depx`
//! - The library and REPL both use the _exact same dependency structure_ for `depx`
//!   - This means that `depx` is the same version, and has the same feature set enabled
//! - The library and REPL both _use_ the dependency in code
//!
//! As an example, the use of the `rand` crate might cause compilation issues to arise if the linked
//! external library also relies of `rand`. The exact cause is having both crates in the dependency
//! graph that rustc cannot discern between. The compilation error is however a good indication that
//! the external library needs to be supplying these transitive dependencies for the REPL's use, as the
//! REPL is really using the external library as a dependency (just in an indirect manner).
//! Usually an error message such as `error[E0523]: found two different crates with name `rand` that
//! are not distinguished by differing -C metadata. This will result in symbol conflicts between the
//! two.` would be encountered.
//!
//! To solve this issue, any REPL dependency that could overlap with a library dependency be exposed by
//! the _library itself_. This can be done by using `pub use depx;` or `pub extern crate depx;` in the
//! root of the library source. Then, alter the `persistent_module_code` on the linking configuration
//! to include a statement such as `use external_lib::depx;` where the external lib is your library
//! name. If you library had the name `awesome` and you wanted to expose the `rand` crate you would add
//! `use awesome::rand;` to the `persistent_module_code` (make sure to test for whitespace and add if
//! necessary). There is access to the `persistent_module_code` through the
//! [`ReplData`](crate::repl::ReplData).
//!
//! Adding this code effectively aliases the library dependency as if it was a root dependency of the
//! REPL. This trick is especially important if one is linking a library that makes use of the `kserd`
//! crate and has implemented `ToKserd` so data types can automatically be transferred across the REPL
//! boundary. The REPL needs to _not_ use the `kserd` dependency it is using and use the `kserd`
//! dependency from the external library. Using `use external_lib::kserd;` will manage this.
//!
//! This is also important as then if the user of the REPL wants to implement `ToKserd` on REPL types,
//! it will still be using the consistent `kserd` dependency, although an astute user might try to
//! implement `::kserd::ToKserd` which would break! At least at this point it is easy to back out
//! changes in the temporary REPL session.

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::{fs, io};

/// The external crate and data linking configuration.
pub struct LinkingConfiguration {
    /// Linking data configuration.
    ///
    /// If the user wants to transfer data from the calling application
    /// then it can specify the type of data as a string.
    /// The string must include library and module path, unless accessible
    /// from std library.
    ///
    /// Example: `MyStruct` under the module `some_mod` in crate `some_lib`
    /// - will add `some_lib::some_mod::MyStruct` to the function argument
    /// - function looks like `fn(app_data: &some_lib::some_mod::MyStruct)`
    pub data_type: Option<String>,

    /// Flag whether to prepend `mut` to fn signature (ie `app_data: &mut data_type`).
    /// Indicates a mutable block.
    pub mutable: bool,

    /// Additional external libraries to link.
    ///
    /// These are only precompiled libraries, it is preferable
    /// to link dependencies using `crates.io`.
    ///
    /// The set contains the library names, such as `rand`.
    pub external_libs: HashSet<Extern>,

    /// Code to append to the top of a module.
    ///
    /// It is sometimes necessary to have injected code, especially to solve dependency duplication
    /// issues. See [`the _linking_ module for a description`](crate::linking).
    pub persistent_module_code: String,
}

impl Default for LinkingConfiguration {
    fn default() -> Self {
        Self {
            data_type: None,
            mutable: false,
            external_libs: HashSet::new(),
            persistent_module_code: String::new(),
        }
    }
}

impl LinkingConfiguration {
    /// Set the data type. Must be fully qualified from the crate level.
    ///
    /// # Safety
    /// This **must** match the type that is passed through.
    pub unsafe fn with_data(mut self, type_name: &str) -> Self {
        self.data_type = Some(type_name.to_string());
        self
    }

    /// Constructs the function arguments signature.
    /// Appends result to buffer.
    pub fn construct_fn_args(&self, buf: &mut String) {
        if let Some(d) = &self.data_type {
            // matches pfh::compile::execute::DataFunc definition.
            buf.push_str("app_data: &"); // 11 len
            if self.mutable {
                buf.push_str("mut ");
            }
            buf.push_str(d);
        }
    }

    /// Calculates the length of the function arguments signature.
    ///
    /// This is used to precalculate buffer sizes.
    pub fn construct_fn_args_length(&self) -> usize {
        self.data_type
            .as_ref()
            .map(|d| 11 + d.len() + if self.mutable { 4 } else { 0 })
            .unwrap_or(0)
    }
}

/// Represents an externally linked library.
///
/// The structure holds a path to an `lib*.rlib` library. The path
/// is validated upon construction. To ensure the compilation works,
/// the `deps` folder that is produced on a build must also exist in the
/// same folder as the library.
pub struct Extern {
    /// Path to rlib.
    path: PathBuf,
    alias: Option<&'static str>,
}

impl Extern {
    /// Constructs a new `Extern`al crate linkage.
    ///
    /// Validates the path and dependency folder. The file name must be of
    /// the format `lib*.rlib`, such that `*` is the library name. In the
    /// same folder that the library exists, there _must_ be a `deps` folder,
    /// even if there is no dependencies. This gets validated as well. The
    /// file must exist on disk.
    pub fn new<P: AsRef<Path>>(rlib_path: P) -> io::Result<Self> {
        Self::ctor(rlib_path, None)
    }

    /// Constructs a new `Extern`al crate linkage, with an alias for the lib name;
    ///
    /// Validates the path and dependency folder. The file name must be of
    /// the format `lib*.rlib`, such that `*` is the library name. In the
    /// same folder that the library exists, there _must_ be a `deps` folder,
    /// even if there is no dependencies. This gets validated as well. The
    /// file must exist on disk.
    pub fn with_alias<P: AsRef<Path>>(rlib_path: P, alias: &'static str) -> io::Result<Self> {
        Self::ctor(rlib_path, Some(alias))
    }

    /// Uses the executable name to derive the library name, and
    /// returns the external linking using this. _The executable and library
    /// must be in the same folder_.
    ///
    /// This is a conveniance function if the library name is the same
    /// as the executeable.
    pub fn from_current_exe() -> io::Result<Self> {
        let exe = std::env::current_exe()?;

        let name = exe
            .file_name()
            .and_then(|s| s.to_str())
            .map(|s| {
                if cfg!(windows) {
                    s.trim_end_matches(".exe")
                } else {
                    s
                }
            })
            .ok_or_else(|| {
                io::Error::new(io::ErrorKind::Other, "failed getting executable name")
            })?;

        let path = get_rlib_path(name)?;

        Self::new(path)
    }

    fn ctor<P: AsRef<Path>>(rlib_path: P, alias: Option<&'static str>) -> io::Result<Self> {
        let path = rlib_path.as_ref();

        let path = path.canonicalize()?;

        if !path.is_file() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("{} not a file on disk", path.display()),
            ));
        }

        let lib = path.file_name().and_then(|s| s.to_str()).ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::InvalidInput,
                format!("{} does not have file name", path.display()),
            )
        })?;

        if !lib.starts_with("lib") || !lib.ends_with(".rlib") {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "library must be in format lib*.rlib",
            ));
        }

        if lib[3..lib.len() - 5].is_empty() {
            return Err(io::Error::new(
                io::ErrorKind::InvalidInput,
                "library has empty name",
            ));
        }

        let deps = path.parent().expect("should have parent").join("deps");
        if !deps.is_dir() {
            return Err(io::Error::new(
                io::ErrorKind::NotFound,
                format!("{} not a directory on disk", deps.display()),
            ));
        }

        let e = Extern { path, alias };

        Ok(e)
    }

    /// The library name. This is the `*` in `lib*.rlib`.
    pub fn lib_name(&self) -> &str {
        let lib = self.path.file_name().and_then(|s| s.to_str()).unwrap(); // this has been validated

        &lib[3..lib.len() - 5]
    }

    /// The alias, is there is one.
    pub fn alias(&self) -> Option<&'static str> {
        self.alias
    }

    /// The canoncialized library path (in `lib*.rlib` format).
    pub fn lib_path(&self) -> &Path {
        self.path.as_path()
    }

    /// The canoncialized `deps` folder which lives in same directory as rlib.
    pub fn deps_path(&self) -> PathBuf {
        self.path.parent().unwrap().join("deps") // this has been validated already.
    }

    /// Append the buffer with the code representation.
    pub fn construct_code_str(&self, buf: &mut String) {
        buf.push_str("extern crate "); // 13
        buf.push_str(self.lib_name());
        if let Some(alias) = self.alias {
            buf.push_str(" as ");
            buf.push_str(alias);
        }
        buf.push_str(";\n"); // 2
    }

    /// Returns the size in bytes that the code representation will require.
    pub fn construct_code_str_length(&self) -> usize {
        13 + self.lib_name().len()
            + if let Some(alias) = self.alias {
                4 + alias.len()
            } else {
                0
            }
            + 2
    }
}

impl PartialEq for Extern {
    fn eq(&self, other: &Self) -> bool {
        self.path == other.path
    }
}

impl Eq for Extern {}

impl std::hash::Hash for Extern {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.path.hash(state)
    }
}

fn get_rlib_path(crate_name: &str) -> io::Result<PathBuf> {
    let lib_name = format!("lib{}.rlib", crate_name);
    let exe = std::env::current_exe()?;
    fs::read_dir(exe.parent().expect("files should always have a parent"))?
        .filter(|entry| entry.is_ok())
        .map(|entry| entry.expect("filtered some").path())
        .find(|path| path.ends_with(&lib_name))
        .ok_or_else(|| {
            io::Error::new(
                io::ErrorKind::NotFound,
                format!("did not find file: '{}'", lib_name),
            )
        })
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn get_rlib_path_test() {
        let r = get_rlib_path("some_crate");
        assert!(r.is_err());
        let e = r.unwrap_err();
        assert_eq!(e.kind(), io::ErrorKind::NotFound);
        assert_eq!(e.to_string(), "did not find file: 'libsome_crate.rlib'");
    }

    #[test]
    fn construct_code_str_test() {
        let mut e = Extern {
            path: PathBuf::from("libsome_lib.rlib"),
            alias: None,
        };

        let mut s = String::new();
        e.construct_code_str(&mut s);

        let ans = "extern crate some_lib;\n";
        assert_eq!(&s, ans);
        assert_eq!(e.construct_code_str_length(), ans.len());

        e.alias = Some("alias");

        let mut s = String::new();
        e.construct_code_str(&mut s);

        let ans = "extern crate some_lib as alias;\n";
        assert_eq!(&s, ans);
        assert_eq!(e.construct_code_str_length(), ans.len());
    }
}