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

cmake: add llext compilation module #67431

Merged
merged 3 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmake/bintools/gnu/target_bintools.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ set_property(TARGET bintools PROPERTY strip_flag_final "")
set_property(TARGET bintools PROPERTY strip_flag_all --strip-all)
set_property(TARGET bintools PROPERTY strip_flag_debug --strip-debug)
set_property(TARGET bintools PROPERTY strip_flag_dwo --strip-dwo)
set_property(TARGET bintools PROPERTY strip_flag_remove_section -R )

set_property(TARGET bintools PROPERTY strip_flag_infile "")
set_property(TARGET bintools PROPERTY strip_flag_outfile -o )
Expand Down
2 changes: 2 additions & 0 deletions cmake/compiler/gcc/target.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ elseif("${ARCH}" STREQUAL "sparc")
include(${CMAKE_CURRENT_LIST_DIR}/target_sparc.cmake)
elseif("${ARCH}" STREQUAL "mips")
include(${CMAKE_CURRENT_LIST_DIR}/target_mips.cmake)
elseif("${ARCH}" STREQUAL "xtensa")
include(${CMAKE_CURRENT_LIST_DIR}/target_xtensa.cmake)
endif()

if(SYSROOT_DIR)
Expand Down
19 changes: 19 additions & 0 deletions cmake/compiler/gcc/target_arm.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,22 @@ endif()

list(APPEND TOOLCHAIN_C_FLAGS ${ARM_C_FLAGS})
list(APPEND TOOLCHAIN_LD_FLAGS NO_SPLIT ${ARM_C_FLAGS})

# Flags not supported by llext linker
# (regexps are supported and match whole word)
set(LLEXT_REMOVE_FLAGS
-fno-pic
-fno-pie
-ffunction-sections
-fdata-sections
-g.*
-Os
-mcpu=.*
)

# Flags to be added to llext code compilation
set(LLEXT_APPEND_FLAGS
-mlong-calls
-mthumb
-mcpu=cortex-m33+nodsp
)
21 changes: 21 additions & 0 deletions cmake/compiler/gcc/target_xtensa.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-License-Identifier: Apache-2.0

# Flags not supported by llext linker
# (regexps are supported and match whole word)
set(LLEXT_REMOVE_FLAGS
-fno-pic
-fno-pie
-ffunction-sections
-fdata-sections
-g.*
-Os
-mcpu=.*
)

# Flags to be added to llext code compilation
set(LLEXT_APPEND_FLAGS
-fPIC
-nostdlib
-nodefaultlibs
-shared
)
136 changes: 136 additions & 0 deletions cmake/modules/extensions.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ include(CheckCXXCompilerFlag)
# 5. Zephyr linker functions
# 5.1. zephyr_linker*
# 6 Function helper macros
# 7 Linkable loadable extensions (llext)

########################################################
# 1. Zephyr-aware extensions
Expand Down Expand Up @@ -4786,3 +4787,138 @@ macro(zephyr_check_flags_exclusive function prefix)
)
endif()
endmacro()

########################################################
# 7. Linkable loadable extensions (llext)
########################################################
#
# These functions simplify the creation and management of linkable
# loadable extensions (llexts).
#

# Add a custom target that compiles a single source file to a .llext file.
#
# Output and source files must be specified using the OUTPUT and SOURCES
# arguments. Only one source file is currently supported.
#
# The llext code will be compiled with mostly the same C compiler flags used
# in the Zephyr build, but with some important modifications. The list of
# flags to remove and flags to append is controlled respectively by the
# LLEXT_REMOVE_FLAGS and LLEXT_APPEND_FLAGS global variables.

# The C_FLAGS argument can be used to pass additional compiler flags to the
# compilation of this particular llext.
Copy link
Collaborator

@marc-hb marc-hb Jan 19, 2024

Choose a reason for hiding this comment

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

