polyplug_lua 0.1.1

Lua loader for polyplug - loads LuaJIT plugins via mlua
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
//! LuaHostBridge — Bridge for Lua hosts implementing host contracts.
//!
//! This module provides the bridge that allows Lua hosts to implement
//! host contracts. The bridge stores Lua callable functions and dispatches
//! calls through the LuaJIT VM.
//!
//! # Architecture
//!
//! When a Lua host registers a host contract implementation:
//! 1. The Lua callable is stored in a HashMap keyed by contract_id
//! 2. When a plugin calls a host contract function, the bridge:
//!    - Looks up the Lua callable
//!    - Invokes it with converted arguments
//!    - Converts the result back to ABI format
//!
//! # Thread Safety
//!
//! The bridge uses `RwLock` to protect the contracts HashMap because:
//! - Registration happens during initialization (write lock)
//! - Calls happen during plugin execution (read lock)
//! - mlua's `send` feature makes `Lua` and `Function` Send/Sync safe
//!
//! # Lua VM Ownership
//!
//! The bridge owns its own `Lua` instance for complete isolation from
//! plugin Lua VMs. This ensures:
//! - Host contract implementations don't interfere with plugin state
//! - Multiple Runtime instances can have isolated Lua host bridges

use std::collections::HashMap;
use std::sync::RwLock;

use mlua::Function;
use mlua::Lua;

use polyplug::host_bridge::BridgeError;
use polyplug::host_bridge::RuntimeLanguageBridge;
use polyplug_abi::AbiError;
use polyplug_abi::AbiErrorCode;
use polyplug_abi::StringView;
use polyplug_abi::SupportedLanguage;

/// Bridge for Lua hosts implementing host contracts.
///
/// This bridge stores Lua callable functions and dispatches calls through
/// the LuaJIT VM. The bridge handles:
/// - Thread-safe storage of registered implementations
/// - Lua error handling and conversion to AbiError
/// - Per-bridge Lua VM isolation
///
/// # Example
///
/// ```rust,ignore
/// use polyplug_lua::bridge::LuaHostBridge;
/// use polyplug::host_bridge::RuntimeLanguageBridge;
///
/// let bridge = LuaHostBridge::new();
///
/// // Register a Lua implementation
/// let lua = bridge.lua();
/// let callable = lua.load("function(fn_id, args, out) return fn_id end").eval::<Function>().unwrap();
/// bridge.register_host_contract(1234, Box::new(callable));
///
/// // Call through the bridge
/// let result = bridge.call_host_contract(1234, 0, args_ptr, out_ptr);
/// ```
pub struct LuaHostBridge {
    /// The Lua VM owned by this bridge.
    /// SAFETY: mlua's `send` feature makes Lua Send + Sync.
    lua: Lua,

    /// Registered host contract implementations.
    /// Key: contract_id (FNV-1a hash of "host_contract:name@major")
    /// Value: Lua callable function (Function)
    contracts: RwLock<HashMap<u64, Function>>,
}

impl LuaHostBridge {
    /// Create a new LuaHostBridge with a fresh LuaJIT VM.
    ///
    /// # Safety
    ///
    /// Uses `Lua::unsafe_new()` to enable the LuaJIT FFI module, which is
    /// required for host contract implementations that need to interact with
    /// ABI structures (struct layout, pointer casts).
    ///
    /// We trust the Lua scripts registered through this bridge.
    pub fn new() -> LuaHostBridge {
        // SAFETY: We trust the Lua scripts loaded through this bridge.
        // The LuaJIT FFI is required for host contract implementations that
        // need to interact with ABI structures.
        let lua: Lua = unsafe { Lua::unsafe_new() };

        LuaHostBridge {
            lua,
            contracts: RwLock::new(HashMap::new()),
        }
    }

    /// Create a new LuaHostBridge with pre-allocated capacity.
    pub fn with_capacity(capacity: usize) -> LuaHostBridge {
        // SAFETY: Same as new() — we trust the Lua scripts.
        let lua: Lua = unsafe { Lua::unsafe_new() };

        LuaHostBridge {
            lua,
            contracts: RwLock::new(HashMap::with_capacity(capacity)),
        }
    }

