/-
Copyright (c) 2021 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone
-/
module

prelude
public import Init.System.IO

public section

namespace Lean

private opaque DynlibImpl : NonemptyType.{0}
/-- A dynamic library handle. -/
def Dynlib := DynlibImpl.type
instance : Nonempty Dynlib := by exact DynlibImpl.property

private opaque Dynlib.SymbolImpl (dynlib : Dynlib) : NonemptyType.{0}
/-- A reference to a symbol within a dynamic library. -/
def Dynlib.Symbol (dynlib : Dynlib) := SymbolImpl dynlib |>.type
instance : Nonempty (Dynlib.Symbol dynlib) := by exact Dynlib.SymbolImpl dynlib |>.property

/--
Dynamically loads a shared library.

The path may also be used perform a system-dependent search for library.
To avoid this, use an absolute path (e.g., from `IO.FS.realPath`).
-/
@[extern "lean_dynlib_load"]
opaque Dynlib.load (path : @& System.FilePath) : IO Dynlib

/-- Returns the symbol of the dynamic library with the specified name (if any).  -/
@[extern "lean_dynlib_get"]
opaque Dynlib.get? (dynlib : @& Dynlib) (sym : @& String) : Option dynlib.Symbol

/--
Runs a module initializer function.
The symbol should have the signature `(builtin : Bool) → IO Unit`
(e.g., `initialize_Foo(uint8_t builtin, obj_arg)`).

This function is unsafe because there is no guarantee the symbol has the
expected signature. An invalid symbol can thus produce undefined behavior.
Furthermore, if the initializer introduces pointers (e.g., function closures)
from the dynamic library into the global state, future garbage collection of
the library will produce undefined behavior. In such cases, garbage collection
of the dynamic library can be prevented via `Runtime.markPersistent` or
`Runtime.forget`.
-/
@[extern "lean_dynlib_symbol_run_as_init"]
unsafe opaque Dynlib.Symbol.runAsInit {dynlib : @& Dynlib} (sym : @& dynlib.Symbol) : IO Unit

/--
Dynamically loads a shared library so that its symbols can be used by
the Lean interpreter (e.g., for interpreting `@[extern]` declarations).
Equivalent to passing `--load-dynlib=path` to `lean`.

**Lean never unloads libraries.** Attempting to load a library that defines
symbols shared with a previously loaded library (including itself) will error.
If multiple libraries share common symbols, those symbols should be linked
and loaded as separate libraries.
-/
@[export lean_load_dynlib]
def loadDynlib (path : @& System.FilePath) : IO Unit := do
  let dynlib ← Dynlib.load path
  -- Lean never unloads libraries.
  -- Safety: There are no concurrent accesses to `dynlib` at this point.
  let _ ← unsafe Runtime.markPersistent dynlib

/--
Loads a Lean plugin and runs its initializers.

A Lean plugin is a shared library built from a Lean module.
This means it has an `initialize_<module-name>` symbol that runs the
module's initializers (including its imports' initializers). Initializers
are declared with the `initialize` or `builtin_initialize` commands.

This is similar to passing `--plugin=path` to `lean`.
Lean environment initializers, such as definitions calling
`registerEnvExtension`, also require `Lean.initializing` to be `true`.
To enable them, use `loadPlugin` within a `withImporting` block. This will
set  `Lean.initializing` (but not `IO.initializing`).

**Lean never unloads plugins.** Attempting to load a plugin that defines
symbols shared with a previously loaded plugin (including itself) will error.
If multiple plugins share common symbols (e.g., imports), those symbols
should be linked and loaded separately.
-/
@[export lean_load_plugin]
def loadPlugin (path : System.FilePath) : IO Unit := do
  -- We never want to look up plugins using the system library search
  let path ← IO.FS.realPath path
  let some name := path.fileStem
    | throw <| IO.userError s!"error, plugin has invalid file name '{path}'"
  let dynlib ← Dynlib.load path
  -- Lean libraries can be prefixed with `lib` or suffixed with `_shared`
  -- under some configurations. We strip these from the initializer symbol.
  let name := name.stripPrefix "lib" |>.stripSuffix "_shared"
  let name := s!"initialize_{name}"
  let some sym := dynlib.get? name
    | throw <| IO.userError s!"error loading plugin, initializer not found '{name}'"
  -- Lean never unloads plugins (once initialized).
  -- Safety: There are no concurrent accesses to `dynlib` at this point.
  let _ ← unsafe Runtime.markPersistent dynlib
  /-
  Safety:
  * As `dynlib` is marked persistent, there is no danger of garbage collection.
  * There is still no guarantee that `sym` has the proper signature, but this
    is about as safe as it can be. The initializer naming convention helps
    avoid accidentally mistaking non-plugins as plugins.
  -/
  unsafe sym.runAsInit