in CI we use command-line parameters like -DEXTRA_CXXFLAGS=-Werror a lot. Will they work with llext too?

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 am no CMake guru so I just tested - it does (well, with EXTRA_CFLAGS as it's a C file, but it does indeed use command line flags in the llext as well).

#
# Example usage:
# add_llext_target(hello_world
# OUTPUT ${PROJECT_BINARY_DIR}/hello_world.llext
# SOURCES ${PROJECT_SOURCE_DIR}/src/llext/hello_world.c
# C_FLAGS -Werror
# )
# will compile the source file src/llext/hello_world.c to a file
# ${PROJECT_BINARY_DIR}/hello_world.llext, adding -Werror to the compilation.
#
function(add_llext_target target_name)
set(single_args OUTPUT)
set(multi_args SOURCES;C_FLAGS)
cmake_parse_arguments(PARSE_ARGV 1 LLEXT "${options}" "${single_args}" "${multi_args}")

# Check that the llext subsystem is enabled for this build
if (NOT CONFIG_LLEXT)
message(FATAL_ERROR "add_llext_target: CONFIG_LLEXT must be enabled")
endif()
Comment on lines +4827 to +4829
Copy link
Collaborator

@kartben kartben Feb 14, 2024

Choose a reason for hiding this comment

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

Just curious, what would be wrong with trying/wanting to generate an llext for a project that doesn't have LLEXT enabled. One needn't enable LLEXT in a project if they only care about having "snippets" of it built as llext, no?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Well, indeed you could compile only the llext, since there's no real technical barrier. However, the generated file would not work with the same Zephyr core with which it was built (no exported symbols or machinery to load it), so it looked like a config error in my eyes.


Now that I look at it again, the converse is useful though - if you know what you are doing and purposefully disable CONFIG_LLEXT on a project that defines such targets, it makes sense not to build them and not complain, unless they are required by other parts in the project.

Taking into account your other comment on dependencies ("why isn't a llext built if it's defined as part of a project?"), I think the most appropriate way to deal with all this should be:

  • if CONFIG_LLEXT is not enabled, do not complain in add_llext_target and define an empty target instead, so other llext_* APIs don't break as well.
  • automatically add the llext target to ALL so it's always built if it's defined in the CMake.

This would result in an actual build error only when LLEXT is disabled and the output file is a required dependency (for example by generate_inc_file_for_target) - or if the Zephyr code actually calls llext APIs, obviously.

@teburd, what do you think about this?

Copy link
Collaborator

@marc-hb marc-hb Mar 2, 2024

Choose a reason for hiding this comment

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

Sorry for the belated reply and - much worse - sorry for not being @teburd.

disable CONFIG_LLEXT on a project that defines such targets, it makes sense not to build them and not complain,

I probably (!) disagree with that? The main purpose of Kconfig is to turn software features on and off. But "llext" isn't really a software feature, it's more about HOW you want some of those features to be delivered: as a separate module or built-in? So CONFIG_LLEXT is more like a "dependency" of real features.

Consider the Linux kernel, which many llext aspects take inspiration from. It had to already deal with this problem. If you turn off Linux CONFIG_MODULES in make menuconfig then it immediately forces you to make all features built-in. Editing the .config and running make oldefconfig does the same thing.

Of course there's a massive difference: the Linux kernel can very quickly and easily flip drivers that support between module and built-in, whereas llext does not do that all. But that "inspiration" is still useful IMHO.

So if you:

  • enable some particular llext feature in Kconfig, AND
  • you turn off CONFIG_LLEXT

... then you have a configuration error and the build should 1) fail, 2) fail as soon as possible, not wait until compilation whether some symbol is used maybe, maybe not.

In other words, if you turn off CONFIG_LLEXT then you MUST also turn off all features that depend on it - and maybe Kconfig can/should help with that but either way CMake should fail ASAP if you feed it an inconsistent configuration.

Copy link
Collaborator Author

@pillo79 pillo79 Mar 4, 2024

Choose a reason for hiding this comment

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

[...] "llext" isn't really a software feature, it's more about HOW you want some of those features to be delivered [...]

In fact, llext is only the implementation enabling the "dynamic loading" feature. The HOW you want [features] to be delivered part is CONFIG_MODULES, which was recently merged in Zephyr. No actual in-tree users yet though - having dynamically loadable parts of the Zephyr core is still far out.