    /// Get a reference to the underlying Lua VM.
    ///
    /// This allows host code to create Lua functions for registration.
    pub fn lua(&self) -> &Lua {
        &self.lua
    }
}

impl Default for LuaHostBridge {
    fn default() -> LuaHostBridge {
        LuaHostBridge::new()
    }
}

impl RuntimeLanguageBridge for LuaHostBridge {
    /// Returns `SupportedLanguage::Lua` to identify this as a Lua bridge.
    fn runtime_type(&self) -> SupportedLanguage {
        SupportedLanguage::Lua
    }

    /// Register a Lua callable as a host contract implementation.
    ///
    /// The `implementation` must be a `Box<Function>` containing a Lua
    /// callable function. The bridge stores this function for later dispatch.
    ///
    /// # Arguments
    ///
    /// - `contract_id`: The FNV-1a hash of `"host_contract:name@major"`
    /// - `implementation`: A boxed Lua function (`Function`)
    ///
    /// # Errors
    ///
    /// - `BridgeError::DuplicateContract`: Contract already registered
    /// - `BridgeError::TypeMismatch`: Implementation is not a `Function`
    fn register_host_contract(
        &mut self,
        contract_id: u64,
        implementation: Box<dyn core::any::Any>,
    ) -> Result<(), BridgeError> {
        // Attempt to downcast to Function
        let callable: Function = implementation
            .downcast::<Function>()
            .map_err(|_| BridgeError::TypeMismatch {
                contract_id,
                expected: "Function".to_owned(),
                got: "unknown type".to_owned(),
            })
            .map(|boxed| *boxed)?;

        // Acquire write lock and insert
        let mut contracts: std::sync::RwLockWriteGuard<'_, HashMap<u64, Function>> = self
            .contracts
            .write()
            .map_err(|_| BridgeError::VmRegistrationFailed {
                contract_id,
                reason: "failed to acquire write lock on contracts map".to_owned(),
            })?;

        if contracts.contains_key(&contract_id) {
            return Err(BridgeError::DuplicateContract { contract_id });
        }

        contracts.insert(contract_id, callable);
        Ok(())
    }

    /// Call a host contract function through Lua dispatch.
    ///
    /// This method:
    /// 1. Looks up the registered Lua callable
    /// 2. Calls the function with converted arguments
    /// 3. Returns the result or an error
    ///
    /// # Arguments
    ///
    /// - `contract_id`: The contract ID to look up
    /// - `fn_id`: Function index within the contract (0-based)
    /// - `args`: Pointer to packed ABI arguments (layout defined by contract)
    /// - `out`: Pointer to output buffer for return value
    ///
    /// # Returns
    ///
    /// - `AbiError::ok()` on success
    /// - `AbiError { code: AbiErrorCode::HostContractNotFound, ... }` if contract not found
    /// - `AbiError { code: AbiErrorCode::HostContractCallFailed, ... }` if dispatch failed
    ///
    /// # Safety
    ///
    /// This method is inherently unsafe because it deals with raw pointers:
    /// - `args` must point to valid ABI-packed arguments for the contract
    /// - `out` must point to a valid buffer sized for the return type
    /// - The caller must ensure proper alignment of both pointers
    ///
    /// # Note
    ///
    /// For MVP, this implementation provides basic dispatch functionality.
    /// Full type marshaling for all primitive types will be added in future tasks.
    unsafe fn call_host_contract(
        &self,
        contract_id: u64,
        fn_id: u32,
        args: *const (),
        out: *mut (),
    ) -> AbiError {
        // Step 1: Look up the registered callable
        let contracts_guard: std::sync::RwLockReadGuard<'_, HashMap<u64, Function>> =
            match self.contracts.read() {
                Ok(guard) => guard,
                Err(_) => {
                    return AbiError {
                        code: AbiErrorCode::HostContractCallFailed as u32,
                        message: StringView::from_static(
                            b"failed to acquire read lock on contracts map",
                        ),
                    };
                }
            };

        let callable: &Function = match contracts_guard.get(&contract_id) {
            Some(f) => f,
            None => {
                return AbiError {
                    code: AbiErrorCode::HostContractNotFound as u32,
                    message: StringView::from_static(b"host contract not found"),
                };
            }
        };

        // Step 2: Call the function
        // For MVP, we pass fn_id as the first argument and args/out as opaque pointers
        // Full type marshaling will be implemented in future tasks
        //
        // Pass pointers as i64 to preserve full 64-bit precision on LuaJIT.
        // LuaJIT lua_Integer is int64_t — safe for pointer-width integers.
        let fn_id_arg: u32 = fn_id;
        let args_ptr: i64 = args as usize as i64;
        let out_ptr: i64 = out as usize as i64;

        let call_result: Result<(), mlua::Error> =
            callable.call::<()>((fn_id_arg, args_ptr, out_ptr));

        match call_result {
            Ok(()) => AbiError::ok(),
            Err(e) => {
                // The full Lua error detail propagates to the caller in the
                // AbiError message below; no side-channel print.
                let message: String = format!("Lua exception: {}", e);
                // SAFETY: We leak the message string to create a 'static StringView.
                // This is acceptable because:
                // 1. Error messages are small and short-lived
                // 2. The alternative would require host_alloc which we don't have here
                // 3. This matches the pattern used in other loaders
                let message_static: &'static str = Box::leak(message.into_boxed_str());
                AbiError {
                    code: AbiErrorCode::HostContractCallFailed as u32,
                    message: StringView {
                        ptr: message_static.as_ptr(),
                        len: message_static.len(),
                    },
                }
            }
        }
    }
}

