Contributions to open62541 include code, documentation, answering user questions, running the project's infrastructure, and advocating for all types of open62541 users.
The open62541 project welcomes all contributions from anyone willing to work in good faith with other contributors and the community. No contribution is too small and all contributions are valued.
This guide explains the process for contributing to the open62541 project's core repository and describes what to expect at each step. Thank you for considering these point.
Your friendly open62541 community!
The open62541 project has a Code of Conduct that all contributors are expected to follow. This code describes the minimum behavior expectations for all contributors.
Everybody can propose a pull request (PR). But only the core-maintainers of the project can merge PR.
The following are the minimal requirements that every PR needs to meet.
-
Pass Continuous Integration (CI): Every PR has to pass our CI. This includes compilation with a range of compilers and for a range of target architectures, passing the unit tests and no detected issues with static code analysis tools.
-
Code-Style: Please consider the Code-Style recommendations when formatting your code.
-
Signed CLA: Every contributor must sign the Contributor License Agreement (CLA) before we can merge his first PR. The signing can be done online. The link automatically appears on the page of the first PR. In addition, the CLA text can be accessed here.
-
Separation of Concerns: Small changes are much easier to review. Typically, small PR are merged much faster. For larger contributions, it might make sense to break them up into a series of PR. For example, a PR with a new feature should not contain other commits with only stylistic improvements to another portion of the code.
-
Feature Commits: The same holds true for the individual PR as well. Every commit inside the PR should do one thing only. If many changes have been applied at the same time,
git add --patch
can be used to partially stage and commit changes that belong together. -
Commit Messages: Good commit messages help in understanding changes. See the next section.
-
Linear Commit History: Our goal is to maintain a linear commit history where possible. Use the
git rebase
functionality before pushing a PR. Usegit rebase --interactive
to squash bugfix commits.
These labels can be used for the PR title to indicate its status.
[WIP]
: The PR is work in progress and at this point simply informative.[Review]
: The PR is ready from the developers perspective. He requests a review from a core-maintainer.[Discussion]
: The PR is a contribution to ongoing technical discussions. The PR may be incomplete and is not intended to be merged before the discussion has concluded.
The core-maintainers are busy people. If they take especially long to react, feel free to trigger them by additional comments in the PR thread. Again, small PR are much faster to review.
It is the job of the developer that posts the PR to rebase the PR on the target branch when the two diverge.
The public API is the collection of header files in the /include folder.
Changes to the public API are under especially high scrutiny. Public API changes are best discussed with the core-maintainers early on. Simply to avoid duplicate work when changes to the proposed API become necessary.
You can create a special issue or PR just for the sake of discussing a proposed API change. The actual implementation can follow later on.
Breaking changes are frowned upon. If you cannot avoid API changes, try to create a new one and keep the old API alive (maybe with an addition UA_DEPRECATED macro).
Breaking changes have to be mentioned in the CHANGELOG file.
We have very precise rules over how our git commit messages can be formatted. This leads to more readable messages that are easy to follow when looking through the project history. But also, we use the git commit messages to generate the change log.
This convention is identical to the Conventional Commits specification or the one used by Angular.
Each commit message consists of a Header, a Body and a Footer. The header has a special format that includes a Type, a Scope and a Subject:
<type>(<scope>): <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
The Header is mandatory and the Scope of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier to read on GitHub as well as in various git tools.
The footer should contain a closing reference to an issue if any.
Samples: (even more samples)
docs(server): add function documentation
fix(core): fix parsing of endpoint url
Parsing of endpoint urls now also supports https
If the commit reverts a previous commit, it should begin with revert:
,
followed by the header of the reverted commit. In the body it should say: This reverts commit <hash>.
, where the hash is the SHA of the commit being reverted.
The commit Type Must be one of the following:
- build: Changes that affect the build system or external dependencies
- ci: Changes to our CI configuration files and scripts (example scopes: travis, appveyor, fuzz)
- docs: Documentation only changes
- feat: A new feature
- fix: A bug fix
- perf: A code change that improves performance
- refactor: A code change that neither fixes a bug nor adds a feature
- style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)
- test: Adding missing tests or correcting existing tests
The commit Scope is optional, but recommended to be used. It should be the name of the component which is affected (as perceived by the person reading the changelog generated from commit messages). The following is the list of supported scopes:
- arch: Changes to specific architecture code in
root/arch
- el: Changes to the eventloop and associated event sources (also networking)
- client: Changes only affecting client code
- core: Core functionality used by the client and server
- ex: Example code changes
- mt: Changes specifically for multithreading
- nc: Nodeset compiler
- pack: Packaging setting changes
- plugin: Change to any (optional) plugin
- pubsub: Changes to the pubsub code
- sec: Changes to security, encryption, etc.
- server: Changes only affecting server code
The Subject contains a succinct description of the change:
- Use the imperative, present tense: "change" not "changed" nor "changes"
- Don't capitalize the first letter
- No dot (.) at the end
For the Body, Just as in the Subject, use the imperative, present tense: "change" not "changed" nor "changes". The body should include the motivation for the change and contrast this with previous behavior.
The Footer should contain any information about Breaking Changes and is also the place to reference GitHub issues that this commit Closes.
- Avoid the use of Magic Numbers.
For example use
UA_STATUSCODE_BADINTERNALERROR
instead of0x80020000
- Avoid using global variables. If you for some reason really need to use
global variables, prefix them with
g_
so that it is immediately clear in the code, that we are dealing with a global variable. - Almost all of the code is indented by four (4) spaces and regardless of which is better, spaces or tabs, you should indent with four spaces in order to be consistent.
- If you find yourself copy-pasting code, consider refactoring it into a function and calling that. Try keeping functions short (about 20-30 lines, that way they will in most cases fit onto the screen). In case of simple switch statements for example this can be exceeded, but there is not much complexity that needs to be understood. Most of the time, if functions are longer than 30 lines, they can be refactored into smaller functions that make their intent clear.
- Use of comments
Use C-style comments as follows:
/* Lorem ipsum dolor sit amet */
/* Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy
* eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
* voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita
* kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem
* ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod
* tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
* At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
* gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. */
Note that public header files (in the /include directory) use comments beginning with a double-star to generate documentation. Just follow along the formatting that is already in place.
/** This is documentation */
Every (good) editor can reflow comments for nice formatting.
Do not use C++ comments (double-slash //
).
See the following general rules for comments: [1]
Comments are good, but there is also a danger of over-commenting. NEVER try to explain HOW your code works in a comment: it's much better to write the code so that the working is obvious, and it's a waste of time to explain badly written code.
Generally, you want your comments to tell WHAT your code does, not HOW. Also, try to avoid putting comments inside a function body: if the function is so complex that you need to separately comment parts of it, you should probably go back to chapter 6 for a while. You can make small comments to note or warn about something particularly clever (or ugly), but try to avoid excess. Instead, put the comments at the head of the function, telling people what it does, and possibly WHY it does it.
- Public methods are prefixed with
UA_
. If a public method operates on a server it begins withUA_Server_
and takes a pointer toUA_Server
as its first arguments (the same for the client). - Methods and instances are camelCased starting with lower case
Examples:
UA_Server_new
,UA_Int32_calculateSize
- Use const on variables that will not be changed. For example in functions,
make all parameters const, that are not used as out arguments. When declaring
local variables in a function make them const, if you know that they will not
be modified. When using pointers, remember to declare both the pointer and
the actual type as const, if they are never modified:
const int *const myInt
- In- variables should be always in front of the out- variables in argument lists
- Public types are prefixed with
UA_
- Types are CamelCased starting with upper case. Example:
UA_ResponseHeader
- Don't use
var = {0}
to zero-initialize variables. This results in warnings for some exotic environments (e.g. some versions of the Android NDK). Usememset(ptr, 0, size)
insted.
- Constants and defines are in full capital letters, eg.:
UA_CONST
- Enums are preferred when defining several related constants, for example the
different states of a secure channel:
typedef enum { UA_SECURECHANNELSTATE_FRESH, UA_SECURECHANNELSTATE_OPEN, UA_SECURECHANNELSTATE_CLOSED } UA_SecureChannelState;
- Variables should be descriptive and use camelCase starting with a lower case letter.
- Some commonly known abbreviations or variable name conventions can be used.
This includes for example loop variables like
i
and temporary variables liketmp
. Use common sense, and think about if you would still understand your own code if you looked at it two weeks in the future. - Do not use Hungarian notation. The code base so far does not use it and neither should you, to be consistent. For some reasons, why Hungarian notation might be bad, see here
- Braces: Opening braces start on the same line as a function, if statement,
etc. and the closing brace is placed on a separate line. If the next keyword
after a closing braces logically belongs to the previous keyword (i.e. if and
else), the keyword is placed next to the closing brace on the same line with
one space in between. Example:
if(a) { doSomething(); } else if(b) { doSomething(); } else doSomething();
- Switch statement: Do not indent the case keyword and place the content of
each case in new lines that are indented. If using a code block to limit the
scope of variables, follow the above convention. Example:
switch(a) { case 0: doA(); case 1: { doB(); doC(); } }
- Align multiline expressions. Example:
int a = 1 + 2 + 3; int b = ((1) + 2) - (4 * 5 / 6 % 7);
- Keep the number of blank lines to a minimum, but leave some if it makes the code more readable.
- Surround binary operators with spaces except for member access operators. Do not use spaces around unary operators.
- Do not place a space between a keyword and following parentheses.
- Do not place spaces within parentheses.
- Place a space before, but not after a pointer symbol (*). Example:
const int *const **const myPtr;
If you are using CLion you can configure your IDE by importing this scheme.
Tests are contained in the /tests
dir and start with check_ prefix followed by a
package name (corresponding .c file).
example: check_encoder.c, check_securityLayer.c
Error handling and corresponding reactions like cleanup, logging or updating internal error states is important for a stable program flow.
For checking return values within functions there are specific macros that are used. Handling check values and errors belongs to three different categories:
- Handling
UA_StatusCode
check values - Handling memory allocation errors
- Handling general
boolean
check values
A very frequent use case is the checking of a UA_StatusCode
return value and
performing a specific action upon error. The workflow and usage is the same for
all of the above categories. The basic check macro follows the structure
UA_CHECK_STATUS(STATUSCODE, EVAL_ON_ERROR)
, performing a check on the
STATUSCODE
and execute EVAL_ON_ERROR
if an erroneous state is detected. The
below example thus checks rv
and executes return rv
on error.
UA_StatusCode rv = someAction();
UA_CHECK_STATUS(rv, return rv);
Similar to the above there exist logging variants for each check macro that logs
to a specific logging level. The macro names are appended with a suffix
_FATAL
, _ERROR
, _WARN
or _INFO
.
In this code snippet, the status code gets checked with a specified error action, while logging a specified error level message:
UA_Logger *logger = &server->config.logger;
UA_StatusCode rv = someAction();
/* logging "error" level with the "_ERROR" suffix */
UA_CHECK_STATUS_ERROR(rv, return rv, logger, UA_LOGCATEGORY_SERVER, "my message");
In some situations it is nice to use the goto
command in C. This is the case
e.g. for jumping to cleanup routines that are used to perform a series of commands.
For the development of open62541
we employ a specific rule for which cleanup
routines should be used: If the same cleanup routine is used from at least two
places in the same function then this routine should be called via a goto
statement.
In this example e.g. only the second error check needs to free up some memory upon encountering an error, thus a jump to a cleanup routine is not indicated.
void noCleanupRoutine(void *data) {
data = malloc(sizeof(int));
UA_CHECK_MEM(data, return UA_STATUSCODE_BADOUTOFMEMORY);
UA_StatusCode rv = do_something();
UA_CHECK_STATUS(rv, free(data); return rv);
return UA_STATUSCODE_GOOD;
}
The below example on the other hand, shows two functions that use the same
routine for cleanup. A goto
routine should thus be used.
void yesCleanupRoutine(void *data) {
data = malloc(sizeof(int));
UA_CHECK_MEM(data, return UA_STATUSCODE_BADOUTOFMEMORY);
/* jumps to cleanup routine upon encountering a bad statuscode */
UA_StatusCode rv = do_something();
UA_CHECK_STATUS(rv, goto cleanup);
/* jumps to the same cleanup routine */
rv = do_something_else();
UA_CHECK_STATUS(rv, goto cleanup);
return UA_STATUSCODE_GOOD;
cleanup:
free(data);
return rv;
}
In this example only the status codes are checked and the corresponding action is evaluated on error. There is no message logging included.
static UA_StatusCode
foo(int *errorCounter) {
UA_StatusCode rv = do_something();
/* if rv != UA_STATUSCODE_GOOD then "return rv" gets evaluated */
UA_CHECK_STATUS(rv, return rv);
rv = do_another_thing();
/*
EVAL_ON_ERROR can take multiple statements
(e.g. first modifying some value then return the error code)
*/
UA_CHECK_STATUS(rv, rv = UA_STATUSCODE_BAD; errorCounter++; return rv);
return UA_STATUSCODE_GOOD;
}
In this example status codes are checked while logging message with different
levels and a specified LOGCATEGORY
are generated with a given logger.
static UA_StatusCode
foo(UA_Server *server) {
/* assign the logger for later simple use */
UA_Logger *logger = &server->config.logger;
UA_StatusCode rv = do_something_error();
/*
if rv != UA_STATUSCODE_GOOD then
an error logging message is generated and
"return rv" gets evaluated
*/
UA_CHECK_STATUS_ERROR(rv, return rv,
logger, UA_LOGCATEGORY_SERVER,
"My error logging message with special info: %d", 42);
rv = do_something_warn();
UA_CHECK_STATUS_WARN(rv, return rv,
logger, UA_LOGCATEGORY_SERVER,
"My warning logging message with special info: %d", 42);
rv = do_something_info();
UA_CHECK_STATUS_INFO(rv, return rv,
logger, UA_LOGCATEGORY_SERVER,
"My info logging message with special info: %d", 42);
rv = do_something_fatal();
UA_CHECK_STATUS_FATAL(rv, return rv,
logger, UA_LOGCATEGORY_SERVER,
"My fatal logging message with special info: %d", 42);
return UA_STATUSCODE_GOOD;
}
All the other check macros function analogously to the above UA_CHECK_STATUS
examples,
with a short example to explain the basic usage:
static UA_StatusCode
foo() {
/* assign the logger for later simple use */
UA_Logger *logger = &server->config.logger;
UA_Boolean mustBeTrue = do_something();
/* if mustBeTrue != true then "return UA_STATUSCODE_BAD" gets evaluated */
UA_CHECK(mustBeTrue, return UA_STATUSCODE_BAD);
UA_StatusCode rv = UA_STATUSCODE_GOOD;
/*
EVAL_ON_ERROR can take multiple statements
(e.g. first assigning some value then return the error code)
*/
UA_CHECK(mustBeTrue, rv = UA_STATUSCODE_BAD; return rv);
UA_Boolean mustBeTrue = do_something_else();
/*
if mustBeTrue != true then
an error logging message is generated and
"return UA_STATUSCODE_BAD" gets evaluated
*/
UA_CHECK_ERROR(mustBeTrue, return UA_STATUSCODE_BAD,
logger, UA_LOGCATEGORY_SERVER,
"My logging message with special info: %d", 42);
void *data = malloc(1, 1);
UA_CHECK_MEM(data, return UA_STATUSCODE_BADOUTOFMEMORY);
void *data2 = malloc(1, 1);
UA_CHECK_MEM_ERROR(data2, return UA_STATUSCODE_BADOUTOFMEMORY,
logger, UA_LOGCATEGORY_SERVER,
"My logging message with special info: %d", 42);
return UA_STATUSCODE_GOOD;
}
If any questions arise concerning code style, feel free to start an issue.