Given that discussion and the existence of both MODULES and LLEXT, I think what you are describing above is a strict dependency between them: either MODULES implies LLEXT, or depends on it, or there's a config error. Absolutely agree with that!

However, and this is when MODULES=n, I still maintain there is merit in building llext-related projects with LLEXT=n without complaining. This would enable scenario 1 of the following 3:

  1. if projects want optional extension support, with a few strategic #ifdef CONFIG_LLEXT they are able to build either way;
  2. if they need extension support and do not care about optionality, they just enable it in their prj.conf;
  3. if they require the functionality but there is a config error, no core functions are defined so the mistake is definitely detected at link time.

OT: We need an RFC / implementation plan on llext pronto! 😅 . I'll try to draft one shortly and share it so we can all agree on the long term path. 👍

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'll try to draft one shortly and share it so we can all agree on the long term path

Thank you so much for leading here again. Whatever are the use cases and intended Kconfig interactions, I think we all agree they cannot just be inferred from scattered comments in source and code reviews. Some high-level design documentation is required indeed.


# Output file must be provided
if(NOT LLEXT_OUTPUT)
message(FATAL_ERROR "add_llext_target: OUTPUT argument must be provided")
endif()

# Source list length must currently be 1
list(LENGTH LLEXT_SOURCES source_count)
if(NOT source_count EQUAL 1)
message(FATAL_ERROR "add_llext_target: only one source file is supported")
endif()

set(output_file ${LLEXT_OUTPUT})
set(source_file ${LLEXT_SOURCES})
get_filename_component(output_name ${output_file} NAME)

# Add user-visible target and dependency
add_custom_target(${target_name}
COMMENT "Compiling ${output_name}"
DEPENDS ${output_file}
)

# Convert the LLEXT_REMOVE_FLAGS list to a regular expression, and use it to
# filter out these flags from the Zephyr target settings
list(TRANSFORM LLEXT_REMOVE_FLAGS
REPLACE "(.+)" "^\\1$"
OUTPUT_VARIABLE llext_remove_flags_regexp
)
string(REPLACE ";" "|" llext_remove_flags_regexp "${llext_remove_flags_regexp}")
set(zephyr_flags
"$<TARGET_PROPERTY:zephyr_interface,INTERFACE_COMPILE_OPTIONS>"
)
set(zephyr_filtered_flags
"$<FILTER:${zephyr_flags},EXCLUDE,${llext_remove_flags_regexp}>"
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is scary stuff :-) How about adding a couple message(DEBUG $llext_remove_flags_regexp) and message(DEBUG zephyr_filtered_flags)?
I'm surprised you didn't need them for yourself!

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 used plenty of them for debugging indeed 😇 but I did not push them, as they are quite cumbersome: to see the end result to that gibberish you need to add a custom_target that echoes the flags on the console. The generator syntax is next-level painful to me!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah yes of course, it's build-time. Got it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

cmake is next level painful, thanks for taking this on

)

# Compile the source file to an object file using current Zephyr settings
# but a different set of flags
add_library(${target_name}_lib OBJECT ${source_file})
teburd marked this conversation as resolved.
Show resolved Hide resolved
target_compile_definitions(${target_name}_lib PRIVATE
$<TARGET_PROPERTY:zephyr_interface,INTERFACE_COMPILE_DEFINITIONS>
)
target_compile_options(${target_name}_lib PRIVATE
${zephyr_filtered_flags}
${LLEXT_APPEND_FLAGS}
${LLEXT_C_FLAGS}
)
target_include_directories(${target_name}_lib PRIVATE
$<TARGET_PROPERTY:zephyr_interface,INTERFACE_INCLUDE_DIRECTORIES>
)
target_include_directories(${target_name}_lib SYSTEM PUBLIC
$<TARGET_PROPERTY:zephyr_interface,INTERFACE_SYSTEM_INCLUDE_DIRECTORIES>
)
add_dependencies(${target_name}_lib
zephyr_interface
zephyr_generated_headers
)

