Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a "plugin" linking mode for executables #3141

Merged
merged 1 commit into from
Mar 6, 2020

Conversation

nojb
Copy link
Collaborator

@nojb nojb commented Feb 15, 2020

This PR implements #1544 (comment). Concretely, it adds a plugin linking mode for executable and executables stanzas, which produce a .cmxs (native) and .cma (bytecode) targets. By default the libraries in (libraries ...) are used for compilation, but are not linked into the resulting plugin. However, a new field is also added (statically_linked_libraries ...) (provisional name) which can specify an explicit list of libraries to be linked in.

Should solve #3136, #1544 and https://discuss.ocaml.org/t/dune-problems-using-dynlink-plugins/2874

src/dune/binary_kind.ml Outdated Show resolved Hide resolved
src/dune/exe.ml Outdated
@@ -36,6 +36,10 @@ module Linkage = struct

let js = { mode = Byte; ext = ".bc.js"; flags = [] }

let is_plugin = function
| { ext = ".cma" | ".cmxs"; _ } -> true
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Mode to avoid the hard coded extensions please.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would need to extend Link_mode.t, roughly as follows:

type t =
  | Byte
  | Native
  | Byte_with_stubs_statically_linked_in
  (* This two are new *)
  | Plugin_native
  | Plugin_byte

it it worth it? By the way we also match on the extension in the case of JS compilation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it's worth it. Although I would do Plugin of Mode.t instead.

But actually, you could also just do:

let is_plugin t = List.mem t.ext [Mode.plugin_ext Byte; Mode.plugin_ext Native]

Not very beautiful, but it's best to avoid having all these extensions everywhere.

The issue with the .js extension is indeed something that we need to fix.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took the second suggestion, it was a less invasive change.

@@ -1565,6 +1571,10 @@ module Executables = struct
"See https://github.com/ocaml/dune/issues/745 for more \
details."
]))
and+ statically_linked_libraries =
field_o "statically_linked_libraries"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this field? Why can't we just statically link all the libraries in libraries.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The precise reason is that the plugin may require a library which is also linked into the main program. If you link the library into the plugin, then you won't be able to dynlink the plugin into the program because the modules of the library are already loaded. This is fairly common: 1) the common library may contains functionality that is used both by the main program and the plugins, or 2) it could be small utility libraries (think uutf) that happen to be used by both the main application and the plugins, etc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even more concretely, the typical way plugins work is that they "register" themselves with the main program via state living on some shared module. This shared module is linked to the main program and should not/could not be linked into the plugins.

@talex5
Copy link

talex5 commented Feb 15, 2020

Thanks - this looks excellent! With this, the 0install dune file can be reduced to just:

[...]

let plugin_ext =
  match List.assoc "natdynlink_supported" Jbuild_plugin.V1.ocamlc_config with
  | "true" -> "cmxs"
  | _ -> "cma"
  | exception Not_found -> "cma"