// SAFETY: LuaHostBridge is Send because:
// - RwLock<HashMap<u64, Function>> is Send (RwLock is Send, HashMap is Send)
// - Lua is Send when compiled with mlua's `send` feature (which we use)
// - Function is Send when compiled with mlua's `send` feature
// - All operations that access Lua objects are thread-safe due to mlua's internal synchronization
unsafe impl Send for LuaHostBridge {}

// SAFETY: LuaHostBridge is Sync because:
// - RwLock provides synchronization for the contracts HashMap
// - Lua is Sync when compiled with mlua's `send` feature (which we use)
// - mlua's internal mutex provides synchronization for Lua state access
// - Concurrent reads are safe (read lock + mlua's internal sync)
// - Concurrent writes are serialized (write lock + mlua's internal sync)
unsafe impl Sync for LuaHostBridge {}

#[cfg(test)]
mod tests {
    #![allow(clippy::expect_used)]

    use super::*;

    #[test]
    fn bridge_new_creates_empty_bridge() {
        let bridge: LuaHostBridge = LuaHostBridge::new();
        let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Function>> =
            bridge.contracts.read().expect("read lock");
        assert!(contracts.is_empty());
    }

    #[test]
    fn bridge_default_creates_empty_bridge() {
        let bridge: LuaHostBridge = LuaHostBridge::default();
        let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Function>> =
            bridge.contracts.read().expect("read lock");
        assert!(contracts.is_empty());
    }

    #[test]
    fn bridge_with_capacity_creates_empty_bridge() {
        let bridge: LuaHostBridge = LuaHostBridge::with_capacity(10);
        let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Function>> =
            bridge.contracts.read().expect("read lock");
        assert!(contracts.is_empty());
    }

    #[test]
    fn bridge_runtime_type_returns_lua() {
        let bridge: LuaHostBridge = LuaHostBridge::new();
        assert_eq!(bridge.runtime_type(), SupportedLanguage::Lua);
    }

    #[test]
    fn bridge_lua_returns_reference() {
        let bridge: LuaHostBridge = LuaHostBridge::new();
        let lua: &Lua = bridge.lua();
        // Verify we can use the Lua reference
        let result: i64 = lua.load("return 42").eval::<i64>().expect("eval");
        assert_eq!(result, 42);
    }