# Arch-specific conversion of the object file to an llext
if(CONFIG_ARM)

# No conversion required, simply copy the object file
add_custom_command(
OUTPUT ${output_file}
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_OBJECTS:${target_name}_lib> ${output_file}
DEPENDS ${target_name}_lib $<TARGET_OBJECTS:${target_name}_lib>
)

elseif(CONFIG_XTENSA)

# Generate an intermediate file name
get_filename_component(output_dir ${output_file} DIRECTORY)
get_filename_component(output_name_we ${output_file} NAME_WE)
set(pre_output_file ${output_dir}/${output_name_we}.pre.llext)

# Need to convert the object file to a shared library, then strip some sections
add_custom_command(
OUTPUT ${output_file}
BYPRODUCTS ${pre_output_file}
COMMAND ${CMAKE_C_COMPILER} ${LLEXT_APPEND_FLAGS}
-o ${pre_output_file}
$<TARGET_OBJECTS:${target_name}_lib>
COMMAND $<TARGET_PROPERTY:bintools,strip_command>
$<TARGET_PROPERTY:bintools,strip_flag>
$<TARGET_PROPERTY:bintools,strip_flag_remove_section>.xt.*
$<TARGET_PROPERTY:bintools,strip_flag_infile>${pre_output_file}
$<TARGET_PROPERTY:bintools,strip_flag_outfile>${output_file}
$<TARGET_PROPERTY:bintools,strip_flag_final>
DEPENDS ${target_name}_lib $<TARGET_OBJECTS:${target_name}_lib>
)

else()
message(FATAL_ERROR "add_llext_target: unsupported architecture")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Removed and added back, looks like a small git glitch.

endif()
endfunction()
34 changes: 7 additions & 27 deletions tests/subsys/llext/hello_world/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,34 +7,14 @@ project(hello_world)

if(NOT CONFIG_MODULES OR CONFIG_LLEXT_TEST_HELLO STREQUAL "m")

# TODO check which architecture is being used
if(CONFIG_ARM)
set(CMAKE_C_FLAGS "-mlong-calls" "-mthumb")
set(llext_src_file ${PROJECT_SOURCE_DIR}/hello_world.c)
set(llext_bin_file ${PROJECT_BINARY_DIR}/hello_world.llext)

add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/hello_world.llext
COMMAND ${CMAKE_C_COMPILER} ${CMAKE_C_FLAGS} -c
-I ${PROJECT_SOURCE_DIR}/../../../../include
-imacros${PROJECT_BINARY_DIR}/../zephyr/include/generated/autoconf.h
-o ${PROJECT_BINARY_DIR}/hello_world.llext
${PROJECT_SOURCE_DIR}/hello_world.c
)
elseif(CONFIG_XTENSA)
set(CMAKE_C_FLAGS "-shared" "-fPIC" "-nostdlib" "-nodefaultlibs")
add_llext_target(hello_world
OUTPUT ${llext_bin_file}
SOURCES ${llext_src_file}
)

add_custom_command(OUTPUT ${PROJECT_BINARY_DIR}/hello_world.llext
COMMAND ${CMAKE_C_COMPILER} ${CMAKE_C_FLAGS}
-I ${PROJECT_SOURCE_DIR}/../../../../include
-imacros${PROJECT_BINARY_DIR}/../zephyr/include/generated/autoconf.h
-o ${PROJECT_BINARY_DIR}/hello_world.pre.llext
${PROJECT_SOURCE_DIR}/hello_world.c
COMMAND ${CROSS_COMPILE}strip -R .xt.*
-o ${PROJECT_BINARY_DIR}/hello_world.llext
${PROJECT_BINARY_DIR}/hello_world.pre.llext
)
endif()

set(HELLO_WORLD_LLEXT ${PROJECT_BINARY_DIR}/hello_world.llext PARENT_SCOPE)

add_custom_target(hello_world DEPENDS ${PROJECT_BINARY_DIR}/hello_world.llext)
set(HELLO_WORLD_LLEXT ${llext_bin_file} PARENT_SCOPE)

endif()
Loading