let dune = {|
  (executable
    (name gui_gtk)
    (modes plugin)
    (libraries lablgtk2 lwt_glib zeroinstall support lwt lwt.unix lwt_react)
    (statically_linked_libraries lablgtk2 lwt_glib))

  (install
    (section lib)
|}
 ^ Printf.sprintf "(files gui_gtk.%s))" plugin_ext

let () =
  Jbuild_plugin.V1.send (if have_gtk then dune else "")

Is there an easy way to get the extension of the plugin, so that it can be installed? I tried public_name, but that wasn't allowed (fair enough).

@nojb
Copy link
Collaborator Author

nojb commented Feb 15, 2020

Is there an easy way to get the extension of the plugin, so that it can be installed? I tried public_name, but that wasn't allowed (fair enough).

Good point. Maybe it makes sense to make plugins (with a public_name) installable to the lib directory by default? What do you think?

@bobot
Copy link
Collaborator

bobot commented Feb 15, 2020

The MR in #3104 makes trivial to add a plugin system to an executable or library, and make it also easy to dynlink plugins (the documentation is not written yet). I'm interested in understanding how the features of this two MR intersects in the use cases.

@nojb
Copy link
Collaborator Author

nojb commented Feb 16, 2020

The MR in #3104 makes trivial to add a plugin system to an executable or library, and make it also easy to dynlink plugins (the documentation is not written yet). I'm interested in understanding how the features of this two MR intersects in the use cases.

Hi @bobot, thanks for pointing me to your PR, I was not aware about it.

Just from a brief glance at the PR description, I would say that maybe there is some overlap on the "plugin" stanza that you propose. But if I understand correctly, your PR assumes plugins are split into libraries and use findlib.dynload to handle dependencies. This may be useful for some (or many) use-cases and "large" plugin systems, but sometimes one "just wants to build a plugin" without depending on findlib.dynload, not having to split the code into libraries (not so feasible when your plugins consist of arbitrary subsets of a universe of hundreds of modules) and in general making full use of the cmxs format.

This (tiny, less than 30 lines of meaningful code) PR exposes the flexibility of the the cmxs format in a "minimal" way. The approach used (extending the executable stanza) matches the underlying semantics rather well. In contrast, the scope of your PR seems to be significantly larger and geared towards specifying conventions for programs with "large" plugin systems. So I think that both PRs are orthogonal in that way. Personally, I would not be able to make use of the approach in your PR and I think both #3136 and #3141 (comment) point to a want for the kind of flexibility afforded by this PR.

@nojb
Copy link
Collaborator Author

nojb commented Feb 16, 2020

Giving it some more thought, I don't see why the plugins produced by the executable stanza as in this PR could not be used by your #3104, so in that sense both PRs are complementary. Maybe even there is no need for your "plugin" stanza if it can be subsumed by the executable stanza as in this PR?

@nojb nojb force-pushed the exe_plugins branch 2 times, most recently from a5ad9ab to d5e858a Compare February 17, 2020 13:38
@nojb
Copy link
Collaborator Author

nojb commented Feb 17, 2020

@diml do you have any comments on the proposed (statically_linked_libraries ...) ?

@bobot
Copy link
Collaborator

bobot commented Feb 17, 2020

The plugin stanza is needed for creating the META in the site directory of the package.

I don't see why the plugins produced by the executable stanza as in this PR could not be used by your #3104, so in that sense both PRs are complementary.

It is possible, but @diml prefered that I don't add any specific plugin related fields to executable, he prefered to add a new stanza with a selected subset of the library stanza. Currently there is only the libraries field. So the new fields of this PR could go into the new plugin stanza of the other.

My worry are more at the big picture:

  • We can't link twice the same module, so when linking an external libraries in a cmxs are we not creating problems later? e.g the main executable start to link with the external library or another plugin link with the same external library
  • If Sites, locations, relocation #3104 is good enough for large plugin system, is it really too complicated for simple one?

@bobot
Copy link
Collaborator

bobot commented Feb 17, 2020

I updated #3104 with an example so that it is easier to understand.

@nojb
Copy link
Collaborator Author

nojb commented Feb 17, 2020

The plugin stanza is needed for creating the META in the site directory of the package.

I don't see why the plugins produced by the executable stanza as in this PR could not be used by your #3104, so in that sense both PRs are complementary.

It is possible, but @diml prefered that I don't add any specific plugin related fields to executable, he prefered to add a new stanza with a selected subset of the library stanza. Currently there is only the libraries field. So the new fields of this PR could go into the new plugin stanza of the other.

For me the following two requirements are absolutely crucial:

  • I need to be able to share modules between plugins. This is trivially doable with the executables stanza, but otherwise impossible in dune (and splitting the modules into libraries is not an option).
  • I need be able to link certain libraries statically into the plugins.

My worry are more at the big picture:

  • We can't link twice the same module, so when linking an external libraries in a cmxs are we not creating problems later? e.g the main executable start to link with the external library or another plugin link with the same external library

Indeed this can happen, and the linking mode in this PR is not meant to be a general solution to the question of plugins. If your plugins needs anything beyond what is offered in this PR then it should use a system such as the one proposed in your PR. For example, if you need to deal with non-trivial dependencies between plugins.

The point of this PR is that it is a trivial extension of the executables stanza (both syntactically and semantically), that allows access to features of the compiler toolchain that are not accessible otherwise and are useful. It is not meant to respond to the needs of full-featured plugin systems, which have other specific needs and constraints.

See my two points above; in principle I am happy with any system that can provide them... but I am very partial to the approach in this PR due to its simplicity.

@bobot
Copy link
Collaborator

bobot commented Feb 17, 2020

Indeed this can happen, and the linking mode in this PR is not meant to be a general solution to the question of plugins. If your plugins needs anything beyond what is offered in this PR then it should use a system such as the one proposed in your PR. For example, if you need to deal with non-trivial dependencies between plugins.

To be clear it is not just for non-trivial dependencies between plugins, it is just for behind able to reliably dynlink two independently created plugins.

From #1544 what I understood of your use case and so what I think is offered by this PR is to be able on windows to create a relocatable directory which contains an executable and different plugins. My goal in #3104 is also to support this use case (which is an important one). Everything is in place, for example findlib is abstracted away and so can be made relocatable, and also the choice of localisation is deferred at installation time so we can add a dune install --relocatable option.

The PR #1544 is going to be an experimental feature at first, so this PR could also be one and we could see how the use cases are supported. However I would be happy to discuss your use case in #1544 so that it could be fully supported.

@ghost
Copy link

ghost commented Feb 19, 2020

Hey, I'll let you discuss whether we need both PRs or whether one is enough, bust just a comment on linking mode vs stanza. For me, the main difference is the public name. If we attach a name to a plugin and it lives in its own namespace, then it doesn't seem right to me to use the executable stanza and it seems better to me to have a dedicated plugin stanza. If we just want to produce a file and we let the user decide what they want to do with the file, a linking mode seems just fine.

@diml do you have any comments on the proposed (statically_linked_libraries ...) ?

That seems fine, but it could be mis-interpreted for plain executables. i.e. intuitively, I would interpret it as: these libraries are statically linked and the other ones are dynamically linked, which is not the case. What about libraries_embed_in_plugin? BTW, what happens for libraries in between the direct dependencies and the statically linked ones? For instance, if a depends on b, b depends on c and we have:

(executable
 ...
 (libraries a)
 (statically_linked_libries c))

is b statically linked or not? I can't think of a scenario where not statically linking b would work. i.e. either b is linked in the main program and in this case c as well, or b isn't and it needs to be linked in the plugin.

@nojb nojb force-pushed the exe_plugins branch 2 times, most recently from f3ece0b to f4f5aa9 Compare February 20, 2020 17:16
@ghost
Copy link

ghost commented Feb 20, 2020

We discussed this in the meeting today, and everyone agreed that having both this PR and #3104 is fine as they serve different purposes. #3104 is a full key in hand system, which will allow users to very quickly get started and add a plugin system to their applications, which is a great feature to add. However, we are adventuring in the runtime territory, i.e. we are not only controlling aspects of how the software is built, but also how it will behave at runtime and it's likely that it won't work for everybody. Indeed, some developers might want to retain control over how their plugins are packaged and distributed and some might simply not have a choice. And if you cannot use #3104 and you use Dune, you are a bit on your own. This PR will bridge the gap and provide a more manual but still manageable solution.

@nojb
Copy link
Collaborator Author

nojb commented Feb 20, 2020

Is there an easy way to get the extension of the plugin, so that it can be installed? I tried public_name, but that wasn't allowed (fair enough).

I added %{ext_plugin} for this. It is equal to .cmxs if natdynlink is supported, otherwise .cma.

src/dune/exe.ml Outdated
Comment on lines 217 to 236
Result.map link_time_code_gen
~f:(fun { Link_time_code_gen.to_link; force_linkall } ->
let to_link =
let rec depends_on lib0 lib =
Lib_name.equal lib0 (Lib.name lib)
|| List.exists (Lib.requires_exn lib)
~f:(depends_on lib0)
in
List.filter_map to_link ~f:(function
| Module _ as x -> Some x
| Lib lib as x ->
if
List.exists embed_in_plugin_libraries
~f:(fun (_, lib0) -> depends_on lib0 lib)
then
Some x
else
None)
in
{ Link_time_code_gen.to_link; force_linkall })
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code filters out the library dependencies which are not inverse dependencies of the libraries listed in the (embed_in_plugin_libraries ...) field. The remaining libraries are linked into the plugin. I would appreciate a second pair of eyes on this code.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that isn't clear to me is:

if a library is mentioned in (embed_in_plugin_libraries ..), do we guarantee in will be linked in the plugin?

This code seems to suggest we also require that the library be a (compile or link?) dependency of the executable. But the name of the field suggests that the libraries should be unconditionally linked.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the PR along the lines of your suggestion -- now the libraries listed on that field are unconditionally linked, no questions asked. It does not try to be clever, just simple to understand and predictable.

@@ -241,6 +241,13 @@ module Map = struct
let model = Ocaml_config.model context.ocaml_config in
let system = Ocaml_config.system context.ocaml_config in
let version_string = Ocaml_config.version_string context.ocaml_config in
let ext_plugin =
Mode.plugin_ext
( if Ocaml_config.natdynlink_supported context.ocaml_config then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have this Dynlink_supported module that's supposed to be used for making these checks. I don't quite remember what its purpose is but it's worth looking at to make sure we aren't missing anything.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just looked, I think it is OK like this. The Dynlink_supported module seems to be used to handle (no_dynlink). I could use Dynlink_supported.By_the_os.get instead, but it amounts to the same thing, and I think it ends up obfuscating things more than anything else.

@rgrinberg
Copy link
Member

rgrinberg commented Feb 21, 2020 via email

to link statically when using ``plugin`` linking mode. By default, no
libraries are linked in. Any library which is a dependency of the executable
that is an inverse dependency of one the libraries in ``<library-list>`` will
also be statically linked.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wording is a bit confusing to me.

Any library which is a dependency of the executable

Dependency in the compilation or linking sense?

that is an inverse dependency of one the libraries in ```

So if we have:

(libraries foo baz)
(embed_in_plugin_libraries bar)

and foo depends on bar, we should link in foo and bar?

What about the transitive dependencies of bar?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the doc according to the new, simpler implementation.

lazy
(Result.List.map
~f:(fun (loc, name) ->
match Lib.DB.find libs name with
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using Lib.DB.resolve?

src/dune/lib.mli Outdated Show resolved Hide resolved
@@ -338,6 +345,7 @@ module Map = struct
; ("ext_lib", string context.lib_config.ext_lib)
; ("ext_dll", string context.lib_config.ext_dll)
; ("ext_exe", string ext_exe)
; ("ext_plugin", since ~version:(2, 4) (Var.Values [ String ext_plugin ]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should reflect this variable in the change log as well.

@@ -668,6 +668,12 @@ Executables can also be linked as object or shared object files. See
pulled in. This field is available since the 2.0 version of the dune
language.

- ``(embed_in_plugin_libraries <library-list>)`` especifies a list of libraries
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- ``(embed_in_plugin_libraries <library-list>)`` especifies a list of libraries
- ``(embed_in_plugin_libraries <library-list>)`` specifies a list of libraries

Also adds a %{ext_plugin} variable

Signed-off-by: Nicolás Ojeda Bär <[email protected]>
@rgrinberg
Copy link
Member

The issues pointed out in the review were all minor so I just fixed them myself. Thanks @nojb!

@rgrinberg rgrinberg merged commit 055e267 into ocaml:master Mar 6, 2020
rgrinberg added a commit to rgrinberg/opam-repository that referenced this pull request Mar 6, 2020
…lugin, dune-private-libs and dune-glob (2.4.0)

CHANGES:

- Add `mdx` extension and stanza version 0.1 (ocaml/dune#3094, @NathanReb)

- Allow to make Odoc warnings fatal. This is configured from the `(env ...)`
  stanza. (ocaml/dune#3029, @Julow)

- Fix separate compilation of JS when findlib is not installed. (ocaml/dune#3177, @nojb)

- Add a `dune describe` command to obtain the topology of a dune workspace, for
  projects such as ROTOR. (ocaml/dune#3128, @diml)

- Add `plugin` linking mode for executables and the `(embed_in_plugin_libraries
  ...)` field. (ocaml/dune#3141, @nojb)

- Add an `%{ext_plugin}` variable (ocaml/dune#3141, @nojb)

- Dune will no longer build shared objects for stubs if
  `supports_shared_libraries` is false (ocaml/dune#3225, fixes ocaml/dune#3222, @rgrinberg)

- Fix a memory leak in the file-watching mode (`dune build -w`)
  (ocaml/dune#3220, @snowleopard and @aalekseyev)
@nojb
Copy link
Collaborator Author

nojb commented Mar 7, 2020

The issues pointed out in the review were all minor so I just fixed them myself. Thanks @nojb!

Thanks a lot @rgrinberg, much appreciated! (Sorry, I was on holidays this week and only saw the comments now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants