Skip to content

Set dll exports - #1863

Closed
MichaelChirico wants to merge 1 commit into
r-lib:mainfrom
MichaelChirico:patch-7
Closed

MichaelChirico wants to merge 1 commit into
r-lib:mainfrom
MichaelChirico:patch-7

Conversation

@MichaelChirico

Copy link
Copy Markdown
Contributor

R-exts: https://cran.r-project.org/doc/manuals/r-devel/R-exts.html#Creating-shared-objects-1

Follows r-lib/xml2#473 and Rdatatable/data.table#7607.

As noted there, it's not clear this is really needed, but it does follow the practice of other packages like {Matrix}, and signals clear intent.


I got here looking around at r-lib repos with $(C_VISIBILITY) declarations; I would add the equivalent file to all those repos if we think it's not a waste of time :)

https://github.com/search?q=org%3Ar-lib+%2F%5B%24%5D%5B%28%5DC_VISIBILITY%2F&type=code

@lionel-

lionel- commented Jan 26, 2026

Copy link
Copy Markdown
Member

So the presence of the .def file allows proper visibility handling (e.g. hidden by default except for explicit exports) without setting the visibility flags?

@MichaelChirico

Copy link
Copy Markdown
Contributor Author

Per Ivan's comment, it seems the visibility flags don't WAI on Windows?

Rdatatable/data.table#7607 (comment)

I also don't have a Windows machine to test, but if you have one, examine objdump.exe on the rlang.dll, my inference is it will show many objects, whereas the intent of the code as written was to only show one.

@lionel-

lionel- commented Jan 27, 2026

Copy link
Copy Markdown
Member

Interesting! But as I understand things, it doesn't really matter which symbols are exported in the package DLL for these reasons:

  • Unlike on Linux, the symbol namespace of loaded dlls and main binary is not flat. Each DLL has an explicit import table and only resolves symbols from DLLs it directly imports. Because of this there is no risk of accidental collisions on Windows.

  • A package's DLL would have to really go out of its way to try and bind to a symbol from another package that's not exported via R's C callable mechanism. So I'm not really worried about making it 100% safe from illegal imports.

So for simplicity's sake I think we can safely ignore the win.def file here, even though R supports it?

@MichaelChirico

Copy link
Copy Markdown
Contributor Author

Yea, I think there's really not any issue with omitting the win.def file. It's more of a "doesn't hurt, and makes the .dll closer to the .so in intent" change.

I combed the r-package-devel and r-devel archives and only find two references to win.def:

I don't think I 100% understand, but it looks like the former requires win.def to prevent 'illegal' names from being exported, and the latter is about a manually-populated .def being done incorrectly.

I will try and ask on r-devel about this if I find the time :)

@lionel-

lionel- commented Jan 27, 2026

Copy link
Copy Markdown
Member

Unless we find a good practical reason to do it, I'd lean towards not adopting this workflow. This way it's one less thing to know about, implement support for in usethis, maintain, etc. What do you think?

@MichaelChirico

Copy link
Copy Markdown
Contributor Author

That's my own instinct as well. I'd like to understand better why other "bedrock" packages like {Matrix} do it, though. Maybe we're missing something subtle.

@MichaelChirico

Copy link
Copy Markdown
Contributor Author

Threw this to Gemini for a "neutral" (ish) third opinion, and agreeing to close. The benefits are minimal.


Here is a breakdown of the technical context, the history behind *-win.def in R, and input you can offer regarding the discussion in r-lib/rlang#1863.


Executive Summary

  • Lionel is technically correct: On Windows (PE/COFF), dynamic linking uses a two-level namespace (explicit Import Address Tables). Unlike Linux (ELF), loaded DLLs do not share a flat global symbol table, so accidental symbol collisions between different R packages at runtime are virtually impossible on Windows.
  • Why everything gets exported on Windows by default: In $(R_HOME)/share/make/winshlib.mk, if no *-win.def file is present, R runs nm $(OBJECTS) and auto-generates a tmp.def file exporting all global symbols (ignoring $(C_VISIBILITY)). Supplying rlang-win.def bypasses tmp.def and restricts exported symbols strictly to R_init_rlang.
  • Why {stats}, {grid}, and {Matrix} have *-win.def: This is a legacy convention from Base R (originating with Brian Ripley and R Core in the early 2000s) to adhere strictly to the principle of least privilege in PE export tables.
  • Recommendation: Since rlang already calls R_useDynamicSymbols(dll, FALSE) and registers all routines via R_registerRoutines(), omitting rlang-win.def has zero practical downsides or safety risks. Lionel’s instinct to not adopt or maintain *-win.def files across r-lib / usethis is sound.

1. Windows PE/COFF vs. Linux ELF Dynamic Linking

The core difference between Unix shared objects (.so) and Windows Dynamic Link Libraries (.dll) explains why symbol visibility matters far more on Linux than on Windows:

Platform Linking Model Symbol Collision Risk Role of Visibility / Exports
Linux (ELF) Flat / Global namespace (by default or when loaded with RTLD_GLOBAL) High: If Package A and Package B both export hash_string() without attribute_hidden or -fvisibility=hidden, one can interpose/override the other and cause crashes (as in data.table#7605). -fvisibility=hidden / $(C_VISIBILITY) ensures only R_init_* enters .dynsym.
Windows (PE/COFF) Two-level namespace (each module has its own Import Directory / IAT) Zero: DLLs resolve imports strictly from the specific named DLL recorded in their import table at compile time. Export tables (.edata) only matter if another binary explicitly links against that DLL via an import library (.dll.a) or calls GetProcAddress().

Because R packages on Windows do not link against each other at link time via DLL import tables, having internal C functions exported in rlang.dll does not risk clashing with another package's functions of the same name.


2. How R Builds Windows DLLs (winshlib.mk)

In R's Windows build infrastructure ($(R_HOME)/share/make/winshlib.mk), the DLL linking rule is:

$(SHLIB): $(OBJECTS)
	@if test "z$(OBJECTS)" != "z"; then \
	  if test -e "$(BASE)-win.def"; then \
	    $(SHLIB_LD) $(SHLIB_LDFLAGS) $(LDFLAGS) $(DLLFLAGS) -o $@ $(BASE)-win.def $(OBJECTS) $(ALL_LIBS); \
	  else \
	    echo EXPORTS > tmp.def; \
	    $(NM) $(OBJECTS) | $(SED) -n $(SYMPAT) $(NM_FILTER) | $(SED) $(ADDQU) >> tmp.def; \
	    $(SHLIB_LD) $(SHLIB_LDFLAGS) $(LDFLAGS) $(DLLFLAGS) -o $@ tmp.def $(OBJECTS) $(ALL_LIBS); \
	    $(RM) tmp.def; \
	  fi \
	fi
  • When pkgname-win.def is absent (the default): R extracts every global symbol ([BCDRT]) from the object files using nm and dumps them into tmp.def. The linker then exports all of them in the DLL's Export Address Table (EAT).
  • When pkgname-win.def is present: R skips tmp.def and only exports the symbols explicitly listed (e.g. R_init_rlang).

As Writing R Extensions (Section 5.5 / 6.1.1) notes:

"The visibility mechanism is not available on Windows, but there is an equally effective way to control which entry points are visible, by supplying a definitions file pkgname/src/pkgname-win.def: only entry points listed in that file will be visible."


3. Context on the Cited r-devel & r-package-devel Threads

The two mailing list threads mentioned in the PR discussion address specific edge cases rather than general runtime bugs:

  1. r-devel 2016-08 (Kyle Baron):
    • Issue: A package contained a C function named BASE(). In older versions of R (R 3.3.x), winshlib.mk did not wrap symbol names in quotes when generating tmp.def. In GNU ld module definition grammar, BASE is a reserved keyword, causing a syntax error.
    • Fix in R: R Core later added ADDQU = 's/[^ ][^ ]*/"&"/g' to winshlib.mk, ensuring all symbol names are double-quoted ("BASE"). This is no longer an issue.
  2. r-package-devel 2023-07 (Russell Almond / Ivan Krylov):
    • Issue: A package's Makevars.win added a custom non-file .PHONY prerequisite to $(SHLIB). Because winshlib.mk used nm $^ (all prerequisites), nm failed trying to read the .PHONY target. Providing a .def file bypassed the nm $^ step.

4. Why Do "Bedrock" Packages (stats, grid, Matrix) Use *-win.def?

  1. Base R Heritage: stats-win.def and grid-win.def were written by R Core during the early development of R on Windows. When R-exts was written, stats was used as the canonical textbook example.
  2. Matrix: Matrix is maintained by R Core members (Martin Mächler, Douglas Bates, Mikael Jagan) and strictly follows Base R development practices.
  3. Protection against unintended C-level binding: While packages cannot collide by accident on Windows, a downstream package could theoretically generate an import library from a DLL and bind directly to non-API internal C routines if they are exported in the PE table. Matrix-win.def prevents downstream packages or external tools from bypassing Matrix's formal R_RegisterCCallable / R_GetCCallable interface.

5. Practical Evaluation for rlang and r-lib

Consideration With rlang-win.def Without rlang-win.def
R-level safety (.Call) Protected via R_useDynamicSymbols(dll, FALSE) Protected via R_useDynamicSymbols(dll, FALSE)
C Callable API (R_RegisterCCallable) Fully functional (uses R-internal registry in R.dll, not PE exports) Fully functional
Accidental collision risk None None (PE two-level namespace)
DLL Size Shaves a few KB of string names from .edata Negligible difference
Maintenance Burden Need to remember to update / maintain .def files across repos Zero extra maintenance

Recommended Response for the PR

You can reply along the following lines:

  1. Windows dynamic linking is safe by design: Because Windows uses explicit, two-level import/export tables rather than ELF's flat symbol scope, un-hidden C symbols in rlang.dll cannot cause runtime symbol interposition or collisions with other packages.
  2. R-level registration already seals the boundary: rlang already sets R_useDynamicSymbols(dll, FALSE) and registers its call routines with R_registerRoutines(), ensuring R itself never attempts dynamic symbol lookup via GetProcAddress.
  3. The *-win.def files in {stats} and {Matrix} are primarily stylistic/historical Base R conventions to ensure clean PE export headers and prevent third-party C code from linking directly against internal routines instead of using R_GetCCallable().
  4. Conclusion: Adding rlang-win.def is harmless and achieves parity with POSIX $(C_VISIBILITY), but there is no compelling technical reason to mandate it across r-lib packages or build tooling support in usethis. Leaving it out is completely fine.

@MichaelChirico
MichaelChirico deleted the patch-7 branch August 27, 2026 18:57
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.

2 participants