doc: document the cross compilation feature

This commit is contained in:
Marius Gerbershagen 2025-10-19 17:28:56 +02:00
parent e30c37691b
commit 872622bd0c
2 changed files with 259 additions and 0 deletions

View file

@ -4,6 +4,7 @@
@menu
* Compiling with ECL::
* Compiling with ASDF::
* Cross compilation::
* C compiler configuration::
@end menu
@c * Compiling with Matroska::
@ -538,6 +539,212 @@ here, since our Lisp library is statically bundled with the executable.
The result is the same as the shared library example above. You can also
build all dependent libraries separately as static libraries.
@node Cross compilation
@subsection Cross compilation
ECL supports cross compiling Lisp files for a target system that differs
from the host system on which the compilation takes place. This section
of the manual describes how to use this feature and explains important
things to keep in mind when cross compiling Common Lisp.
@node Getting started with cross compilation
@subsubsection Getting started with cross compilation
To get started, follow the steps described below:
@enumerate
@item
Cross compile ECL itself following the steps described in @ref{Building
ECL}.
@item
Step one will create a Lisp file called @file{target-info.lsp}
containing all the information necessary for the ECL compiler to create
compiled files for the chosen target system. Load this information into
a running ECL process on the host system by calling
@code{c:read-target-info} on this file.
@item
Supply the resulting target information to the @code{:target} option of
@coderef{compile-file} or @coderef{with-compilation-unit}.
@end enumerate
For an example, consider two files @file{a.lisp} and @file{b.lisp} which
are supposed to be linked into a shared library. This can be
accomplished with the following steps:
@example
(defvar *target* (c:read-target-info "/path/to/ecl/installation/lib/ecl-xx.x.x/target-info.lsp"))
(compile-file "a.lisp" :target *target* :system-p t)
(load "a.lisp") ; make macro definitions in "a.lisp" accessible to "b.lisp"
(compile-file "b.lisp" :target *target* :system-p t)
(with-compilation-unit (:target *target*)
(c:build-shared-library "example"
:lisp-files '("a.o" "b.o")
:init-name "init_example"))
@end example
Cross compilation using ASDF is not supported yet but is planned for the
future.
@node Limitations and pitfalls for cross compiling Common Lisp
@subsubsection Limitations and pitfalls for cross compiling Common Lisp
The ubiquity with which typical Common Lisp programs run code at compile
time in macros or other similar constructs makes cross compilation
somewhat more challenging than in other programming languages. Since
compilation happens on a different system than the one in which the code
generated by a macro is run, differences between the host and target
environment can lead to bugs if the macro hasn't been written with cross
compilation in mind. Consider for instance the following example macro
iterating over prime number in the range from @code{a} to @code{b}:
@example
(defmacro do-prime-numbers ((a b) p &body body)
`(loop with ,p = (next-prime ,a)
until (> ,p ,b)
do ,(if (typep b 'fixnum)
`(locally (declare (fixnum ,p))
,@@body)
`(progn ,@@body))
(setf ,p (next-prime (1+ ,p)))))
@end example
While this macro works fine in a standard setting, it will produce
an incorrect declaration when cross compiling if the size of a fixnum
is larger in the host than in the target and the upper bound @code{b} is
larger than @code{most-positive-fixnum} in the target but smaller than
@code{most-positive-fixnum} in the host. In general, any observable
difference between target and host system may contribute to such issues.
In the list below, we collect a number of such potential issues and
explain strategies to solve them.
@itemize
@item
Different type or subtype definitions. This may arise from differing
word sizes, as in the fixnum example above, or different feature support
such as complex floating point number support in the C compiler leading
to differing subtype relationships for complex numbers in Lisp. There
are two main strategies for dealing with this problem.
@enumerate
@item
Avoid the problem by rewriting the code not to rely on target specific
information. For instance, in the above example we can change the
declaration as follows to avoid relying on fixnums:
@example
(defmacro do-prime-numbers ((a b) p &body body)
`(loop with ,p = (next-prime ,a)
until (> ,p ,b)
do (locally (declare (type (integer ,a ,b) ,p))
,@@body)
(setf ,p (next-prime (1+ ,p)))))
@end example
If the declared integer type is a subtype of fixnum, the ECL compiler
will automatically take this into account and optimize the same as if a
fixnum declaration had been made.
@item
Use the lexical environment parameter to @clhs{f_typep.htm,typep} and
@clhs{f_subtpp.htm,subtypep}. Both functions take an optional parameter
which can be used to supply information about type relationships in the
target system. In the above example, this works as follows:
@example
(defmacro do-prime-numbers ((a b) p &body body &environment env)
`(loop with ,p = (next-prime ,a)
until (> ,p ,b)
do ,(if (typep b 'fixnum env)
`(locally (declare (fixnum ,p))
,@@body)
`(progn ,@@body))
(setf ,p (next-prime (1+ ,p)))))
@end example
It is important, however, not to blindly modify every occurrence of
@code{typep} and @code{subtypep} in this way. Some calls to these
functions may check for types in the host environment, in which case
they should not receive an environment parameter. It is necessary to
know for which environment the call is meant in order to decide what to
do.
@end enumerate
@item
Different value of @code{*features*}, leading to mismatched @code{#+}
and @code{#-} read time conditionals. During cross compilation,
@code{*features*} will be rebound to the value in the target
system.@footnote{Changes to @code{*features*} during compilation are
carried over in the target information structure. This means that in a
scenario where one cross compiles a file @file{a.lisp} containing
@code{(eval-when (:compile-toplevel) (push :my-feature *features*))}
before cross compiling another file @file{b.lisp} containing
@code{#+my-feature}, the read time conditional in @file{b.lisp} will
evaluate to true.} However, macros that are defined in the host system
will see the host @code{*features*} during read time. Consider again the
example given above where @file{a.lisp} was cross compiled, then loaded
before @file{b.lisp} was cross compiled. If @file{a.lisp} contains a
macro definition
@example
(defmacro my-macro (...)
#+android `(do-something ...)
#-android `(do-something-else ...))
@end example
then @code{my-macro} will expand to @code{do-something} in @file{a.lisp}
but @code{do-something-else} in @file{b.lisp} if cross compiling for the
android target. There are several ways to deal with this issue:
@enumerate
@item
Load source files containing macro definitions in an environment where
@code{*features*} has the same value as in the target system. This can
be accomplished either by wrapping @code{load} in a
@coderef{with-compilation-unit} call with a @code{:target} option given
or by using the @code{:load} keyword option of @coderef{compile-file} in
conjunction with @code{:target}. In the above example, we would compile
as follows:
@example
(compile-file "a.lisp" :target *target* :system-p t :load t)
(compile-file "b.lisp" :target *target* :system-p t)
@end example
Note that this will not work if the file contains read time conditionals
selecting between different compile time code paths due to differences
in the host system (let's say for example some code generation from a
file that is located in a different location for different compilation
hosts).
@item
Replace read time conditionals by lookups that run during macro
expansion, e.g.
@example
(defmacro my-macro (...)
(if (member :android *features*)
`(do-something ...)
`(do-something-else ...)))
@end example
@item
Add feature keywords for the target system to @code{*features*} in the
host before compilation. This is best suited for conditionals which only
add but not subtract code (keywords which only appear in @code{#+}
conditionals). In our example, we would call
@example
(push :android *features*)
@end example
before starting cross compilation.
@end enumerate
@item
Floating point accuracy issues. ECL relies on the C standard library for
numerical functions which may return slightly different values for
different systems. If your code depends sensitively on numerical
functions returning bitwise identical results, it is best not to run any
numerical code at compile time and restrict yourself to only running run
time code.
@end itemize
Another option to avoid the aformentioned issues entirely is to use
emulation instead of cross compilation. Even if full-blown emulation is
too complicated, simply cross compiling from a version of ECL compiled
for the same word size as the target system would for instance avoid the
fixnum issues in our example macro alltogether.
Whether the compiler is switched to cross compilation mode or not can be
diagnosed from the presence of a @code{:cross} keyword in
@code{*features*}. If necessary, this can be used to select different
code paths as well.
@node C compiler configuration
@subsection C compiler configuration

View file

@ -1,6 +1,58 @@
@node System construction
@section System construction
@node System construction - Dictionary
@subsection Dictionary
@subsubsection Extensions
See also @ref{System building}.
@lspdef compile-file
@defun compile-file input-file &key output-file verbose print external-format target system-p load c-file h-file data-file
Additional options for @clhs{f_cmp_fi.htm,compile-file} include:
@table @code
@item :system-p
Create an object file (.o)
@item :target
Cross compilation target, see @ref{Cross compilation}. Can be either the
target information obtained by calling @code{c:read-target-info} or a
pathname pointing to the installation directory of the target ECL for
which to compile.
@item :load
Load the output file after compilation. If a @code{:target} argument is
given alongside @code{:load}, load the source file in the target
environment.
@item :c-file, :h-file, :data-file
Boolean flags controlling whether to keep temporary files created by the compiler
@end table
@end defun
@lspdef compile-file-pathname
@defun compile-file-pathname input-file &key output-file type system-p
Additional options for @clhs{f_cmp__1.htm,compile-file-pathname} include:
@table @code
@item :type
Specify the type of output file. One of @code{:fasl} (default, alias @code{:fas}), @code{:c}, @code{:h}, @code{:data}, @code{:object}, @code{:program}, @code{:shared-library} (alias @code{:dll}), @code{:static-library} (alias @code{:library}, @code{:lib}), @code{:precompiled-header} or @code{:import-library} (MSVC only)
@item :system-p
If no type is supplied, default to @code{:object} type instead of @code{:fasl}
@end table
@end defun
@lspdef with-compilation-unit
@deffn {Macro} with-compilation-unit (&key override target) &body body
Additional options for @clhs{m_w_comp.htm,with-compilation-unit} include:
@table @code
@item :target
Cross compilation target, see @ref{Cross compilation}. Any compilation
operation produces outputs for the specified target system. This
includes @clhs{f_cmp_fi.htm,compile-file},
@clhs{f_disass.htm,disassemble} and all ECL specific functions described
in the section on @ref{Compiling with ECL}. The only exception is
@clhs{f_cmp.htm,compile} which continues to work as usual.
@end table
@end deffn
@subsection C Reference
@subsubsection ANSI Dictionary