    #[test]
    fn bridge_register_host_contract_success() {
        let mut bridge: LuaHostBridge = LuaHostBridge::new();

        // Create a simple Lua callable
        let callable: Function = bridge
            .lua()
            .load("function(fn_id, args, out) return fn_id end")
            .eval::<Function>()
            .expect("eval function");

        // Register it
        let result: Result<(), BridgeError> =
            bridge.register_host_contract(1234, Box::new(callable));
        assert!(result.is_ok());

        // Verify it's stored
        let contracts: std::sync::RwLockReadGuard<'_, HashMap<u64, Function>> =
            bridge.contracts.read().expect("read lock");
        assert!(contracts.contains_key(&1234));
    }

    #[test]
    fn bridge_register_host_contract_duplicate_fails() {
        let mut bridge: LuaHostBridge = LuaHostBridge::new();

        // Create a simple Lua callable
        let callable: Function = bridge
            .lua()
            .load("function(fn_id, args, out) return fn_id end")
            .eval::<Function>()
            .expect("eval function");

        // Register it twice
        let callable2: Function = bridge
            .lua()
            .load("function(fn_id, args, out) return fn_id * 2 end")
            .eval::<Function>()
            .expect("eval function 2");

        let result1: Result<(), BridgeError> =
            bridge.register_host_contract(1234, Box::new(callable));
        assert!(result1.is_ok());

        let result2: Result<(), BridgeError> =
            bridge.register_host_contract(1234, Box::new(callable2));
        assert!(result2.is_err());
        let err: BridgeError = result2.expect_err("should fail");
        assert!(matches!(
            err,
            BridgeError::DuplicateContract { contract_id: 1234 }
        ));
    }

    #[test]
    fn bridge_register_host_contract_type_mismatch_fails() {
        let mut bridge: LuaHostBridge = LuaHostBridge::new();

        // Try to register a non-Function
        let result: Result<(), BridgeError> = bridge.register_host_contract(1234, Box::new(42i32));
        assert!(result.is_err());
        let err: BridgeError = result.expect_err("should fail");
        assert!(matches!(
            err,
            BridgeError::TypeMismatch {
                contract_id: 1234,
                ..
            }
        ));
    }

    #[test]
    fn bridge_call_host_contract_not_found() {
        let bridge: LuaHostBridge = LuaHostBridge::new();

        // SAFETY: null pointers are valid here — the lookup fails before any
        // pointer is dereferenced (contract 9999 is not registered).
        let result: AbiError =
            unsafe { bridge.call_host_contract(9999, 0, core::ptr::null(), core::ptr::null_mut()) };
        assert_eq!(result.code, AbiErrorCode::HostContractNotFound as u32);
    }

    #[test]
    fn bridge_call_host_contract_success() {
        let mut bridge: LuaHostBridge = LuaHostBridge::new();

        // Create a simple Lua callable that returns fn_id
        let callable: Function = bridge
            .lua()
            .load("function(fn_id, args, out) return fn_id end")
            .eval::<Function>()
            .expect("eval function");

        // Register it
        bridge
            .register_host_contract(1234, Box::new(callable))
            .expect("register");

        // Call it
        // SAFETY: the registered Lua function treats args/out as opaque integers and
        // never dereferences them, so passing null pointers is sound here.
        let result: AbiError =
            unsafe { bridge.call_host_contract(1234, 5, core::ptr::null(), core::ptr::null_mut()) };
        assert!(result.is_ok());
    }

    #[test]
    fn bridge_call_host_contract_exception() {
        let mut bridge: LuaHostBridge = LuaHostBridge::new();

        // Create a Lua callable that raises an error
        let callable: Function = bridge
            .lua()
            .load("function(fn_id, args, out) error('test error') end")
            .eval::<Function>()
            .expect("eval function");

        // Register it
        bridge
            .register_host_contract(1234, Box::new(callable))
            .expect("register");

        // Call it - should return error
        // SAFETY: the registered Lua function raises before touching args/out, so
        // passing null pointers is sound here.
        let result: AbiError =
            unsafe { bridge.call_host_contract(1234, 0, core::ptr::null(), core::ptr::null_mut()) };
        assert_eq!(result.code, AbiErrorCode::HostContractCallFailed as u32);
    }
}